Compare commits

...
Author SHA1 Message Date
King a4b05ef564 修正版本号残留 2026-06-11 03:56:37 +08:00
King 274078084e 首页诸多细节优化 2026-06-11 03:52:14 +08:00
King d57b02b832 跨盘秒传新增线路与多项修复 2026-06-11 03:52:14 +08:00
King 861bb63f37 修复Emby 反代不适配 strm v2问题 2026-06-11 03:52:14 +08:00
King e406731d9e 跨盘秒传优化 2026-06-11 03:52:14 +08:00
King c284a9259a 新增跨盘秒传模块 2026-06-11 03:52:14 +08:00
King 032f7c487d 优化重置密码逻辑 2026-06-06 20:31:37 +08:00
66 changed files with 8428 additions and 2991 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
# LitePan v0.2.0-beta
# LitePan v0.3.0-beta
LitePan 是一个多网盘聚合与管理工具,支持 Web 界面、WebDAV、STRM 生成、媒体整理等功能。
+27 -3
View File
@@ -1,6 +1,7 @@
"""管理员 API:账号 CRUD、缓存、认证调度、系统配置。"""
from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse
from typing import Dict, Any
from datetime import datetime, UTC
import time
@@ -15,6 +16,7 @@ from api.responses import error_response as _error_response, success_response as
from core.account_utils import filter_runtime_config as _filter_driver_config, get_account_or_404
from core.driver_service import get_account_driver_instance
from core.security import generate_password_hash, assess_admin_credential_state
from core.session_manager import session_manager
router = APIRouter()
def _mask_sensitive_value(value: Any) -> str:
@@ -1348,7 +1350,17 @@ async def update_admin_credentials(
await apply_admin_home_return_mode()
await apply_theme()
await apply_log_retention_days()
return _success_response(message="管理员设置更新成功")
response = JSONResponse(content=_success_response(message="管理员设置更新成功"))
session_manager.create_session(
response=response,
user_data={
"is_admin": True,
"username": username,
},
remember=bool(session_data.get("remember")),
request=request,
)
return response
if len(password) < 6:
return _error_response(message="密码长度至少6位")
@@ -1357,6 +1369,8 @@ async def update_admin_credentials(
await config_manager.set_async('admin_username', username)
await config_manager.set_async('admin_password', password_hash)
await config_manager.set_async("admin_temp_password_hash", "")
await config_manager.set_async("admin_temp_password_expires_at", 0)
if session_timeout is not None:
await config_manager.set_async('session_timeout', session_timeout_seconds)
if oauth_server_url:
@@ -1368,8 +1382,18 @@ async def update_admin_credentials(
await apply_admin_home_return_mode()
await apply_theme()
await apply_log_retention_days()
return _success_response(message="管理员设置更新成功")
response = JSONResponse(content=_success_response(message="管理员设置更新成功"))
session_manager.create_session(
response=response,
user_data={
"is_admin": True,
"username": username,
},
remember=bool(session_data.get("remember")),
request=request,
)
return response
except Exception as e:
return {
+112 -17
View File
@@ -3,7 +3,7 @@ from fastapi.responses import JSONResponse
from pydantic import BaseModel
from typing import Optional
from core.session_manager import session_manager
from core.security import check_password_hash, assess_admin_credential_state, is_password_hash
from core.security import check_password_hash, assess_admin_credential_state, generate_password_hash, is_password_hash
from core.error_handler import raise_unauthorized
from api.deps import is_public_index_enabled
@@ -25,6 +25,32 @@ class AuthResponse(BaseModel):
data: Optional[dict] = None
message: str = ""
def _now_ts() -> int:
import time
return int(time.time())
def _get_temp_password_state() -> dict:
from config import config_manager
hashed = config_manager.get("admin_temp_password_hash") or ""
expires_at = int(config_manager.get("admin_temp_password_expires_at") or 0)
last_reset_at = int(config_manager.get("admin_temp_password_last_reset_at") or 0)
now = _now_ts()
valid = bool(hashed) and expires_at > now
return {
"hash": str(hashed),
"expires_at": expires_at,
"last_reset_at": last_reset_at,
"valid": valid,
}
_reset_ip_cooldown: dict = {}
@router.post("/login")
async def login(
request: Request,
@@ -35,21 +61,30 @@ async def login(
stored_username, stored_password = get_admin_credentials()
credential_state = assess_admin_credential_state(stored_username, stored_password)
# 兼容早期明文密码:未哈希的直接等值比较,已哈希的走 check_password_hash
temp_state = _get_temp_password_state()
password_match = False
if is_password_hash(stored_password):
password_match = check_password_hash(stored_password, password)
else:
password_match = (password == stored_password)
if username == stored_username and password_match:
temp_password_match = False
if not password_match and username == stored_username and temp_state["valid"] and temp_state["hash"]:
temp_password_match = check_password_hash(temp_state["hash"], password)
if username == stored_username and (password_match or temp_password_match):
must_change_password = bool(credential_state["must_change_password"] or temp_password_match)
password_change_reason = (
"temporary_password" if temp_password_match else credential_state["password_change_reason"]
)
response_data = {
"success": True,
"data": {
"username": username,
"is_admin": True,
"must_change_password": credential_state["must_change_password"],
"password_change_reason": credential_state["password_change_reason"],
"must_change_password": must_change_password,
"password_change_reason": password_change_reason,
},
"message": "登录成功"
}
@@ -61,6 +96,8 @@ async def login(
user_data={
'is_admin': True,
'username': username,
'must_change_password': must_change_password,
'password_change_reason': password_change_reason,
},
remember=(remember == "1"),
request=request
@@ -82,10 +119,16 @@ async def get_auth_status(request: Request) -> AuthResponse:
session_data = session_manager.get_session(request)
is_admin = session_data.get('is_admin', False)
public_index_enabled = await is_public_index_enabled()
credential_state = get_admin_credential_state() if is_admin else {
"must_change_password": False,
"password_change_reason": "",
}
if is_admin and bool(session_data.get("must_change_password")):
credential_state = {
"must_change_password": True,
"password_change_reason": str(session_data.get("password_change_reason") or "temporary_password"),
}
else:
credential_state = get_admin_credential_state() if is_admin else {
"must_change_password": False,
"password_change_reason": "",
}
return AuthResponse(
success=True,
@@ -101,22 +144,74 @@ async def get_auth_status(request: Request) -> AuthResponse:
@router.post("/reset-password")
async def reset_password(request: Request):
"""生成一个随机明文密码塞进库里,新密码打到容器日志。明文形式保证 login 后会强制改密码。"""
"""生成临时密码写入配置(哈希 + 过期时间),不修改原管理员密码。"""
import secrets
import string
import time
from config import config_manager
from core.log_manager import get_writer, LogModule
now = int(time.time())
cooldown_seconds = 60
ttl_seconds = 600
ip = ""
try:
ip = (request.client.host if request.client else "") or ""
except Exception:
ip = ""
if ip:
last_ip = int(_reset_ip_cooldown.get(ip) or 0)
if now - last_ip < cooldown_seconds:
return JSONResponse(content={
"success": False,
"message": "操作过于频繁,请稍后再试。"
})
temp_state = _get_temp_password_state()
if temp_state["valid"]:
remaining = max(0, temp_state["expires_at"] - now)
return JSONResponse(content={
"success": True,
"message": "临时密码仍在有效期内,请查看容器日志获取临时密码。",
"data": {
"expires_at": temp_state["expires_at"],
"remaining_seconds": remaining,
"ttl_seconds": ttl_seconds,
}
})
last_reset_at = int(temp_state["last_reset_at"] or 0)
if now - last_reset_at < cooldown_seconds:
return JSONResponse(content={
"success": False,
"message": "操作过于频繁,请稍后再试。"
})
alphabet = string.ascii_letters + string.digits
new_password = ''.join(secrets.choice(alphabet) for i in range(12))
temp_password = ''.join(secrets.choice(alphabet) for _ in range(12))
temp_password_hash = generate_password_hash(temp_password)
expires_at = now + ttl_seconds
system_log = get_writer(LogModule.SYSTEM)
await config_manager.set_async("admin_temp_password_hash", temp_password_hash)
await config_manager.set_async("admin_temp_password_expires_at", expires_at)
await config_manager.set_async("admin_temp_password_last_reset_at", now)
await config_manager.set_async('admin_password', new_password)
if ip:
_reset_ip_cooldown[ip] = now
get_writer(LogModule.SYSTEM).warning("管理员临时密码已生成,请尽快登录并修改密码。")
print(
f"\n[重置密码] 临时管理员密码: {temp_password} (有效期 {ttl_seconds//60} 分钟,过期后失效;原密码仍可用;使用临时密码登录后需修改密码)\n"
)
print(f"\n[重置密码] 您的新管理员密码是: {new_password} (登录后必须修改)\n")
return JSONResponse(content={
"success": True,
"message": "重置成功,请查看容器控制台日志获取新密码。"
"message": "已生成临时密码,请查看容器控制台日志获取临时密码。",
"data": {
"expires_at": expires_at,
"remaining_seconds": ttl_seconds,
"ttl_seconds": ttl_seconds,
}
})
+190
View File
@@ -0,0 +1,190 @@
"""跨盘秒传管理接口。"""
import asyncio
import json
from typing import List, Optional
from fastapi import APIRouter, Depends, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from api.deps import require_admin_auth
from api.responses import error_response as _error_response, success_response as _success_response
from cross_transfer import build_routes, execute_stream, probe_stream, scan_source
from cross_transfer.relay_task_manager import relay_task_manager
from core.log_manager import LogModule, get_writer
router = APIRouter(prefix="/api/cross-transfer", tags=["跨盘秒传"])
def _log():
return get_writer(LogModule.API)
class ScanRequest(BaseModel):
source_account_id: int
source_parent_id: str
method: str
source_display_path: str = ""
class ProbeFile(BaseModel):
source_file_id: Optional[str] = ""
rel_path: Optional[str] = ""
name: str
size: int = 0
hash: str = ""
class ProbeRequest(BaseModel):
source_account_id: int
target_account_id: int
target_parent_id: str
method: str
files: List[ProbeFile]
class TransferFile(BaseModel):
source_file_id: Optional[str] = ""
rel_path: Optional[str] = ""
rel_dir: Optional[str] = ""
name: str
size: int = 0
hash: str
class ExecuteRequest(BaseModel):
source_account_id: int
source_account_name: str = ""
source_driver_type: str = ""
target_account_id: int
target_account_name: str = ""
target_driver_type: str = ""
target_parent_id: str
target_display_path: str = ""
method: str
files: List[TransferFile]
conflict: str = "rename"
fallback: bool = False
class RelayTaskBatchDeleteRequest(BaseModel):
task_ids: List[str]
@router.get("/routes")
async def get_routes(session_data: dict = Depends(require_admin_auth)):
try:
routes = build_routes()
return _success_response(data=routes, message=f"可用线路 {len(routes)} 条")
except Exception as exc:
_log().error(f"获取跨盘秒传线路失败: {exc}")
return _error_response(message=f"获取线路失败: {exc}", data=[])
@router.post("/scan")
async def scan(req: ScanRequest, session_data: dict = Depends(require_admin_auth)):
try:
result = await scan_source(
source_account_id=req.source_account_id,
source_parent_id=req.source_parent_id,
method_id=req.method,
source_display_path=req.source_display_path,
)
return _success_response(data=result, message=f"已扫描 {result['total']} 个文件")
except Exception as exc:
_log().error(f"跨盘秒传扫描失败: {exc}")
return _error_response(message=f"扫描失败: {exc}")
@router.post("/probe")
async def probe(req: ProbeRequest, session_data: dict = Depends(require_admin_auth)):
async def generate():
try:
async for msg in probe_stream(
source_account_id=req.source_account_id,
target_account_id=req.target_account_id,
target_parent_id=req.target_parent_id,
method_id=req.method,
files=[f.model_dump() for f in req.files],
):
yield json.dumps(msg, ensure_ascii=False) + "\n"
except Exception as exc:
_log().error(f"跨盘秒传试探失败: {exc}")
yield json.dumps({"event": "error", "message": str(exc)}, ensure_ascii=False) + "\n"
return StreamingResponse(generate(), media_type="application/x-ndjson; charset=utf-8")
@router.post("/execute")
async def execute(req: ExecuteRequest, session_data: dict = Depends(require_admin_auth)):
async def generate():
try:
async for msg in execute_stream(
source_account_id=req.source_account_id,
source_account_name=req.source_account_name,
source_driver_type=req.source_driver_type,
target_account_id=req.target_account_id,
target_account_name=req.target_account_name,
target_driver_type=req.target_driver_type,
target_parent_id=req.target_parent_id,
target_display_path=req.target_display_path,
method_id=req.method,
files=[f.model_dump() for f in req.files],
conflict=req.conflict,
fallback=req.fallback,
):
yield json.dumps(msg, ensure_ascii=False) + "\n"
except Exception as exc:
_log().error(f"跨盘秒传执行失败: {exc}")
yield json.dumps({"event": "error", "message": str(exc)}, ensure_ascii=False) + "\n"
return StreamingResponse(generate(), media_type="application/x-ndjson; charset=utf-8")
@router.get("/relay/tasks")
async def list_relay_tasks(session_data: dict = Depends(require_admin_auth)):
try:
tasks = await relay_task_manager.list_tasks()
return _success_response(data=tasks, message="获取跨盘任务成功")
except Exception as exc:
return _error_response(message=f"获取跨盘任务失败: {exc}", data=[])
@router.get("/relay/tasks/stream")
async def stream_relay_tasks(request: Request, session_data: dict = Depends(require_admin_auth)):
async def event_stream():
queue = await relay_task_manager.subscribe_task_stream()
try:
while True:
if await request.is_disconnected():
break
try:
payload = await asyncio.wait_for(queue.get(), timeout=15.0)
yield f"event: tasks\ndata: {payload}\n\n"
except asyncio.TimeoutError:
yield "event: ping\ndata: {}\n\n"
finally:
await relay_task_manager.unsubscribe_task_stream(queue)
return StreamingResponse(
event_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
@router.post("/relay/tasks/batch-delete")
async def batch_delete_relay_tasks(
req: RelayTaskBatchDeleteRequest,
session_data: dict = Depends(require_admin_auth),
):
try:
removed = await relay_task_manager.delete_tasks(req.task_ids or [])
return _success_response(data={"removed": removed}, message=f"已删除 {removed} 个跨盘任务")
except Exception as exc:
return _error_response(message=f"删除跨盘任务失败: {exc}")
+3
View File
@@ -53,6 +53,9 @@ async def require_admin_auth(request: Request):
stored_username = config_manager.get("admin_username") or "admin"
stored_password = config_manager.get("admin_password") or "admin"
credential_state = assess_admin_credential_state(stored_username, stored_password)
if session_data.get("must_change_password") and request.url.path not in PASSWORD_CHANGE_EXEMPT_PATHS:
raise_forbidden("当前会话使用临时密码登录,请先到系统设置修改管理员密码")
if credential_state["must_change_password"] and request.url.path not in PASSWORD_CHANGE_EXEMPT_PATHS:
raise_forbidden("检测到管理员仍在使用默认或旧版明文密码,请先到系统设置修改密码")
+27 -8
View File
@@ -15,6 +15,7 @@ from api.responses import success_response as _success_response, error_response
from core.range_proxy import get_range_proxy_session
from core.emby_proxy_server import emby_proxy_server_manager
from core.driver_service import get_account_driver_instance
from core.strm_security import decode_strm_file_key
from database.db import db
@@ -155,21 +156,39 @@ def _normalize_media_url(candidate: str, request: Request, config: Dict[str, Any
return value
def _is_litepan_strm_url(value: str) -> bool:
def _litepan_strm_path(value: str) -> str:
text = str(value or "").strip()
if not text:
return False
return ""
if text.startswith("http://") or text.startswith("https://"):
return urlparse(text).path.lower().startswith("/api/strm/play/")
return text.lower().startswith("/api/strm/play/")
return urlparse(text).path
return text.split("?", 1)[0]
def _is_litepan_strm_url(value: str) -> bool:
path = _litepan_strm_path(value).lower()
if not path:
return False
return path.startswith("/api/strm/play/") or path.startswith("/api/strm/v2/play/")
def _parse_litepan_strm_url(value: str) -> Optional[Dict[str, Any]]:
text = str(value or "").strip()
if not text:
path = _litepan_strm_path(value)
if not path:
return None
parsed = urlparse(text)
path = parsed.path if parsed.scheme else text.split("?", 1)[0]
# v2 链接:/api/strm/v2/play/{account_id}/{file_key}/t/{token}[/s/{signature}]
v2_match = re.match(r"^/api/strm/v2/play/(\d+)/([^/]+)/t/", path)
if v2_match:
try:
file_id = decode_strm_file_key(v2_match.group(2))
except Exception:
return None
if not file_id:
return None
return {
"account_id": int(v2_match.group(1)),
"file_id": file_id,
}
match = re.match(r"^/api/strm/play/(\d+)/(.*)$", path)
if not match:
return None
+1 -1
View File
@@ -7,7 +7,7 @@ from typing import Any, Dict
from pathlib import Path
APP_NAME = "LitePan"
APP_VERSION = "0.2.0-beta"
APP_VERSION = "0.3.0-beta"
try:
# 日志模块延迟导入,避免应用启动早期的循环依赖
+115 -64
View File
@@ -1,64 +1,115 @@
"""驱动基类:统一初始化、请求节流;缓存由装饰器处理。"""
import asyncio
import contextvars
import time
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Any
from core.base import FileItem, OperationResult, DriverInfo
from core.log_manager import get_writer, LogModule
# 协程级额外 API 延迟补偿,供缓存保持/STRM 等任务叠加在 operation_delay 之上,不影响其他协程
_extra_api_delay: contextvars.ContextVar[int] = contextvars.ContextVar('extra_api_delay', default=0)
class BaseDriver(ABC):
def __init__(self, config):
self.config = config
self.account_id = getattr(config, 'account_id', 'default')
self._cache_manager = None
self._log = get_writer(LogModule.DRIVER)
self._request_interval_lock = asyncio.Lock()
self._last_request_started_at: Optional[float] = None
def set_cache_manager(self, cache_manager):
self._cache_manager = cache_manager
def is_connectivity_test(self) -> bool:
"""account_id='temp_test' 代表只是新增/编辑账号时做一次连通性验证,不写入任何持久状态。"""
account_id = getattr(self, "_account_id", None) or getattr(self, "account_id", None)
return str(account_id) == "temp_test"
async def wait_for_request_interval(self) -> None:
delay_ms = int(getattr(self.config, "operation_delay", 0) or 0) + _extra_api_delay.get()
if delay_ms <= 0:
return
async with self._request_interval_lock:
now = time.monotonic()
if self._last_request_started_at is not None:
elapsed_ms = (now - self._last_request_started_at) * 1000
remaining_ms = delay_ms - elapsed_ms
if remaining_ms > 0:
await asyncio.sleep(remaining_ms / 1000.0)
self._last_request_started_at = time.monotonic()
@classmethod
@abstractmethod
def get_info(cls) -> DriverInfo:
raise NotImplementedError
@abstractmethod
async def init(self) -> None:
raise NotImplementedError
@abstractmethod
async def close(self) -> None:
raise NotImplementedError
@abstractmethod
async def test_connection(self) -> OperationResult:
raise NotImplementedError
def __str__(self) -> str:
return f"<{self.__class__.__name__} account_id={self.account_id}>"
"""驱动基类:统一初始化、请求节流;缓存由装饰器处理。"""
import asyncio
import contextvars
import time
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Any
from core.base import FileItem, OperationResult, DriverInfo
from core.log_manager import get_writer, LogModule
# 协程级额外 API 延迟补偿,供缓存保持/STRM 等任务叠加在 operation_delay 之上,不影响其他协程
_extra_api_delay: contextvars.ContextVar[int] = contextvars.ContextVar('extra_api_delay', default=0)
class BaseDriver(ABC):
def __init__(self, config):
self.config = config
self.account_id = getattr(config, 'account_id', 'default')
self._cache_manager = None
self._log = get_writer(LogModule.DRIVER)
self._request_interval_lock = asyncio.Lock()
self._last_request_started_at: Optional[float] = None
def set_cache_manager(self, cache_manager):
self._cache_manager = cache_manager
def is_connectivity_test(self) -> bool:
"""account_id='temp_test' 代表只是新增/编辑账号时做一次连通性验证,不写入任何持久状态。"""
account_id = getattr(self, "_account_id", None) or getattr(self, "account_id", None)
return str(account_id) == "temp_test"
async def wait_for_request_interval(self) -> None:
delay_ms = int(getattr(self.config, "operation_delay", 0) or 0) + _extra_api_delay.get()
if delay_ms <= 0:
return
async with self._request_interval_lock:
now = time.monotonic()
if self._last_request_started_at is not None:
elapsed_ms = (now - self._last_request_started_at) * 1000
remaining_ms = delay_ms - elapsed_ms
if remaining_ms > 0:
await asyncio.sleep(remaining_ms / 1000.0)
self._last_request_started_at = time.monotonic()
@classmethod
@abstractmethod
def get_info(cls) -> DriverInfo:
raise NotImplementedError
@abstractmethod
async def init(self) -> None:
raise NotImplementedError
@abstractmethod
async def close(self) -> None:
raise NotImplementedError
@abstractmethod
async def test_connection(self) -> OperationResult:
raise NotImplementedError
def supports_parallel_range_download(self) -> bool:
"""跨盘兜底下载是否允许多 Range 并发。CDN 拒绝并发 Range 的驱动覆写为 False。"""
return True
@staticmethod
def normalize_transfer_hash(method: str, value: str) -> str:
text = str(value or "").strip().lower()
if str(method or "").lower() == "md5":
if len(text) != 32 or any(ch not in "0123456789abcdef" for ch in text):
return ""
return text
if str(method or "").lower() == "sha1":
if len(text) != 40 or any(ch not in "0123456789abcdef" for ch in text):
return ""
return text
return ""
async def resolve_transfer_hash(self, item: FileItem, method: str, *, allow_stream: bool = False) -> str:
extra = item.extra or {}
hashes = extra.get("hashes") or {}
value = hashes.get(method)
if not value and method == "sha1":
value = extra.get("hash_info")
if not value and method == "md5":
value = extra.get("md5") or extra.get("etag") or extra.get("content_md5")
return self.normalize_transfer_hash(method, str(value or ""))
async def rapid_upload_by_hash(
self,
parent_id: str,
filename: str,
hash_type: str,
hash_value: str,
size: int,
duplicate: int = 1,
) -> OperationResult:
"""按文件指纹秒传(跨盘秒传通用能力)。
目标盘若支持某种指纹的秒传,应覆写本方法并在 DRIVER_INFO.capabilities
声明对应能力(如 rapid_sha1)。命中返回 data={"reuse": True, "file_id": ...},
未命中返回 data={"reuse": False}。默认不支持。
"""
raise NotImplementedError(f"{self.__class__.__name__} 不支持指纹秒传")
async def purge_file(self, file_id: str) -> OperationResult:
"""彻底删除(不进回收站)。默认等同 delete_file;具备彻底删除能力的驱动覆写。
供跨盘探测临时目录、未命中空目录等内部清理场景使用,避免污染目标盘回收站。
"""
return await self.delete_file(file_id)
def __str__(self) -> str:
return f"<{self.__class__.__name__} account_id={self.account_id}>"
+4
View File
@@ -117,10 +117,12 @@ async def lifespan(_app: FastAPI):
await log_manager.start_auto_cleanup(retention_days)
from core.upload_task_manager import upload_task_manager
from cross_transfer.relay_task_manager import relay_task_manager
await upload_task_manager.refresh_concurrency_limit()
await upload_task_manager.cleanup_orphan_temp_files_on_startup()
await upload_task_manager.start_temp_file_cleanup()
await relay_task_manager.cleanup_legacy_temp_dir_on_startup()
cache_manager = init_cache_system()
await start_cache_system()
@@ -175,6 +177,7 @@ async def lifespan(_app: FastAPI):
from core.plugin_system import plugin_manager
from core.strm_sync_manager import strm_sync_manager
from core.upload_task_manager import upload_task_manager
from cross_transfer.relay_task_manager import relay_task_manager
# 调度器和后台服务彼此独立,关闭时并行取消,避免串行吃满容器退出时间。
await _run_shutdown_group(
@@ -185,6 +188,7 @@ async def lifespan(_app: FastAPI):
("停止Emby反代监听", emby_proxy_server_manager.shutdown, "Emby反代监听已停止"),
("停止插件系统", plugin_manager.shutdown, "插件系统已停止"),
("停止认证系统", stop_auth_system, "认证系统已停止"),
("停止跨盘中继", relay_task_manager.stop, "跨盘中继已停止"),
("停止上传系统", upload_task_manager.stop, "上传系统已停止"),
("停止缓存系统", stop_cache_system, "缓存系统已停止"),
),
+53 -8
View File
@@ -510,8 +510,25 @@ class StrmSyncManager:
result: List[Tuple[str, str, str, int, Optional[datetime], PurePosixPath]],
metadata_result: Optional[List[Tuple[str, str, int, Optional[datetime], PurePosixPath]]] = None,
task: Optional[StrmSyncTask] = None,
is_root: bool = False,
skipped_dirs: Optional[Set[PurePosixPath]] = None,
) -> bool:
files = await driver.list_files(parent_id)
try:
files = await driver.list_files(parent_id)
except Exception as e:
# 根路径(任务配置的根/各分支根)列举失败:直接失败,让用户知道任务路径已不可用。
if is_root:
raise
# 子目录列举失败(典型:WebDAV 上游某挂载点 404/不可达):记警告并跳过该子树。
# 同时登记到 skipped_dirs,清理阶段会保留该子树下已存在的 strm,避免误删。
if self._logger:
self._logger.warning(
f"STRM跳过无法列举的子目录: account={getattr(task, 'account_id', '-')} "
f"base={base_posix_path} rel={str(relative_parts) or '.'} err={e}"
)
if skipped_dirs is not None:
skipped_dirs.add(relative_parts)
return False
return await self._collect_files_from_items(
driver,
files,
@@ -528,6 +545,7 @@ class StrmSyncManager:
result,
metadata_result,
task,
skipped_dirs,
)
async def _collect_files_from_items(
@@ -547,6 +565,7 @@ class StrmSyncManager:
result: List[Tuple[str, str, str, int, Optional[datetime], PurePosixPath]],
metadata_result: Optional[List[Tuple[str, str, int, Optional[datetime], PurePosixPath]]] = None,
task: Optional[StrmSyncTask] = None,
skipped_dirs: Optional[Set[PurePosixPath]] = None,
) -> bool:
directory_metadata: List[Tuple[str, str, int, Optional[datetime], PurePosixPath]] = []
directory_has_media = False
@@ -579,6 +598,8 @@ class StrmSyncManager:
result,
metadata_result,
task=task,
is_root=False,
skipped_dirs=skipped_dirs,
)
subtree_has_media = subtree_has_media or child_has_media
continue
@@ -620,6 +641,26 @@ class StrmSyncManager:
metadata_result.extend(directory_metadata)
return subtree_has_media
def _is_strm_under_skipped(
self,
strm_rel_text: str,
task_folder: str,
skipped_dirs: Set[PurePosixPath],
) -> bool:
"""判断某个 strm 文件(相对 strm 根的路径)是否落在被跳过的子树下,落在则清理时应保留。"""
if not skipped_dirs:
return False
strm_path = PurePosixPath(strm_rel_text)
for skipped in skipped_dirs:
skipped_text = str(skipped).strip("/")
prefix = PurePosixPath(task_folder) / skipped_text if skipped_text and skipped_text != "." else PurePosixPath(task_folder)
try:
strm_path.relative_to(prefix)
return True
except ValueError:
continue
return False
def _media_stem(self, file_name: str) -> str:
name = str(file_name or "")
return name.rsplit(".", 1)[0] if "." in name else name
@@ -1556,13 +1597,10 @@ class StrmSyncManager:
if self._logger:
self._logger.info(f"STRM分支检查跳过全量扫描: {task.name} 未配置有效分支")
if task.scan_mode == "full_sync" and not use_branch_check:
for file_path in task_root.rglob("*.strm"):
try:
file_path.unlink(missing_ok=True)
deleted_count += 1
except Exception:
pass
# full_sync 不再开头删光重建:改由扫描后的差异清理处理(最终文件集一致)。
# 这样某个子目录列举失败被跳过时,不会因为先删了再重建不回来而丢失其 strm,
# 也避免扫描过程中 Emby 恰好扫到一片空目录。
skipped_dirs: Set[PurePosixPath] = set()
collected: List[Tuple[str, str, str, int, Optional[datetime], PurePosixPath]] = []
metadata_collected: List[Tuple[str, str, int, Optional[datetime], PurePosixPath]] = []
@@ -1668,6 +1706,8 @@ class StrmSyncManager:
result=collected,
metadata_result=metadata_collected if task.sync_metadata and metadata_extensions else None,
task=task,
is_root=True,
skipped_dirs=skipped_dirs,
)
branch_id = branch.get("id")
if branch_id:
@@ -1697,6 +1737,8 @@ class StrmSyncManager:
result=collected,
metadata_result=metadata_collected if task.sync_metadata and metadata_extensions else None,
task=task,
is_root=True,
skipped_dirs=skipped_dirs,
)
collected, conflict_skipped_count = self._select_conflict_winners(collected, conflict_policy)
@@ -1811,6 +1853,9 @@ class StrmSyncManager:
for file_path in file_iter:
rel = str(PurePosixPath(file_path.relative_to(self._strm_root)))
if rel not in desired_local_rel_paths:
# 子目录这次没列举成功(被跳过),保留其已存在 strm,不当成“已删除”清掉
if self._is_strm_under_skipped(rel, task_folder, skipped_dirs):
continue
try:
file_path.unlink(missing_ok=True)
deleted_count += 1
+12 -2
View File
@@ -12,6 +12,8 @@ from typing import Any, Dict, List, Optional
from core.operation_wrapper import current_account_id
TEMP_UPLOAD_DIR = os.path.join("data", "upload_tasks")
@dataclass
class UploadTask:
@@ -72,7 +74,7 @@ class UploadTask:
class UploadTaskManager:
_PROGRESS_UPDATE_INTERVAL = 0.25
_TEMP_UPLOAD_DIR = os.path.join("data", "upload_tasks")
_TEMP_UPLOAD_DIR = TEMP_UPLOAD_DIR
_TEMP_FILE_CLEANUP_INTERVAL = 3600
_TEMP_FILE_MAX_AGE = 24 * 3600
@@ -640,7 +642,15 @@ class UploadTaskManager:
async def _get_active_temp_paths(self) -> set[str]:
async with self._lock:
local_paths = [task.local_path for task in self._tasks.values() if task.local_path]
return {self._normalize_temp_path(path) for path in local_paths}
paths = {self._normalize_temp_path(path) for path in local_paths}
try:
from cross_transfer.relay_task_manager import relay_task_manager
relay_paths = await relay_task_manager.get_active_local_paths()
paths.update(self._normalize_temp_path(path) for path in relay_paths)
except Exception:
pass
return paths
def _normalize_temp_path(self, path: str) -> str:
return os.path.normcase(os.path.abspath(path))
+20
View File
@@ -0,0 +1,20 @@
"""跨盘秒传:基于文件指纹的跨网盘秒传能力。
- methods:可插拔的秒传方法(sha1 / md5 / ...)
- routes:由各驱动声明的能力推导出可行线路(卡片)
- service:扫描源目录指纹、试探可秒传、执行秒传
"""
from .methods import TRANSFER_METHODS, get_method
from .routes import build_routes
from .service import scan_source, probe_stream, execute_stream, execute_transfer
__all__ = [
"TRANSFER_METHODS",
"get_method",
"build_routes",
"scan_source",
"probe_stream",
"execute_stream",
"execute_transfer",
]
+27
View File
@@ -0,0 +1,27 @@
"""秒传方法注册表。
每种方法对应一种文件指纹(hash)。源盘需能提供该指纹,目标盘需支持该指纹秒传。
新增其它指纹(如 md5)时,只需在此登记一条,并让对应驱动在 DRIVER_INFO 声明
provide_hashes / rapid_upload 即可,无需改动公共层与前端。
"""
from dataclasses import dataclass
@dataclass(frozen=True)
class TransferMethod:
id: str
label: str
TRANSFER_METHODS = {
"sha1": TransferMethod(id="sha1", label="SHA1秒传"),
"md5": TransferMethod(id="md5", label="MD5秒传"),
}
def get_method(method_id: str) -> TransferMethod:
method = TRANSFER_METHODS.get(str(method_id or "").lower())
if not method:
raise ValueError(f"未知的秒传方法: {method_id}")
return method
+446
View File
@@ -0,0 +1,446 @@
"""跨盘兜底中继:从源盘下载到临时文件,再上传到目标盘。"""
import asyncio
import os
import time
from typing import Awaitable, Callable, List, Optional, Tuple
from core.driver_service import get_account_driver_instance, resolve_download
from core.log_manager import LogModule, get_writer
from core.range_proxy import _stream_chunked_range, build_proxy_file_info, open_upstream_get, resolve_chunk_cap
_DOWNLOAD_CONCURRENCY = 8
_MIN_PARALLEL_BYTES = 2 * 1024 * 1024
_MIN_PART_BYTES = 4 * 1024 * 1024
_PROGRESS_EMIT_INTERVAL = 0.25
def _log():
return get_writer(LogModule.API)
ProgressCallback = Callable[[int, int, str, float], Awaitable[None]]
def _supports_parallel_download(driver) -> bool:
checker = getattr(driver, "supports_parallel_range_download", None)
if callable(checker):
return bool(checker())
return True
def _parse_content_range_total(value: str) -> int:
text = (value or "").strip()
if "/" not in text:
return 0
tail = text.split("/")[-1].strip()
if tail == "*":
return 0
try:
return int(tail)
except ValueError:
return 0
def plan_download_segments(total_bytes: int, *, concurrency: int = _DOWNLOAD_CONCURRENCY) -> List[Tuple[int, int]]:
total = max(int(total_bytes or 0), 0)
if total <= 0:
return [(0, 0)]
if total < _MIN_PARALLEL_BYTES:
return [(0, total - 1)]
max_parts = min(max(1, concurrency), max(1, total // _MIN_PART_BYTES))
if max_parts <= 1:
return [(0, total - 1)]
part_size = (total + max_parts - 1) // max_parts
segments: List[Tuple[int, int]] = []
start = 0
while start < total:
end = min(start + part_size - 1, total - 1)
segments.append((start, end))
start = end + 1
return segments
class _DownloadProgress:
def __init__(
self,
*,
expected: int,
message: str,
progress_callback: Optional[ProgressCallback],
):
self.expected = max(int(expected or 0), 0)
self.message = message
self.progress_callback = progress_callback
self.downloaded = 0
self._lock = asyncio.Lock()
self._last_emit = 0.0
self._since_emit = 0
async def add(self, nbytes: int) -> None:
if nbytes <= 0:
return
async with self._lock:
self.downloaded += nbytes
self._since_emit += nbytes
if not self.progress_callback:
return
now = time.monotonic()
done = self.expected > 0 and self.downloaded >= self.expected
if self._last_emit and now - self._last_emit < _PROGRESS_EMIT_INTERVAL and not done:
return
elapsed = max(now - self._last_emit, 0.001) if self._last_emit else 1.0
speed = self._since_emit / elapsed if self._last_emit else 0.0
self._since_emit = 0
self._last_emit = now
await self.progress_callback(self.downloaded, self.expected, self.message, speed)
async def finish(self) -> None:
if self.progress_callback:
await self.progress_callback(self.downloaded, self.expected or self.downloaded, "源盘下载完成", 0.0)
async def _probe_range_download(
*,
driver,
source_file_id: str,
download_url: str,
headers_override: Optional[dict],
expected: int,
) -> Tuple[bool, int]:
resp = await open_upstream_get(
driver=driver,
file_id=source_file_id,
user_agent="",
initial_url=download_url,
file_info=None,
range_header="bytes=0-0",
upstream_headers_override=headers_override,
)
try:
if resp.status == 206:
total = _parse_content_range_total(resp.headers.get("Content-Range", ""))
return True, total or expected
if resp.status == 200:
accept_ranges = (resp.headers.get("Accept-Ranges") or "").lower()
if "bytes" in accept_ranges:
content_length = int(resp.headers.get("Content-Length") or 0)
return True, content_length or expected
return False, expected
finally:
resp.release()
async def _download_single_stream(
*,
driver,
source_file_id: str,
download_url: str,
headers_override: Optional[dict],
local_path: str,
expected: int,
progress: _DownloadProgress,
) -> int:
resp = await open_upstream_get(
driver=driver,
file_id=source_file_id,
user_agent="",
initial_url=download_url,
file_info=None,
range_header=None,
upstream_headers_override=headers_override,
)
try:
if resp.status >= 400:
body = await resp.text(errors="ignore")
raise RuntimeError(f"源盘下载失败 HTTP {resp.status}: {body[:200]}")
os.makedirs(os.path.dirname(local_path), exist_ok=True)
with open(local_path, "wb") as fp:
async for chunk in resp.content.iter_chunked(1024 * 256):
if not chunk:
continue
fp.write(chunk)
await progress.add(len(chunk))
return progress.downloaded
finally:
resp.release()
async def _download_range_part(
*,
driver,
source_file_id: str,
download_url: str,
headers_override: Optional[dict],
start: int,
end: int,
fp,
io_lock: asyncio.Lock,
progress: _DownloadProgress,
) -> int:
range_header = f"bytes={start}-{end}"
expected_len = end - start + 1
written = 0
resp = await open_upstream_get(
driver=driver,
file_id=source_file_id,
user_agent="",
initial_url=download_url,
file_info=None,
range_header=range_header,
upstream_headers_override=headers_override,
)
try:
if resp.status not in (200, 206):
body = await resp.text(errors="ignore")
raise RuntimeError(f"源盘分片下载失败 HTTP {resp.status}: {body[:200]}")
# 流式写盘:分片可达数 GB,整段读入内存会在大文件并发时打爆内存并导致进度长时间停在 0%
pos = start
async for chunk in resp.content.iter_chunked(1024 * 256):
if not chunk:
continue
async with io_lock:
fp.seek(pos)
fp.write(chunk)
pos += len(chunk)
written += len(chunk)
await progress.add(len(chunk))
finally:
resp.release()
if written != expected_len:
raise RuntimeError(
f"分片下载不完整 bytes={start}-{end} 期望 {expected_len} 实际 {written}"
)
return written
async def _download_via_range_proxy(
*,
driver,
source_file_id: str,
download_url: str,
headers_override: Optional[dict],
local_path: str,
expected: int,
progress: _DownloadProgress,
file_info=None,
) -> int:
"""与前台 proxy 下载同源:复用 range_proxy._stream_chunked_range 顺序分片拉流。"""
if expected <= 0:
return await _download_single_stream(
driver=driver,
source_file_id=source_file_id,
download_url=download_url,
headers_override=headers_override,
local_path=local_path,
expected=expected,
progress=progress,
)
info = file_info
if info is None:
try:
info = await driver.file_info(source_file_id)
except Exception:
info = None
if info is None:
info = build_proxy_file_info(
source_file_id,
file_name=f"file_{source_file_id}",
file_size=expected,
)
os.makedirs(os.path.dirname(local_path), exist_ok=True)
with open(local_path, "wb") as fp:
async for chunk in _stream_chunked_range(
driver=driver,
file_id=source_file_id,
user_agent="",
initial_url=download_url,
file_info=info,
range_start=0,
range_end=expected - 1,
chunk_cap=resolve_chunk_cap(driver),
upstream_headers_override=headers_override,
):
if not chunk:
continue
fp.write(chunk)
await progress.add(len(chunk))
return progress.downloaded
async def _download_parallel_ranges(
*,
driver,
source_file_id: str,
download_url: str,
headers_override: Optional[dict],
local_path: str,
expected: int,
progress: _DownloadProgress,
) -> int:
segments = plan_download_segments(expected)
if len(segments) <= 1:
return await _download_single_stream(
driver=driver,
source_file_id=source_file_id,
download_url=download_url,
headers_override=headers_override,
local_path=local_path,
expected=expected,
progress=progress,
)
os.makedirs(os.path.dirname(local_path), exist_ok=True)
io_lock = asyncio.Lock()
with open(local_path, "wb") as fp:
if expected > 0:
fp.truncate(expected)
results = await asyncio.gather(*[
_download_range_part(
driver=driver,
source_file_id=source_file_id,
download_url=download_url,
headers_override=headers_override,
start=start,
end=end,
fp=fp,
io_lock=io_lock,
progress=progress,
)
for start, end in segments
])
downloaded = sum(results)
if expected and downloaded < expected:
_log().warning(f"中继并行下载体积不足: 期望 {expected} 实际 {downloaded}")
return downloaded
async def download_source_to_file(
*,
source_account_id: int,
source_file_id: str,
local_path: str,
total_bytes: int,
progress_callback: Optional[ProgressCallback] = None,
) -> int:
driver = await get_account_driver_instance(source_account_id)
download = await resolve_download(driver, source_file_id, "")
if not download.download_url:
raise RuntimeError("获取源盘下载链接失败")
expected = int(total_bytes or download.file_size or 0)
progress = _DownloadProgress(
expected=expected,
message="正在从源盘下载",
progress_callback=progress_callback,
)
supports_range, probed_total = await _probe_range_download(
driver=driver,
source_file_id=source_file_id,
download_url=download.download_url,
headers_override=download.headers,
expected=expected,
)
if probed_total > 0:
expected = probed_total
progress.expected = expected
can_parallel = (
supports_range
and expected >= _MIN_PARALLEL_BYTES
and _supports_parallel_download(driver)
)
try:
if can_parallel:
downloaded = await _download_parallel_ranges(
driver=driver,
source_file_id=source_file_id,
download_url=download.download_url,
headers_override=download.headers,
local_path=local_path,
expected=expected,
progress=progress,
)
elif supports_range:
downloaded = await _download_via_range_proxy(
driver=driver,
source_file_id=source_file_id,
download_url=download.download_url,
headers_override=download.headers,
local_path=local_path,
expected=expected,
progress=progress,
file_info=download.file_info,
)
else:
downloaded = await _download_single_stream(
driver=driver,
source_file_id=source_file_id,
download_url=download.download_url,
headers_override=download.headers,
local_path=local_path,
expected=expected,
progress=progress,
)
except Exception as error:
if can_parallel or supports_range:
_log().warning(f"中继下载失败,回退顺序分片: {error}")
progress.downloaded = 0
progress._since_emit = 0
progress._last_emit = 0.0
downloaded = await _download_via_range_proxy(
driver=driver,
source_file_id=source_file_id,
download_url=download.download_url,
headers_override=download.headers,
local_path=local_path,
expected=expected,
progress=progress,
file_info=download.file_info,
)
else:
raise
await progress.finish()
return downloaded
async def upload_temp_to_target(
*,
target_account_id: int,
target_parent_id: str,
local_path: str,
file_name: str,
file_size: int,
conflict_policy: str,
progress_callback: Optional[ProgressCallback] = None,
) -> dict:
driver = await get_account_driver_instance(target_account_id)
async def upload_progress(uploaded: int, total: int, message: str):
if not progress_callback:
return
await progress_callback(uploaded, total, message or "正在上传到目标盘")
result = await driver.upload_local_file(
local_path,
file_name,
target_parent_id,
progress_callback=upload_progress,
conflict_policy=conflict_policy,
)
if not result.success:
raise RuntimeError(result.message or "上传到目标盘失败")
return result.data or {}
+337
View File
@@ -0,0 +1,337 @@
"""跨盘兜底中继任务管理器(内存队列,进程重启不持久化)。"""
import asyncio
import json
import os
import time
import uuid
from copy import deepcopy
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from core.upload_task_manager import TEMP_UPLOAD_DIR
from cross_transfer.relay import download_source_to_file, upload_temp_to_target
_LEGACY_TEMP_DIR = os.path.join("data", "cross_transfer")
@dataclass
class RelayTask:
task_id: str
source_account_id: int
source_account_name: str
source_driver_type: str
target_account_id: int
target_account_name: str
target_driver_type: str
source_file_id: str
file_name: str
rel_path: str
rel_dir: str
target_parent_id: str
target_display_path: str
total_bytes: int
method: str = "sha1"
conflict_policy: str = "rename"
status: str = "pending"
phase: str = "pending"
progress: int = 0
downloaded_bytes: int = 0
uploaded_bytes: int = 0
speed_bytes_per_second: float = 0.0
message: str = "等待中继"
error: str = ""
result: Optional[Dict[str, Any]] = None
local_path: str = ""
created_at: float = field(default_factory=time.time)
updated_at: float = field(default_factory=time.time)
_runner: Optional[asyncio.Task] = field(default=None, repr=False)
def to_dict(self) -> Dict[str, Any]:
return {
"task_id": self.task_id,
"source_account_id": self.source_account_id,
"source_account_name": self.source_account_name,
"source_driver_type": self.source_driver_type,
"target_account_id": self.target_account_id,
"target_account_name": self.target_account_name,
"target_driver_type": self.target_driver_type,
"source_file_id": self.source_file_id,
"file_name": self.file_name,
"rel_path": self.rel_path,
"rel_dir": self.rel_dir,
"target_parent_id": self.target_parent_id,
"target_display_path": self.target_display_path,
"total_bytes": self.total_bytes,
"method": self.method,
"conflict_policy": self.conflict_policy,
"status": self.status,
"phase": self.phase,
"progress": self.progress,
"downloaded_bytes": self.downloaded_bytes,
"uploaded_bytes": self.uploaded_bytes,
"speed_bytes_per_second": self.speed_bytes_per_second,
"message": self.message,
"error": self.error,
"result": deepcopy(self.result),
"created_at": self.created_at,
"updated_at": self.updated_at,
}
class RelayTaskManager:
_TEMP_DIR = TEMP_UPLOAD_DIR
_CONCURRENCY_LIMIT = 2
_MAX_TASKS = 200
def __init__(self):
self._tasks: Dict[str, RelayTask] = {}
self._lock = asyncio.Lock()
self._running_count = 0
self._condition = asyncio.Condition()
self._subscribers: set[asyncio.Queue[str]] = set()
async def create_task(self, **kwargs) -> Dict[str, Any]:
os.makedirs(self._TEMP_DIR, exist_ok=True)
task_id = uuid.uuid4().hex
suffix = os.path.splitext(kwargs.get("file_name") or "")[1]
local_path = os.path.join(self._TEMP_DIR, f"{task_id}{suffix}")
task = RelayTask(task_id=task_id, local_path=local_path, **kwargs)
async with self._lock:
self._tasks[task_id] = task
self._prune_locked()
task._runner = asyncio.create_task(self._run_task(task_id))
data = task.to_dict()
await self._broadcast()
return data
async def list_tasks(self) -> List[Dict[str, Any]]:
async with self._lock:
tasks = list(self._tasks.values())
tasks.sort(key=lambda item: item.created_at, reverse=True)
return [task.to_dict() for task in tasks]
async def delete_tasks(self, task_ids: List[str]) -> int:
removed = 0
async with self._lock:
for task_id in task_ids:
task = self._tasks.pop(task_id, None)
if not task:
continue
if task._runner and not task._runner.done():
task._runner.cancel()
await self._remove_local_file(task.local_path)
removed += 1
if removed:
await self._broadcast()
return removed
async def subscribe_task_stream(self) -> asyncio.Queue[str]:
queue: asyncio.Queue[str] = asyncio.Queue(maxsize=8)
async with self._lock:
self._subscribers.add(queue)
await queue.put(await self._snapshot_payload())
return queue
async def unsubscribe_task_stream(self, queue: asyncio.Queue[str]) -> None:
async with self._lock:
self._subscribers.discard(queue)
async def get_active_local_paths(self) -> List[str]:
async with self._lock:
return [task.local_path for task in self._tasks.values() if task.local_path]
async def cleanup_legacy_temp_dir_on_startup(self) -> int:
if not os.path.isdir(_LEGACY_TEMP_DIR):
return 0
removed = 0
for name in os.listdir(_LEGACY_TEMP_DIR):
path = os.path.join(_LEGACY_TEMP_DIR, name)
try:
if os.path.isfile(path):
os.remove(path)
removed += 1
except OSError:
pass
try:
if not os.listdir(_LEGACY_TEMP_DIR):
os.rmdir(_LEGACY_TEMP_DIR)
except OSError:
pass
return removed
async def stop(self) -> None:
async with self._lock:
runners = [task._runner for task in self._tasks.values() if task._runner and not task._runner.done()]
for runner in runners:
runner.cancel()
if runners:
await asyncio.gather(*runners, return_exceptions=True)
async def _run_task(self, task_id: str) -> None:
try:
async with self._condition:
while self._running_count >= self._CONCURRENCY_LIMIT:
await self._condition.wait()
self._running_count += 1
task = await self._get_object(task_id)
if not task:
return
await self._update(task_id, status="running", phase="downloading", message="正在从源盘下载", progress=0)
async def on_download(downloaded: int, total: int, message: str, speed: float):
total = int(total or task.total_bytes or 0)
# 下载与上传各自独立计 0-100,前端两段进度分别铺满,不再拼接
progress = int(downloaded / total * 100) if total else 0
await self._update(
task_id,
phase="downloading",
downloaded_bytes=downloaded,
progress=min(100, progress),
speed_bytes_per_second=speed,
message=message,
)
downloaded = await download_source_to_file(
source_account_id=task.source_account_id,
source_file_id=task.source_file_id,
local_path=task.local_path,
total_bytes=task.total_bytes,
progress_callback=on_download,
)
if downloaded <= 0:
raise RuntimeError("源盘下载为空文件")
await self._update(
task_id,
phase="uploading",
downloaded_bytes=downloaded,
progress=0,
speed_bytes_per_second=0.0,
message="正在上传到目标盘",
)
target_folder_id = await self._resolve_target_folder(task)
async def on_upload(uploaded: int, total: int, message: str, speed: float = 0.0):
total = int(total or task.total_bytes or 0)
ratio = (uploaded / total) if total else 0
progress = int(ratio * 100)
await self._update(
task_id,
phase="uploading",
uploaded_bytes=uploaded,
progress=min(99, progress),
message=message or "正在上传到目标盘",
)
data = await upload_temp_to_target(
target_account_id=task.target_account_id,
target_parent_id=target_folder_id,
local_path=task.local_path,
file_name=task.file_name,
file_size=task.total_bytes,
conflict_policy=task.conflict_policy,
progress_callback=on_upload,
)
await self._update(
task_id,
status="success",
phase="done",
progress=100,
uploaded_bytes=task.total_bytes,
speed_bytes_per_second=0.0,
message="兜底传输完成",
result=data,
error="",
)
except asyncio.CancelledError:
await self._update(task_id, status="canceled", message="任务已取消", error="任务已取消")
raise
except Exception as exc:
await self._update(
task_id,
status="failed",
phase="failed",
speed_bytes_per_second=0.0,
message="兜底传输失败",
error=str(exc),
)
finally:
final = await self._get_object(task_id)
if final and final.status == "success":
await self._remove_local_file(final.local_path)
async with self._condition:
self._running_count = max(0, self._running_count - 1)
self._condition.notify_all()
async def _resolve_target_folder(self, task: RelayTask) -> str:
if not task.rel_dir:
return task.target_parent_id
from cross_transfer.service import _ensure_target_dir
from core.driver_service import get_account_driver_instance
driver = await get_account_driver_instance(task.target_account_id)
cache = {"": task.target_parent_id}
return await _ensure_target_dir(driver, task.target_parent_id, task.rel_dir, cache)
async def _get_object(self, task_id: str) -> Optional[RelayTask]:
async with self._lock:
return self._tasks.get(task_id)
async def _update(self, task_id: str, **fields) -> None:
async with self._lock:
task = self._tasks.get(task_id)
if not task:
return
for key, value in fields.items():
setattr(task, key, value)
task.updated_at = time.time()
await self._broadcast()
async def _snapshot_payload(self) -> str:
tasks = await self.list_tasks()
return json.dumps({"tasks": tasks}, ensure_ascii=False)
async def _broadcast(self) -> None:
payload = await self._snapshot_payload()
async with self._lock:
subscribers = list(self._subscribers)
for queue in subscribers:
try:
queue.put_nowait(payload)
except asyncio.QueueFull:
try:
queue.get_nowait()
except asyncio.QueueEmpty:
pass
try:
queue.put_nowait(payload)
except asyncio.QueueFull:
pass
async def _remove_local_file(self, local_path: str) -> None:
if local_path and os.path.exists(local_path):
try:
os.remove(local_path)
except OSError:
pass
def _prune_locked(self) -> None:
if len(self._tasks) <= self._MAX_TASKS:
return
finished = [
task for task in self._tasks.values()
if task.status in {"success", "failed", "canceled"}
]
finished.sort(key=lambda item: item.updated_at)
for task in finished[: max(0, len(self._tasks) - self._MAX_TASKS)]:
self._tasks.pop(task.task_id, None)
relay_task_manager = RelayTaskManager()
+63
View File
@@ -0,0 +1,63 @@
"""由驱动能力矩阵推导可行的跨盘秒传线路(卡片)。
线路可行性 = 源盘能提供某指纹(provide_hashes)且目标盘支持该指纹秒传(rapid_upload)。
若正反方向均可行,则该线路为双向。
"""
from typing import Dict, List
from core.registry import driver_registry
from .methods import TRANSFER_METHODS
def _pan_meta(info: Dict) -> Dict:
return {
"driver": info.get("name"),
"name": info.get("display_name") or info.get("card_name") or info.get("name"),
"logo": info.get("card_logo") or "",
"color": info.get("card_color") or "",
# 上传冲突策略能力:默认支持改名+覆盖;不支持某项的驱动在 DRIVER_INFO 显式声明(如光鸭只会自动改名)
"conflict_policies": list(info.get("upload_conflict_policies") or ["rename", "overwrite"]),
}
def _feasible_methods(src_info: Dict, dst_info: Dict) -> List[str]:
provides = set(src_info.get("provide_hashes") or [])
accepts = set(dst_info.get("rapid_upload") or [])
return [mid for mid in TRANSFER_METHODS if mid in provides and mid in accepts]
def build_routes() -> List[Dict]:
infos = driver_registry.get_all_driver_info()
names = list(infos.keys())
feasible: Dict[tuple, List[str]] = {}
for a in names:
for b in names:
if a == b:
continue
methods = _feasible_methods(infos[a], infos[b])
if methods:
feasible[(a, b)] = methods
routes: List[Dict] = []
seen = set()
for (a, b), methods in feasible.items():
if (a, b) in seen:
continue
bidirectional = (b, a) in feasible
method_id = methods[0]
routes.append({
"id": f"{a}__{b}",
"from": _pan_meta(infos[a]),
"to": _pan_meta(infos[b]),
"method": method_id,
"method_label": TRANSFER_METHODS[method_id].label,
"bidirectional": bidirectional,
})
seen.add((a, b))
if bidirectional:
seen.add((b, a))
return routes
+477
View File
@@ -0,0 +1,477 @@
"""跨盘秒传服务:扫描源指纹、试探可秒传(流式)、执行秒传。"""
import asyncio
import time
from typing import Any, AsyncGenerator, Dict, List, Optional
from core.base import FileItem
from core.driver_service import get_account_driver_instance
from core.log_manager import LogModule, get_writer
from .methods import get_method
MAX_FILES = 3000
MAX_DEPTH = 40
SCAN_DIR_CONCURRENCY = 6
def _log():
return get_writer(LogModule.API)
def _source_root_prefix(display_path: str) -> str:
parts = [p for p in str(display_path or "").strip("/").split("/") if p]
if not parts:
return ""
return f"{parts[-1]}/"
async def _append_file_node(
driver,
it: FileItem,
method: str,
rel_prefix: str,
acc: Dict,
acc_lock: asyncio.Lock,
) -> Optional[Dict]:
async with acc_lock:
if acc["count"] >= MAX_FILES:
return None
file_hash = await driver.resolve_transfer_hash(it, method, allow_stream=False)
node = {
"type": "file",
"id": it.id,
"name": it.name,
"size": int(it.size or 0),
"hash": file_hash,
"rel_path": f"{rel_prefix}{it.name}",
"rel_dir": rel_prefix.rstrip("/"),
"eligible": bool(file_hash),
"reuse": None,
}
acc["files"].append(node)
acc["count"] += 1
return node
async def _walk_source(
driver,
parent_id: str,
method: str,
rel_prefix: str,
depth: int,
acc: Dict,
acc_lock: asyncio.Lock,
dir_sem: asyncio.Semaphore,
) -> List[Dict]:
if depth > MAX_DEPTH:
return []
async with acc_lock:
if acc["count"] >= MAX_FILES:
return []
items = await driver.list_files(parent_id)
dirs = [it for it in items if it.is_dir]
files = [it for it in items if not it.is_dir]
async def walk_dir(it: FileItem) -> Optional[Dict]:
async with dir_sem:
async with acc_lock:
if acc["count"] >= MAX_FILES:
return None
child_prefix = f"{rel_prefix}{it.name}/"
children = await _walk_source(
driver, str(it.id), method, child_prefix, depth + 1, acc, acc_lock, dir_sem,
)
return {"type": "dir", "id": it.id, "name": it.name, "children": children}
dir_nodes = [node for node in await asyncio.gather(*[walk_dir(it) for it in dirs]) if node]
file_nodes: List[Dict] = []
for it in files:
node = await _append_file_node(driver, it, method, rel_prefix, acc, acc_lock)
if node:
file_nodes.append(node)
else:
break
return dir_nodes + file_nodes
def _count_shallow_dirs(tree: List[Dict]) -> int:
total = 0
for node in tree:
if node.get("type") != "dir":
continue
total += 1
for child in node.get("children") or []:
if child.get("type") == "dir":
total += 1
return total
async def _ensure_file_hash(src, file_rec: Dict, method_id: str, *, allow_stream: bool = True) -> str:
existing = str(file_rec.get("hash") or "").strip().lower()
if existing:
return existing
source_file_id = str(file_rec.get("source_file_id") or "").strip()
if not source_file_id:
return ""
file_info = await src.file_info(source_file_id)
if not file_info:
return ""
return await src.resolve_transfer_hash(file_info, method_id, allow_stream=allow_stream)
async def scan_source(
*,
source_account_id: int,
source_parent_id: str,
method_id: str,
source_display_path: str = "",
) -> Dict[str, Any]:
method = get_method(method_id)
src = await get_account_driver_instance(source_account_id)
acc: Dict[str, Any] = {"count": 0, "files": []}
acc_lock = asyncio.Lock()
dir_sem = asyncio.Semaphore(SCAN_DIR_CONCURRENCY)
root_prefix = _source_root_prefix(source_display_path)
tree = await _walk_source(src, source_parent_id, method.id, root_prefix, 0, acc, acc_lock, dir_sem)
files: List[Dict] = acc["files"]
return {
"tree": tree,
"total": len(files),
"shallow_dirs": _count_shallow_dirs(tree),
"truncated": acc["count"] >= MAX_FILES,
"files": [
{
"source_file_id": f["id"],
"rel_path": f["rel_path"],
"rel_dir": f["rel_dir"],
"name": f["name"],
"size": f["size"],
"hash": f["hash"],
"eligible": f["eligible"],
}
for f in files
],
}
async def probe_stream(
*,
source_account_id: int,
target_account_id: int,
target_parent_id: str,
method_id: str,
files: List[Dict],
) -> AsyncGenerator[Dict[str, Any], None]:
method = get_method(method_id)
src = await get_account_driver_instance(source_account_id)
tgt = await get_account_driver_instance(target_account_id)
yield {"event": "start", "total": len(files)}
probe_folder_id = ""
ok = 0
no = 0
try:
probe_name = f"_litepan_probe_{int(time.time())}"
created = await tgt.create_folder(target_parent_id, probe_name)
if not created.success:
yield {"event": "error", "message": f"创建临时探测目录失败: {created.message}"}
return
probe_folder_id = str((created.data or {}).get("folder_id") or "")
if not probe_folder_id:
yield {"event": "error", "message": "创建临时探测目录失败: 未返回目录ID"}
return
for f in files:
rel_path = f.get("rel_path")
file_hash = str(f.get("hash") or "").strip().lower()
if not file_hash:
yield {"event": "hashing", "rel_path": rel_path, "name": f.get("name")}
file_hash = await _ensure_file_hash(src, f, method.id)
if file_hash:
f["hash"] = file_hash
reuse = False
probe_error = ""
if file_hash:
try:
result = await tgt.rapid_upload_by_hash(
probe_folder_id, f.get("name"), method.id, file_hash, int(f.get("size") or 0), duplicate=2,
)
reuse = bool(result.data and result.data.get("reuse"))
except Exception as exc:
probe_error = str(exc)
_log().warning(f"跨盘秒传试探失败 {f.get('name')}: {exc}")
reuse = False
if reuse:
ok += 1
else:
no += 1
# probe_error 区分“目标盘报错(如上传流量超限)”与“正常未命中”,供前端提示
yield {"event": "item", "rel_path": rel_path, "reuse": reuse, "hash": file_hash, "error": probe_error}
yield {"event": "end", "ok": ok, "no": no}
finally:
if probe_folder_id:
try:
await tgt.purge_file(probe_folder_id)
except Exception as exc:
_log().warning(f"清理临时探测目录失败 {probe_folder_id}: {exc}")
async def _ensure_target_dir(
driver,
root_id: str,
rel_dir: str,
cache: Dict[str, str],
created_dirs: Optional[List[str]] = None,
) -> str:
if rel_dir in cache:
return cache[rel_dir]
parts = [p for p in rel_dir.split("/") if p]
cur = root_id
cur_rel = ""
for part in parts:
cur_rel = f"{cur_rel}/{part}" if cur_rel else part
if cur_rel in cache:
cur = cache[cur_rel]
continue
children = await driver.list_files(cur)
found = next((c for c in children if c.is_dir and c.name == part), None)
if found:
cur = found.id
else:
created = await driver.create_folder(cur, part)
if not created.success:
raise RuntimeError(created.message or f"创建目录失败: {part}")
cur = str((created.data or {}).get("folder_id") or "")
if not cur:
raise RuntimeError(f"创建目录失败: {part}")
if created_dirs is not None:
created_dirs.append(cur)
cache[cur_rel] = cur
cache[rel_dir] = cur
return cur
async def _execute_transfer_file(
*,
f: Dict,
method_id: str,
tgt,
target_parent_id: str,
dir_cache: Dict[str, str],
duplicate: int,
fallback: bool,
source_account_id: int,
source_account_name: str,
source_driver_type: str,
target_account_id: int,
target_account_name: str,
target_driver_type: str,
target_display_path: str,
conflict: str,
relay_task_manager,
dir_cache_created: Optional[List[str]] = None,
) -> Dict[str, Any]:
name = f.get("name")
file_hash = str(f.get("hash") or "").strip().lower()
rel_dir = f.get("rel_dir") or ""
rel_path = f.get("rel_path")
if not file_hash:
src = await get_account_driver_instance(source_account_id)
file_hash = await _ensure_file_hash(src, f, method_id)
if file_hash:
f["hash"] = file_hash
if not file_hash:
return {
"rel_path": rel_path,
"name": name,
"success": False,
"mode": "skip",
"error": "缺少指纹",
}
try:
folder_id = await _ensure_target_dir(tgt, target_parent_id, rel_dir, dir_cache, dir_cache_created)
result = await tgt.rapid_upload_by_hash(
folder_id, name, method_id, file_hash, int(f.get("size") or 0), duplicate=duplicate,
)
reuse = bool(result.data and result.data.get("reuse"))
if reuse:
return {
"rel_path": rel_path,
"name": name,
"success": True,
"mode": "rapid",
"file_id": (result.data or {}).get("file_id", ""),
"error": "",
}
source_file_id = str(f.get("source_file_id") or "").strip()
if fallback and source_file_id:
await relay_task_manager.create_task(
source_account_id=source_account_id,
source_account_name=source_account_name,
source_driver_type=source_driver_type,
target_account_id=target_account_id,
target_account_name=target_account_name,
target_driver_type=target_driver_type,
source_file_id=source_file_id,
file_name=name,
rel_path=f.get("rel_path") or "",
rel_dir=rel_dir,
target_parent_id=target_parent_id,
target_display_path=target_display_path,
total_bytes=int(f.get("size") or 0),
method=method_id,
conflict_policy=conflict,
)
return {
"rel_path": rel_path,
"name": name,
"success": False,
"mode": "relay",
"file_id": "",
"error": "",
}
return {
"rel_path": rel_path,
"name": name,
"success": False,
"mode": "rapid",
"file_id": "",
"error": "未命中秒传",
}
except Exception as exc:
_log().warning(f"跨盘秒传执行失败 {name}: {exc}")
return {
"rel_path": rel_path,
"name": name,
"success": False,
"mode": "error",
"error": str(exc),
}
async def execute_stream(
*,
source_account_id: int,
source_account_name: str,
source_driver_type: str,
target_account_id: int,
target_account_name: str,
target_driver_type: str,
target_parent_id: str,
target_display_path: str,
method_id: str,
files: List[Dict],
conflict: str = "rename",
fallback: bool = False,
) -> AsyncGenerator[Dict[str, Any], None]:
from cross_transfer.relay_task_manager import relay_task_manager
method = get_method(method_id)
tgt = await get_account_driver_instance(target_account_id)
duplicate = 2 if str(conflict).lower() == "overwrite" else 1
dir_cache: Dict[str, str] = {"": target_parent_id}
dir_cache_created: List[str] = []
results: List[Dict] = []
relay_queued = 0
yield {"event": "start", "total": len(files)}
for f in files:
item = await _execute_transfer_file(
f=f,
method_id=method.id,
tgt=tgt,
target_parent_id=target_parent_id,
dir_cache=dir_cache,
duplicate=duplicate,
fallback=fallback,
source_account_id=source_account_id,
source_account_name=source_account_name,
source_driver_type=source_driver_type,
target_account_id=target_account_id,
target_account_name=target_account_name,
target_driver_type=target_driver_type,
target_display_path=target_display_path,
conflict=conflict,
relay_task_manager=relay_task_manager,
dir_cache_created=dir_cache_created,
)
results.append(item)
if item.get("mode") == "relay":
relay_queued += 1
yield {"event": "item", **item}
# 未开兜底时,未命中文件不会写入目标,清理本次新建却仍为空的目录(深层优先)。
# 开了兜底时不清理:未命中会异步中转落地,目录稍后会被填充。
if not fallback and dir_cache_created:
for folder_id in reversed(dir_cache_created):
try:
children = await tgt.list_files(folder_id)
if not children:
await tgt.purge_file(folder_id)
except Exception as exc:
_log().warning(f"清理空目录失败 {folder_id}: {exc}")
rapid_done = sum(1 for r in results if r.get("mode") == "rapid" and r["success"])
yield {
"event": "end",
"done": rapid_done,
"total": len(files),
"rapid_done": rapid_done,
"relay_queued": relay_queued,
"results": results,
}
async def execute_transfer(
*,
source_account_id: int,
source_account_name: str,
source_driver_type: str,
target_account_id: int,
target_account_name: str,
target_driver_type: str,
target_parent_id: str,
target_display_path: str,
method_id: str,
files: List[Dict],
conflict: str = "rename",
fallback: bool = False,
) -> Dict[str, Any]:
summary: Optional[Dict[str, Any]] = None
async for msg in execute_stream(
source_account_id=source_account_id,
source_account_name=source_account_name,
source_driver_type=source_driver_type,
target_account_id=target_account_id,
target_account_name=target_account_name,
target_driver_type=target_driver_type,
target_parent_id=target_parent_id,
target_display_path=target_display_path,
method_id=method_id,
files=files,
conflict=conflict,
fallback=fallback,
):
if msg.get("event") == "end":
summary = msg
return summary or {
"done": 0,
"total": len(files),
"rapid_done": 0,
"relay_queued": 0,
"results": [],
}
+1
View File
@@ -8,6 +8,7 @@ DRIVER_INFO = {
"description": "115网盘官方API接入,支持文件管理、上传下载等功能",
"author": "LitePan",
"capabilities": ["list", "info", "download", "create_folder", "delete", "batch_delete", "rename", "move"],
"provide_hashes": ["sha1"],
"driver_class": OneOneFiveOpenDriver,
"config_class": OneOneFiveOpenConfig,
"card_color": "#22A7F0",
+3
View File
@@ -40,6 +40,9 @@ class OneOneFiveOpenDriver(BaseDriver):
self._session: Optional[aiohttp.ClientSession] = None
self._refresh_lock = asyncio.Lock()
def supports_parallel_range_download(self) -> bool:
return False
@classmethod
def get_info(cls) -> DriverInfo:
return DriverInfo(
+193 -192
View File
@@ -1,192 +1,193 @@
"""115 驱动数据模型:把 115 响应 dict 转成统一 FileItem。"""
from typing import Dict, Any, Optional
from datetime import datetime, timezone
from core.base import FileItem
from core.log_manager import get_writer, LogModule
class OneOneFiveFile:
def __init__(self, data: Dict[str, Any]):
self.data = data
self._log = get_writer(LogModule.DRIVER)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'OneOneFiveFile':
return cls(data)
def get_id(self) -> str:
return str(self.data.get('fid', self.data.get('file_id', '')))
def get_name(self) -> str:
return self.data.get('n', self.data.get('fn', self.data.get('file_name', '')))
def is_directory(self) -> bool:
# 列表接口: fc='0' 目录;详情接口: file_category=0 目录
if 'file_category' in self.data:
return str(self.data.get('file_category')) == '0'
return str(self.data.get('fc', '1')) == '0'
def get_size(self) -> int:
for key in ('size_byte', 's', 'fs'):
raw = self.data.get(key)
if raw is None or raw == '':
continue
try:
return int(raw)
except (TypeError, ValueError):
continue
return 0
def get_parent_id(self) -> str:
parent_id = (
self.data.get('pid')
or self.data.get('cid')
or self.data.get('parent_id')
or ''
)
return str(parent_id)
def _parse_time_value(self, value: Any) -> Optional[datetime]:
if value is None or value == '' or value == 0:
return None
try:
if isinstance(value, (int, float)):
return datetime.fromtimestamp(value, tz=timezone.utc)
if isinstance(value, str):
text = value.strip()
if not text or text == '0':
return None
if text.isdigit() or text.replace('.', '', 1).isdigit():
return datetime.fromtimestamp(float(text), tz=timezone.utc)
for fmt in ('%Y-%m-%d %H:%M:%S', '%Y/%m/%d %H:%M:%S'):
try:
return datetime.strptime(text, fmt).replace(tzinfo=timezone.utc)
except ValueError:
continue
if 'T' in text:
return datetime.fromisoformat(text.replace('Z', '+00:00'))
except (ValueError, TypeError, OSError) as e:
self._log.warning(f"时间格式解析失败: {value}, 错误: {e}", driver_name="115_open")
return None
def get_modified_time(self) -> Optional[datetime]:
# 列表: t/upt;详情 get_info: utime/ptime
for key in ('t', 'upt', 'utime', 'ptime'):
parsed = self._parse_time_value(self.data.get(key))
if parsed:
return parsed
return None
def get_pick_code(self) -> str:
# 115 提取码字段在不同接口下名字不一致,逐个兜底
pc_value = self.data.get('pc', '')
pick_code_value = self.data.get('pick_code', '')
pickcode_value = self.data.get('pickcode', '')
code_value = self.data.get('code', '')
pick_code = pc_value or pick_code_value or pickcode_value or code_value
if pick_code:
self._log.debug(f"文件'{self.get_name()}'的提取码: {pick_code}", driver_name="115_open")
return pick_code
def get_hash_info(self) -> str:
return self.data.get('sha1', '')
def get_thumbnail(self) -> str:
return self.data.get('thumb', '')
def is_trashed(self) -> bool:
return False
def to_file_item(self) -> FileItem:
modified_time = self.get_modified_time()
return FileItem(
id=self.get_id(),
name=self.get_name(),
path=f"/{self.get_id()}", # 115 没有真正的 path,这里用 id 占位
size=self.get_size(),
is_dir=self.is_directory(),
modified=modified_time,
created=None,
download_url=None,
thumbnail_url=self.get_thumbnail() or None,
mime_type=None,
extra={
'parent_id': self.get_parent_id(),
'pick_code': self.get_pick_code(),
'hash_info': self.get_hash_info(),
'thumbnail': self.get_thumbnail()
}
)
class OneOneFiveRecycleFile:
def __init__(self, data: Dict[str, Any]):
self.data = data
self._log = get_writer(LogModule.DRIVER)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'OneOneFiveRecycleFile':
return cls(data)
def get_recycle_id(self) -> str:
return str(self.data.get('id', ''))
def get_file_id(self) -> str:
return str(self.data.get('file_id', ''))
def get_name(self) -> str:
return self.data.get('file_name', '')
def get_delete_time(self) -> int:
dtime = self.data.get('dtime', '0')
try:
return int(dtime)
except (ValueError, TypeError):
return 0
def is_directory(self) -> bool:
return self.data.get('file_category', 0) == 0
def get_size(self) -> int:
for key in ('size_byte', 's', 'fs', 'size'):
raw = self.data.get(key)
if raw is None or raw == '':
continue
try:
return int(raw)
except (TypeError, ValueError):
continue
return 0
def to_file_item(self) -> FileItem:
delete_time = self.get_delete_time()
modified_time = None
if delete_time > 0:
try:
modified_time = datetime.fromtimestamp(delete_time, tz=timezone.utc)
except (ValueError, TypeError):
pass
return FileItem(
id=self.get_recycle_id(),
name=self.get_name(),
path=f"/recycle/{self.get_recycle_id()}",
size=self.get_size(),
is_dir=self.is_directory(),
modified=modified_time,
created=None,
download_url=None,
thumbnail_url=None,
mime_type=None,
extra={
'recycle_id': self.get_recycle_id(),
'original_file_id': self.get_file_id(),
'delete_time': delete_time,
'is_recycled': True
}
)
"""115 驱动数据模型:把 115 响应 dict 转成统一 FileItem。"""
from typing import Dict, Any, Optional
from datetime import datetime, timezone
from core.base import FileItem
from core.log_manager import get_writer, LogModule
class OneOneFiveFile:
def __init__(self, data: Dict[str, Any]):
self.data = data
self._log = get_writer(LogModule.DRIVER)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'OneOneFiveFile':
return cls(data)
def get_id(self) -> str:
return str(self.data.get('fid', self.data.get('file_id', '')))
def get_name(self) -> str:
return self.data.get('n', self.data.get('fn', self.data.get('file_name', '')))
def is_directory(self) -> bool:
# 列表接口: fc='0' 目录;详情接口: file_category=0 目录
if 'file_category' in self.data:
return str(self.data.get('file_category')) == '0'
return str(self.data.get('fc', '1')) == '0'
def get_size(self) -> int:
for key in ('size_byte', 's', 'fs'):
raw = self.data.get(key)
if raw is None or raw == '':
continue
try:
return int(raw)
except (TypeError, ValueError):
continue
return 0
def get_parent_id(self) -> str:
parent_id = (
self.data.get('pid')
or self.data.get('cid')
or self.data.get('parent_id')
or ''
)
return str(parent_id)
def _parse_time_value(self, value: Any) -> Optional[datetime]:
if value is None or value == '' or value == 0:
return None
try:
if isinstance(value, (int, float)):
return datetime.fromtimestamp(value, tz=timezone.utc)
if isinstance(value, str):
text = value.strip()
if not text or text == '0':
return None
if text.isdigit() or text.replace('.', '', 1).isdigit():
return datetime.fromtimestamp(float(text), tz=timezone.utc)
for fmt in ('%Y-%m-%d %H:%M:%S', '%Y/%m/%d %H:%M:%S'):
try:
return datetime.strptime(text, fmt).replace(tzinfo=timezone.utc)
except ValueError:
continue
if 'T' in text:
return datetime.fromisoformat(text.replace('Z', '+00:00'))
except (ValueError, TypeError, OSError) as e:
self._log.warning(f"时间格式解析失败: {value}, 错误: {e}", driver_name="115_open")
return None
def get_modified_time(self) -> Optional[datetime]:
# 列表: t/upt;详情 get_info: utime/ptime
for key in ('t', 'upt', 'utime', 'ptime'):
parsed = self._parse_time_value(self.data.get(key))
if parsed:
return parsed
return None
def get_pick_code(self) -> str:
# 115 提取码字段在不同接口下名字不一致,逐个兜底
pc_value = self.data.get('pc', '')
pick_code_value = self.data.get('pick_code', '')
pickcode_value = self.data.get('pickcode', '')
code_value = self.data.get('code', '')
pick_code = pc_value or pick_code_value or pickcode_value or code_value
if pick_code:
self._log.debug(f"文件'{self.get_name()}'的提取码: {pick_code}", driver_name="115_open")
return pick_code
def get_hash_info(self) -> str:
return self.data.get('sha1', '')
def get_thumbnail(self) -> str:
return self.data.get('thumb', '')
def is_trashed(self) -> bool:
return False
def to_file_item(self) -> FileItem:
modified_time = self.get_modified_time()
return FileItem(
id=self.get_id(),
name=self.get_name(),
path=f"/{self.get_id()}", # 115 没有真正的 path,这里用 id 占位
size=self.get_size(),
is_dir=self.is_directory(),
modified=modified_time,
created=None,
download_url=None,
thumbnail_url=self.get_thumbnail() or None,
mime_type=None,
extra={
'parent_id': self.get_parent_id(),
'pick_code': self.get_pick_code(),
'hash_info': self.get_hash_info(),
'hashes': {'sha1': self.get_hash_info()} if self.get_hash_info() else {},
'thumbnail': self.get_thumbnail()
}
)
class OneOneFiveRecycleFile:
def __init__(self, data: Dict[str, Any]):
self.data = data
self._log = get_writer(LogModule.DRIVER)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'OneOneFiveRecycleFile':
return cls(data)
def get_recycle_id(self) -> str:
return str(self.data.get('id', ''))
def get_file_id(self) -> str:
return str(self.data.get('file_id', ''))
def get_name(self) -> str:
return self.data.get('file_name', '')
def get_delete_time(self) -> int:
dtime = self.data.get('dtime', '0')
try:
return int(dtime)
except (ValueError, TypeError):
return 0
def is_directory(self) -> bool:
return self.data.get('file_category', 0) == 0
def get_size(self) -> int:
for key in ('size_byte', 's', 'fs', 'size'):
raw = self.data.get(key)
if raw is None or raw == '':
continue
try:
return int(raw)
except (TypeError, ValueError):
continue
return 0
def to_file_item(self) -> FileItem:
delete_time = self.get_delete_time()
modified_time = None
if delete_time > 0:
try:
modified_time = datetime.fromtimestamp(delete_time, tz=timezone.utc)
except (ValueError, TypeError):
pass
return FileItem(
id=self.get_recycle_id(),
name=self.get_name(),
path=f"/recycle/{self.get_recycle_id()}",
size=self.get_size(),
is_dir=self.is_directory(),
modified=modified_time,
created=None,
download_url=None,
thumbnail_url=None,
mime_type=None,
extra={
'recycle_id': self.get_recycle_id(),
'original_file_id': self.get_file_id(),
'delete_time': delete_time,
'is_recycled': True
}
)
+2
View File
@@ -10,6 +10,8 @@ DRIVER_INFO = {
"config_class": Pan123OpenConfig,
"driver_class": Pan123OpenDriver,
"capabilities": ["list", "info", "download", "create_folder", "delete", "batch_delete", "rename", "move", "copy"],
"provide_hashes": ["md5"],
"rapid_upload": ["sha1", "md5"],
"card_color": "#1890ff",
"card_name": "123 Open",
"card_logo": "/logos/123.png",
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Any, Dict, Tuple
class Pan123OpenAPI:
BASE_URL = "https://open-api.123pan.com"
USER_AGENT = "LitePan/0.2.0-beta"
USER_AGENT = "LitePan/0.3.0-beta"
ENDPOINTS = {
"user_info": "/api/v1/user/info",
+73
View File
@@ -522,6 +522,12 @@ class Pan123OpenDriver(BaseDriver):
},
)
except Exception as e:
if "不能复制目录" in str(e):
return OperationResult(
success=False,
message="123云盘官方Open接口暂不支持复制文件夹",
data={"warning": True},
)
return OperationResult(success=False, message=f"复制失败: {str(e)}")
async def batch_copy_file(self, file_ids: List[str], target_parent_id: str) -> OperationResult:
@@ -752,6 +758,73 @@ class Pan123OpenDriver(BaseDriver):
normalized = str(file_id or "0").strip() or "0"
return int(normalized) if normalized.isdigit() else normalized
@auto_cleanup_cache("upload_file")
async def rapid_upload_by_hash(
self,
parent_id: str,
filename: str,
hash_type: str,
hash_value: str,
size: int,
duplicate: int = 1,
) -> OperationResult:
normalized_parent = self._normalize_parent_id(parent_id)
hash_kind = str(hash_type or "").lower()
if hash_kind == "sha1":
sha1 = self.normalize_transfer_hash("sha1", hash_value)
if not sha1:
return OperationResult(success=False, message="无效的 SHA1 指纹")
response = await self._api_request(
"sha1_reuse",
"POST",
json_data={
"parentFileID": self._to_api_file_id(normalized_parent),
"filename": filename,
"sha1": sha1,
"size": int(size or 0),
"duplicate": int(duplicate or 1),
},
)
data = Pan123OpenApiHelper.extract_data(response) or {}
reuse = bool(data.get("reuse"))
file_id = data.get("fileID") or data.get("fileId")
return OperationResult(
success=True,
message="秒传命中" if reuse else "未命中秒传",
data={
"reuse": reuse,
"file_id": str(file_id) if file_id else "",
"parent_id": normalized_parent,
},
)
if hash_kind == "md5":
file_md5 = self.normalize_transfer_hash("md5", hash_value)
if not file_md5:
return OperationResult(success=False, message="无效的 MD5 指纹")
conflict_policy = "overwrite" if int(duplicate or 1) == 2 else "rename"
create_data = await self._create_upload_file(
parent_id=normalized_parent,
target_name=filename,
file_size=int(size or 0),
file_md5=file_md5,
conflict_policy=conflict_policy,
)
reuse = bool(create_data.get("reuse"))
file_id = create_data.get("fileID") or create_data.get("fileId")
return OperationResult(
success=True,
message="秒传命中" if reuse else "未命中秒传",
data={
"reuse": reuse,
"file_id": str(file_id) if file_id else "",
"parent_id": normalized_parent,
},
)
raise NotImplementedError(f"123云盘Open不支持 {hash_type} 秒传")
async def _create_upload_file(
self,
*,
+1 -1
View File
@@ -933,7 +933,7 @@ class Cloud189Driver(BaseDriver):
progress_callback,
min(total_size, base_uploaded + size),
total_size,
f"正在上传分片 {part_number}/{total_parts}",
f"正在上传到天翼云盘,分片({part_number}/{total_parts})",
)
@auto_cleanup_cache("upload_file")
+1
View File
@@ -9,6 +9,7 @@ DRIVER_INFO = {
"description": "百度网盘官方开放API接入,当前支持OAuth认证与文件浏览",
"author": "LitePan",
"capabilities": ["list", "info", "download", "create_folder", "delete", "batch_delete", "rename", "move", "upload"],
"provide_hashes": ["md5"],
"driver_class": BaiduOpenDriver,
"config_class": BaiduOpenConfig,
"card_color": "#FF4C94",
+84 -4
View File
@@ -19,7 +19,7 @@ from core.operation_wrapper import auto_cleanup_cache, with_file_info_cache, wit
from .api import BaiduOpenAPI, BaiduOpenApiHelper
from .config import BaiduOpenConfig
from .models import BaiduOpenFile
from .models import BaiduOpenFile, normalize_content_md5
class BaiduOpenDriver(BaseDriver):
@@ -28,9 +28,13 @@ class BaiduOpenDriver(BaseDriver):
self.access_token = config.access_token
self.refresh_token = config.refresh_token
self._session: Optional[aiohttp.ClientSession] = None
self._transfer_md5_cache: Dict[str, str] = {}
self._oauth_server_url = get_oauth_server_url()
self._refresh_lock = asyncio.Lock()
def supports_parallel_range_download(self) -> bool:
return False
@classmethod
def get_info(cls) -> DriverInfo:
return DriverInfo(
@@ -551,7 +555,7 @@ class BaiduOpenDriver(BaseDriver):
return OperationResult(success=False, message="暂不支持上传空文件")
normalized_parent_path = self._normalize_path(parent_path)
cache_parent_id = "0" if normalized_parent_path == self._get_root_path() else normalized_parent_path
cache_parent_id = normalized_parent_path
target_path = self._build_child_path(normalized_parent_path, target_name)
temp_path = ""
@@ -617,7 +621,7 @@ class BaiduOpenDriver(BaseDriver):
return OperationResult(success=False, message="待上传文件不存在")
normalized_parent_path = self._normalize_path(parent_path)
cache_parent_id = "0" if normalized_parent_path == self._get_root_path() else normalized_parent_path
cache_parent_id = normalized_parent_path
target_path = self._build_child_path(normalized_parent_path, target_name)
try:
@@ -682,7 +686,7 @@ class BaiduOpenDriver(BaseDriver):
return OperationResult(success=False, message="待上传文件不存在")
normalized_parent_path = self._normalize_path(parent_path)
cache_parent_id = "0" if normalized_parent_path == self._get_root_path() else normalized_parent_path
cache_parent_id = normalized_parent_path
target_path = self._build_child_path(normalized_parent_path, target_name)
try:
@@ -1243,6 +1247,17 @@ class BaiduOpenDriver(BaseDriver):
"User-Agent": BaiduOpenAPI.USER_AGENT,
}
@staticmethod
def _extract_content_md5_from_headers(headers) -> str:
for key in ("Content-MD5", "content-md5", "Etag", "etag"):
value = headers.get(key)
if not value:
continue
normalized = normalize_content_md5(str(value).strip().strip('"\''))
if normalized:
return normalized
return ""
async def _resolve_download_url(self, official_url: str) -> str:
await self._ensure_session()
async with self._session.head(
@@ -1255,6 +1270,38 @@ class BaiduOpenDriver(BaseDriver):
return location
return official_url
async def _probe_content_md5_from_download_url(
self,
download_url: str,
headers_override: Optional[Dict[str, str]] = None,
) -> str:
await self._ensure_session()
headers = dict(headers_override or {})
headers.setdefault("User-Agent", BaiduOpenAPI.USER_AGENT)
async with self._session.head(
download_url,
headers=headers,
allow_redirects=True,
) as response:
if response.status < 400:
result = self._extract_content_md5_from_headers(response.headers)
if result:
return result
range_headers = dict(headers)
range_headers["Range"] = "bytes=0-0"
async with self._session.get(
download_url,
headers=range_headers,
allow_redirects=True,
) as response:
if response.status in {200, 206}:
result = self._extract_content_md5_from_headers(response.headers)
if result:
return result
return ""
async def _get_file_metas(self, fs_ids: List[str], dlink: bool = False) -> List[FileItem]:
fsids_json = json.dumps([int(fid) for fid in fs_ids])
response = await self._api_request(
@@ -1273,6 +1320,39 @@ class BaiduOpenDriver(BaseDriver):
result.append(item)
return result
async def _resolve_content_md5_from_download(self, item: FileItem) -> str:
from core.driver_service import resolve_download
extra = item.extra or {}
cache_key = str(extra.get("fs_id") or item.id or "").strip()
if cache_key and cache_key in self._transfer_md5_cache:
return self._transfer_md5_cache[cache_key]
if int(item.size or 0) <= 0:
return ""
download = await resolve_download(self, str(item.id), "", file_info=item)
result = await self._probe_content_md5_from_download_url(
download.download_url,
download.headers,
)
if result and cache_key:
self._transfer_md5_cache[cache_key] = result
return result
async def resolve_transfer_hash(self, item: FileItem, method: str, *, allow_stream: bool = False) -> str:
if str(method or "").lower() != "md5" or not allow_stream:
return ""
try:
return await self._resolve_content_md5_from_download(item)
except Exception as exc:
self._log.warning(
f"百度获取 content-md5 失败 {item.name}: {exc}",
driver_name="baidu_open",
)
return ""
def __str__(self) -> str:
masked_token = f"{self.access_token[:12]}..." if self.access_token else "empty"
return f"<{self.__class__.__name__} access_token={masked_token}>"
+9
View File
@@ -6,6 +6,15 @@ from typing import Dict, Any, Optional, List
from core.base import FileItem
def normalize_content_md5(value: str) -> str:
text = str(value or "").strip().lower()
if len(text) != 32:
return ""
if any(ch not in "0123456789abcdef" for ch in text):
return ""
return text
@dataclass
class BaiduOpenFile:
fs_id: str
+4
View File
@@ -8,6 +8,10 @@ DRIVER_INFO = {
"description": "光鸭云盘接入",
"author": "LitePan",
"capabilities": ["list", "info", "download", "create_folder", "delete", "batch_delete", "rename", "move", "upload"],
"provide_hashes": ["md5"],
"rapid_upload": ["md5"],
# 光鸭服务端对同名文件只会自动改名,没有覆盖语义
"upload_conflict_policies": ["rename"],
"driver_class": GuangYaDriver,
"config_class": GuangYaConfig,
"card_color": "#FF7A1A",
+1
View File
@@ -12,6 +12,7 @@ class GuangYaAPI:
"file_detail": "/userres/v1/file/get_file_detail",
"download_url": "/userres/v1/get_res_download_url",
"upload_token": "/userres/v1/get_res_center_token",
"check_can_flash_upload": "/userres/v1/check_can_flash_upload",
"upload_task_info": "/userres/v1/file/get_info_by_task_id",
"create_dir": "/userres/v1/file/create_dir",
"rename": "/userres/v1/file/rename",
+82 -9
View File
@@ -595,7 +595,11 @@ class GuangYaDriver(BaseDriver):
return OperationResult(success=True, message="没有文件需要删除")
return await self._delete_files(file_ids)
async def _delete_files(self, file_ids: List[str]) -> OperationResult:
async def purge_file(self, file_id: str) -> OperationResult:
"""彻底删除(强制清空回收站),用于跨盘探测临时目录等内部清理,不受账号 delete_mode 影响。"""
return await self._delete_files([str(file_id or "").strip()], force_permanent=True)
async def _delete_files(self, file_ids: List[str], force_permanent: bool = False) -> OperationResult:
normalized_file_ids = [str(file_id or "").strip() for file_id in file_ids if str(file_id or "").strip()]
if not normalized_file_ids:
return OperationResult(success=True, message="没有文件需要删除")
@@ -613,7 +617,7 @@ class GuangYaDriver(BaseDriver):
message = f"已将 {len(normalized_file_ids)} 个文件移到回收站" if len(normalized_file_ids) > 1 else "已将文件移到回收站"
if self.config.delete_mode == "delete":
if self.config.delete_mode == "delete" or force_permanent:
recycle_map: Dict[str, Dict[str, Any]] = {}
for _ in range(6):
recycle_items = await self._list_recycle_items()
@@ -748,6 +752,71 @@ class GuangYaDriver(BaseDriver):
except Exception:
pass
async def _fetch_file_md5(self, file_id: str) -> str:
"""按 fileId 走详情接口取真实整文件 md5(光鸭列表不带 md5)。"""
detail = await self._api_request(
GuangYaAPI.ENDPOINTS["file_detail"],
{"fileId": str(file_id or "").strip()},
)
info = (detail.get("data") or {}).get("fileInfo") or {}
return str(info.get("md5") or "")
async def resolve_transfer_hash(self, item: FileItem, method: str, *, allow_stream: bool = False) -> str:
if str(method or "").lower() != "md5":
return ""
extra = item.extra or {}
value = extra.get("md5")
if not value:
# 详情接口轻量,但仍仅在确需指纹时(探测/执行阶段)调用,避免扫描期逐文件请求
if not allow_stream:
return ""
try:
value = await self._fetch_file_md5(item.id)
except Exception as exc:
self._log.warning(f"光鸭获取 md5 失败 {item.name}: {exc}", driver_name="guangya")
return ""
return self.normalize_transfer_hash("md5", str(value or ""))
async def rapid_upload_by_hash(
self,
parent_id: str,
filename: str,
hash_type: str,
hash_value: str,
size: int,
duplicate: int = 1,
) -> OperationResult:
if str(hash_type or "").lower() != "md5":
raise NotImplementedError("光鸭云盘仅支持 md5 秒传")
md5 = self.normalize_transfer_hash("md5", hash_value)
if not md5:
return OperationResult(success=False, message="无效的 MD5 指纹")
resolved_parent_id = self._resolve_parent_id(parent_id)
token_data, code = await self._get_upload_token(resolved_parent_id, filename, int(size or 0))
task_id = str(token_data.get("taskId") or "").strip()
if not task_id:
return OperationResult(success=False, message="未获取到上传任务ID")
# 服务端偶发直接判定秒传(无需提交指纹)
if code == 156:
file_id = await self._wait_upload_task_info(task_id)
return OperationResult(success=True, message="秒传命中", data={
"reuse": True, "file_id": file_id, "parent_id": resolved_parent_id,
})
check = await self._api_request(
GuangYaAPI.ENDPOINTS["check_can_flash_upload"],
{"taskId": task_id, "md5": md5},
)
if not bool((check.get("data") or {}).get("canFlashUpload")):
return OperationResult(success=True, message="未命中秒传", data={"reuse": False})
file_id = await self._wait_upload_task_info(task_id)
return OperationResult(success=True, message="秒传命中", data={
"reuse": True, "file_id": file_id, "parent_id": resolved_parent_id,
})
@auto_cleanup_cache("upload_file")
async def upload_local_file(
self,
@@ -1231,12 +1300,16 @@ class GuangYaDriver(BaseDriver):
return resource
def _calc_upload_part_size(self, size: int) -> int:
# 分片串行上传,片越小请求次数越多、签名/往返开销越大。OSS 限制:单片 100KB~5GB、
# 最多 10000 片,故在限制内尽量用大分片以减少请求数(128MB 上限可覆盖到 ~1.25TB)。
mb = 1024 * 1024
gb = 1024 * 1024 * 1024
if size <= 100 * mb:
return 1 * mb
if size <= 16 * gb:
return 2 * mb
if size <= 160 * gb:
return 4 * mb
return 8 * mb
if size <= 16 * mb:
return 16 * mb
if size <= 4 * gb:
return 16 * mb
if size <= 32 * gb:
return 32 * mb
if size <= 128 * gb:
return 64 * mb
return 128 * mb
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Any, Dict
class OneDriveAPI:
BASE_URL = "https://graph.microsoft.com/v1.0"
USER_AGENT = "LitePan/0.2.0-beta"
USER_AGENT = "LitePan/0.3.0-beta"
ENDPOINTS = {
"me": "/me",
+39 -6
View File
@@ -170,7 +170,7 @@ class QuarkReverseDriver(BaseDriver):
await self.wait_for_request_interval()
@with_auth_retry(max_retries=1)
async def _api_request(self, operation: str, method: str, **kwargs) -> Dict[str, Any]:
async def _api_request(self, operation: str, method: str, benign_codes: Optional[set] = None, **kwargs) -> Dict[str, Any]:
if not self._session:
raise Exception("HTTP会话未初始化")
self._last_cookie_change_detected = False
@@ -206,7 +206,17 @@ class QuarkReverseDriver(BaseDriver):
async with self._session.request(method, url, **kwargs) as response:
self._last_cookie_change_detected = await self._update_cookies_from_response(response)
response_text = await response.text()
# 良性业务码(如删除时“文件已删除”)短路返回,避免把幂等结果当错误打日志/抛异常
if benign_codes:
try:
import json as _json
_peek = _json.loads(response_text)
except Exception:
_peek = None
if isinstance(_peek, dict) and _peek.get("code") in benign_codes:
return _peek
if response.status >= 400:
self._log.error(f"API请求失败:",
details={
@@ -428,6 +438,25 @@ class QuarkReverseDriver(BaseDriver):
return recycle_ids
# 夸克对“删除已不存在文件”返回该业务码,等价于幂等删除成功
_ALREADY_DELETED_CODE = 23004
def _already_deleted_result(self, file_ids: List[str], parent_ids: set) -> OperationResult:
"""目标文件已不存在:删除目的已达成,按成功返回,不打错误日志。"""
self._log.debug(
f"夸克网盘 {len(file_ids)} 个文件已不存在,视为删除成功",
driver_name="quark_reverse",
)
return OperationResult(
success=True,
message="文件已不存在,视为删除成功",
data={
"deleted_count": len(file_ids),
"file_ids": file_ids,
"parent_ids": list(parent_ids),
},
)
async def _delete_files(self, file_ids: List[str]) -> OperationResult:
"""删除文件,支持回收站模式和永久删除模式。"""
try:
@@ -464,10 +493,12 @@ class QuarkReverseDriver(BaseDriver):
driver_name="quark_reverse",
)
await self._api_request("trash", "POST", json={
trash_resp = await self._api_request("trash", "POST", json={
"filelist": file_ids,
"action_type": 1,
})
}, benign_codes={self._ALREADY_DELETED_CODE})
if isinstance(trash_resp, dict) and trash_resp.get("code") == self._ALREADY_DELETED_CODE:
return self._already_deleted_result(file_ids, parent_ids)
# 状态收敛等待:移动到回收站成功后,需等待回收站记录真正可查询。
if self.config.operation_delay > 0:
@@ -511,10 +542,12 @@ class QuarkReverseDriver(BaseDriver):
driver_name="quark_reverse",
)
await self._api_request("trash", "POST", json={
trash_resp = await self._api_request("trash", "POST", json={
"filelist": file_ids,
"action_type": 1,
})
}, benign_codes={self._ALREADY_DELETED_CODE})
if isinstance(trash_resp, dict) and trash_resp.get("code") == self._ALREADY_DELETED_CODE:
return self._already_deleted_result(file_ids, parent_ids)
# 状态收敛等待:即使仅移到回收站,也需要等待网盘列表状态稳定。
if self.config.operation_delay > 0:
+2
View File
@@ -129,6 +129,7 @@ from api.plugins import router as plugins_router
from api.notifications import router as notifications_router
from api.media_organize import router as media_organize_router
from api.local_fs import router as local_fs_router, admin_router as local_fs_admin_router
from api.cross_transfer import router as cross_transfer_router
app.include_router(admin_router, prefix="/api/admin", tags=["管理"])
app.include_router(notifications_router, prefix="/api/admin", tags=["通知"])
@@ -146,6 +147,7 @@ app.include_router(emby_proxy_router, prefix="/emby-proxy", tags=["Emby反代"])
app.include_router(plugins_router)
app.include_router(local_fs_router)
app.include_router(local_fs_admin_router)
app.include_router(cross_transfer_router)
@app.get("/")
async def serve_frontend():
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "litepan-frontend",
"version": "0.2.0-beta",
"version": "0.3.0-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "litepan-frontend",
"version": "0.2.0-beta",
"version": "0.3.0-beta",
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"axios": "^1.6.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "litepan-frontend",
"version": "0.2.0-beta",
"version": "0.3.0-beta",
"type": "module",
"scripts": {
"dev": "vite",
+33
View File
@@ -1501,6 +1501,39 @@
color: rgba(226, 232, 240, 0.82) !important;
}
:root[data-theme="dark"] .index-page .upload-task-item:hover {
background: rgba(148, 163, 184, 0.1) !important;
}
:root[data-theme="dark"] .index-page .task-phase-pill.is-upload {
background: rgba(59, 130, 246, 0.22) !important;
color: #93c5fd !important;
}
:root[data-theme="dark"] .index-page .task-phase-pill.is-download {
background: rgba(245, 158, 11, 0.2) !important;
color: #fcd34d !important;
}
:root[data-theme="dark"] .index-page .task-chip {
background: rgba(148, 163, 184, 0.16) !important;
color: rgba(226, 232, 240, 0.85) !important;
}
:root[data-theme="dark"] .index-page .task-chip.is-speed {
background: rgba(59, 130, 246, 0.22) !important;
color: #93c5fd !important;
}
:root[data-theme="dark"] .index-page .task-chip.is-percent {
background: rgba(148, 163, 184, 0.2) !important;
color: #e5edf8 !important;
}
:root[data-theme="dark"] .index-page .upload-task-hairline {
background: rgba(148, 163, 184, 0.16) !important;
}
:root[data-theme="dark"] .index-page .upload-task-loading-state,
:root[data-theme="dark"] .index-page .upload-task-loading-text,
:root[data-theme="dark"] .index-page .upload-task-empty {
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,191 @@
<template>
<div class="cross-pick-shell">
<div class="cross-pick-accounts">
<div class="cpa-title">选择账号</div>
<div class="cpa-list">
<div v-if="!accounts.length" class="cpa-empty">
没有可用的{{ panName }}账号<br>请先到「存储管理」添加
</div>
<div
v-for="a in accounts"
:key="a.id"
class="cpa-item"
:class="{ active: a.id === selAcc }"
@click="selAcc = a.id"
>
<span class="cpa-name">{{ a.name }}</span>
<span class="cpa-sub">{{ panName }}</span>
</div>
</div>
</div>
<div class="cross-pick-folder">
<FolderSelectorModal
v-if="selAcc"
:key="selAcc"
:account-id="selAcc"
:title="folderTitle"
:initial-path="selAcc === initialAccId ? initialPath : ''"
:show-refresh-button="true"
@resolve="onResolve"
@cancel="$emit('cancel')"
/>
<div v-else class="cpa-empty cpa-empty-folder">请选择左侧账号</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import FolderSelectorModal from '../common/FolderSelectorModal.vue'
const props = defineProps({
mode: { type: String, default: 'src' },
panName: { type: String, default: '' },
accounts: { type: Array, default: () => [] },
initialAccId: { type: [Number, String], default: '' },
initialPath: { type: String, default: '' }
})
const emit = defineEmits(['resolve', 'cancel'])
const selAcc = ref(props.initialAccId || props.accounts[0]?.id || '')
const folderTitle = computed(() => `选择${props.mode === 'src' ? '源' : '目标'}目录`)
const onResolve = (folder) => {
const acc = props.accounts.find(a => a.id === selAcc.value)
emit('resolve', {
accId: selAcc.value,
accName: acc?.name || '账号',
parentId: folder.id,
path: folder.path
})
}
</script>
<style scoped>
.cross-pick-shell {
display: flex;
width: min(94vw, 900px);
height: min(86vh, 570px);
min-height: 0;
}
.cross-pick-accounts {
width: 220px;
flex: 0 0 auto;
display: flex;
flex-direction: column;
min-height: 0;
padding: 18px 12px 18px 18px;
border-right: 1px solid var(--border-color);
}
.cpa-title {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
padding: 0 6px 10px;
}
.cpa-list {
flex: 1;
min-height: 0;
overflow: auto;
display: flex;
flex-direction: column;
gap: 4px;
}
.cpa-item {
display: flex;
flex-direction: column;
gap: 2px;
padding: 10px 12px;
border-radius: 10px;
cursor: pointer;
transition: background 0.15s;
}
.cpa-item:hover {
background: rgba(127, 127, 127, 0.1);
}
.cpa-item.active {
background: rgba(76, 116, 223, 0.12);
}
.cpa-name {
font-weight: 600;
color: var(--text-main);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cpa-item.active .cpa-name {
color: var(--primary-color);
}
.cpa-sub {
font-size: 12px;
color: var(--text-secondary);
}
.cpa-empty {
padding: 22px 14px;
text-align: center;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.7;
}
.cross-pick-folder {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
min-height: 0;
}
/* 嵌入 FolderSelectorModal 时未挂 modal-folder-selector,需覆盖全局 .breadcrumb { flex:1 } */
.cross-pick-folder :deep(.folder-selector-shell) {
height: 100%;
}
.cross-pick-folder :deep(.folder-selector-content) {
padding-top: 12px;
}
.cross-pick-folder :deep(.breadcrumb) {
flex: none !important;
margin-top: -6px !important;
margin-bottom: 2px !important;
}
.cross-pick-folder :deep(.file-list) {
flex: none !important;
height: 345px;
max-height: 345px;
min-height: 345px;
margin-top: 2px !important;
overflow-y: auto;
}
.cpa-empty-folder {
margin: auto;
}
@media (max-width: 640px) {
.cross-pick-shell {
flex-direction: column;
height: min(86vh, 570px);
}
.cross-pick-accounts {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--border-color);
max-height: 160px;
}
}
</style>
@@ -0,0 +1,214 @@
<template>
<div class="notice-dialog">
<div class="notice-title-row">
<div class="notice-title">试探秒传流程:</div>
<button type="button" class="notice-close" @click="$emit('cancel')">×</button>
</div>
<div class="notice-card">
<div class="notice-flow">
<div class="notice-node">
<div class="notice-icon"><SvgIcon name="folder" :size="26" /></div>
<span>源文件指纹</span>
</div>
<div class="notice-arrow">→</div>
<div class="notice-node wide">
<div class="notice-icon"><SvgIcon name="monitor" :size="26" /></div>
<span>LitePan 试探</span>
</div>
<div class="notice-arrow">→</div>
<div class="notice-node">
<div class="notice-icon"><SvgIcon name="cloud" :size="26" /></div>
<span>目标盘临时目录</span>
</div>
</div>
</div>
<div class="notice-tips">
<div class="notice-tip">
<span class="notice-tip-bar" aria-hidden="true"></span>
<span>先试探哪些文件能秒传;若直接传输且选了「覆盖」,误覆盖后无法撤销。</span>
</div>
<div class="notice-tip">
<span class="notice-tip-bar" aria-hidden="true"></span>
<span>部分网盘(如 123)无法预检秒传,试探在临时目录进行,不影响目标目录。</span>
</div>
<div class="notice-tip">
<span class="notice-tip-bar" aria-hidden="true"></span>
<span>试探结束后自动删除临时目录,删除后可能留在回收站,请及时清空回收站。</span>
</div>
</div>
<label class="notice-checkbox">
<input v-model="skipNextTime" type="checkbox">
<span>不再提示</span>
</label>
<div class="notice-actions">
<button type="button" class="notice-confirm" @click="confirmNotice">我知道了,继续</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import SvgIcon from '../icons/SvgIcon.vue'
const props = defineProps({
skipChecked: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['resolve', 'cancel'])
const skipNextTime = ref(props.skipChecked)
const confirmNotice = () => {
emit('resolve', {
confirmed: true,
skipNextTime: skipNextTime.value
})
}
</script>
<style scoped>
.notice-dialog {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.notice-title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.notice-title {
font-size: 17px;
font-weight: 700;
color: #111827;
line-height: 1.5;
}
.notice-close {
width: 24px;
height: 24px;
border: none;
background: transparent;
color: #64748b;
font-size: 24px;
line-height: 1;
cursor: pointer;
}
.notice-card {
padding: 14px 16px;
border-radius: 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
overflow: hidden;
}
.notice-flow {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.2fr) auto minmax(0, 1fr);
align-items: center;
gap: 10px;
}
.notice-node {
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 10px 8px;
border-radius: 12px;
background: #fff;
border: 1px solid #e5e7eb;
color: #334155;
font-size: 13px;
text-align: center;
}
.notice-node.wide {
min-width: 0;
}
.notice-icon {
line-height: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
.notice-arrow {
color: #94a3b8;
font-size: 20px;
}
.notice-tips {
margin-top: 14px;
display: flex;
flex-direction: column;
gap: 8px;
color: #475569;
font-size: 14px;
line-height: 1.8;
}
.notice-tip {
display: flex;
align-items: stretch;
gap: 10px;
}
.notice-tip-bar {
flex-shrink: 0;
width: 4px;
border-radius: 999px;
background: linear-gradient(135deg, #4c74df 0%, #02a6f0 100%);
}
.notice-checkbox {
display: flex;
align-items: center;
gap: 8px;
margin-top: 18px;
color: #64748b;
font-size: 14px;
cursor: pointer;
}
.notice-actions {
display: flex;
justify-content: center;
margin-top: 18px;
}
.notice-confirm {
min-width: 168px;
height: 38px;
border: none;
border-radius: 10px;
background: linear-gradient(135deg, #4c74df 0%, #02a6f0 100%);
color: #fff;
cursor: pointer;
font-size: 14px;
}
@media (max-width: 640px) {
.notice-flow {
grid-template-columns: 1fr;
}
.notice-arrow {
justify-self: center;
transform: rotate(90deg);
}
}
</style>
@@ -0,0 +1,109 @@
<template>
<template v-for="node in nodes" :key="node.id + '_' + node.name">
<div
v-if="node.type === 'dir'"
class="tnode folder"
:style="{ paddingLeft: (6 + depth * 16) + 'px' }"
@click="toggle(node)"
>
<span class="caret" :class="{ open: node.open }"><i class="fas fa-chevron-right"></i></span>
<span class="t-ic dir"><i class="fas fa-folder"></i></span>
<span class="t-name">{{ node.name }}</span>
<span v-if="mode === 'src' && countOk(node) > 0" class="folder-count">{{ countOk(node) }} 个可秒传</span>
</div>
<CrossTransferTree
v-if="node.type === 'dir' && node.open"
:nodes="node.children || []"
:mode="mode"
:depth="depth + 1"
/>
<div
v-else-if="node.type === 'file'"
class="tnode file"
:class="{ probing: node.state === 'run' }"
:data-rel-path="node.rel_path"
:style="{ paddingLeft: (6 + depth * 16) + 'px' }"
>
<span class="caret-spacer"></span>
<span class="t-ic file"><i class="fas fa-file"></i></span>
<span class="t-name">{{ node.name }}</span>
<span class="t-meta">{{ fmtSize(node.size) }}</span>
<span v-if="mode === 'src'" class="tag" :class="statusClass(node)">
<i class="fas" :class="statusIcon(node)"></i>
{{ statusText(node) }}
</span>
</div>
</template>
</template>
<script setup>
defineOptions({ name: 'CrossTransferTree' })
const props = defineProps({
nodes: { type: Array, default: () => [] },
mode: { type: String, default: 'src' },
depth: { type: Number, default: 0 }
})
const toggle = (node) => { node.open = !node.open }
const fmtSize = (b) => {
const u = ['B', 'KB', 'MB', 'GB', 'TB']
let v = Number(b || 0); let i = 0
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++ }
return (i === 0 ? v.toFixed(0) : v.toFixed(2)) + ' ' + u[i]
}
const countOk = (node) => {
if (node.type === 'file') return node.reuse === true ? 1 : 0
return (node.children || []).reduce((a, c) => a + countOk(c), 0)
}
const statusClass = (n) => {
if (n.transferred) return 'done'
if (n.relay) return 'run'
if (n.state === 'run') return 'run'
if (n.reuse === true) return 'ok'
if (n.reuse === false) return 'no'
return 'pending'
}
const statusIcon = (n) => {
if (n.transferred) return 'fa-check'
if (n.relay) return 'fa-truck-fast'
if (n.state === 'run') return 'fa-spinner fa-spin'
if (n.reuse === true) return 'fa-bolt'
if (n.reuse === false) return 'fa-ban'
return 'fa-clock'
}
const statusText = (n) => {
if (n.transferred) return '已转存'
if (n.relay) return '兜底传输中'
if (n.state === 'run') return '验证中'
if (n.reuse === true) return '可秒传'
if (n.reuse === false) return '不可秒传'
return '待试探'
}
</script>
<style scoped>
.tnode { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 10px; color: var(--text-main); }
.tnode:hover { background: rgba(127,127,127,.1); }
.tnode.file.probing { background: rgba(217,119,6,.1); }
.tnode.folder { cursor: pointer; }
.caret { width: 14px; color: var(--text-secondary); font-size: 12px; transition: transform .14s; flex: 0 0 auto; }
.caret.open { transform: rotate(90deg); }
.caret-spacer { width: 14px; flex: 0 0 auto; }
.t-ic { width: 26px; height: 26px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 13px; flex: 0 0 auto; }
.t-ic.dir { color: #f5b942; background: rgba(245,185,66,.16); }
.t-ic.file { color: #7c93b3; background: rgba(124,147,179,.16); }
.t-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.t-meta { color: var(--text-secondary); font-size: 12px; white-space: nowrap; }
.folder-count { font-size: 12px; padding: 2px 9px; border-radius: 999px; white-space: nowrap; border: 1px solid rgba(76,116,223,.22); background: rgba(76,116,223,.08); color: #1d4ed8; }
.tag { font-size: 12px; font-weight: 600; padding: 3px 9px; border-radius: 999px; white-space: nowrap; display: inline-flex; align-items: center; gap: 5px; }
.tag.ok { color: #16a34a; background: #dcfce7; }
.tag.no { color: #94a3b8; background: #eef2f7; }
.tag.run { color: #d97706; background: #fef3c7; }
.tag.done { color: #16a34a; background: #dcfce7; }
.tag.pending { color: #64748b; background: #f1f5f9; }
</style>
+2
View File
@@ -53,6 +53,8 @@ const SVG_MAP = {
upload: `<svg viewBox="0 0 24 24"${NS}><path fill="#E2E8F0" d="M4 18h16v2H4z"/><path fill="#2563EB" d="M12 4 8 10h3v8h2v-8h3L12 4z"/></svg>`,
relay: `<svg viewBox="0 0 24 24"${NS}><path fill="none" stroke="#2563EB" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" d="M13 2 5 14h6l-1 8 9-12h-6z"/></svg>`,
pause: `<svg viewBox="0 0 24 24"${NS}><rect x="6" y="5" width="4" height="14" rx="1" fill="#475569"/><rect x="14" y="5" width="4" height="14" rx="1" fill="#475569"/></svg>`,
play: `<svg viewBox="0 0 24 24"${NS}><path fill="#475569" d="M9.5 6.5V17.5L18.5 12 9.5 6.5Z"/></svg>`,
+16 -2
View File
@@ -472,6 +472,14 @@ const props = defineProps({
copyFileInline: {
type: Function,
default: null
},
deletingIds: {
type: Array,
default: () => []
},
movingIds: {
type: Array,
default: () => []
}
})
@@ -612,8 +620,14 @@ const focusCreateFolderInput = async () => {
}
const isInlineRenaming = file => String(file.id || file.name) === String(renamingId.value)
const isInlineDeleting = file => String(file.id || file.name) === String(deletingId.value)
const isInlineMoving = file => String(file.id || file.name) === String(movingId.value)
const batchDeletingIdSet = computed(() => new Set((props.deletingIds || []).map(String)))
const isInlineDeleting = file =>
String(file.id || file.name) === String(deletingId.value) ||
batchDeletingIdSet.value.has(String(file.id || file.name))
const batchMovingIdSet = computed(() => new Set((props.movingIds || []).map(String)))
const isInlineMoving = file =>
String(file.id || file.name) === String(movingId.value) ||
batchMovingIdSet.value.has(String(file.id || file.name))
const isInlineCopying = file => String(file.id || file.name) === String(copyingId.value)
const isInlineProcessing = file => isInlineDeleting(file) || isInlineMoving(file) || isInlineCopying(file)
const getInlineProcessingText = file => {
+967
View File
@@ -0,0 +1,967 @@
<template>
<div class="upload-task-panel">
<div class="upload-task-panel-header">
<span>任务面板</span>
<div class="upload-task-panel-actions">
<button
type="button"
class="upload-task-header-info-btn"
title="查看上传说明"
@click="openUploadNoticeFromPanel"
>
!
</button>
<button type="button" class="upload-task-close-btn" @click="closeUploadTaskPanel" aria-label="关闭">×</button>
</div>
</div>
<div class="upload-task-panel-body">
<div class="upload-task-layout">
<div class="upload-task-sidebar">
<button
type="button"
class="upload-task-nav-item"
:class="{ active: taskPanelCategory === 'upload' }"
@click="taskPanelCategory = 'upload'"
>
<span class="upload-task-nav-icon"><SvgIcon name="upload" :size="16" /></span>
<span class="upload-task-nav-label">上传任务</span>
<span
class="upload-task-nav-badge"
:class="{ 'is-empty': activeUploadTasks.length === 0 }"
>{{ activeUploadTasks.length || 0 }}</span>
</button>
<button
type="button"
class="upload-task-nav-item"
:class="{ active: taskPanelCategory === 'relay' }"
@click="taskPanelCategory = 'relay'"
>
<span class="upload-task-nav-icon"><SvgIcon name="relay" :size="16" /></span>
<span class="upload-task-nav-label">跨盘任务</span>
<span
class="upload-task-nav-badge"
:class="{ 'is-empty': activeRelayCount === 0 }"
>{{ activeRelayCount || 0 }}</span>
</button>
</div>
<div class="upload-task-content">
<template v-if="taskPanelCategory === 'upload'">
<div class="upload-task-toolbar">
<div class="upload-task-batch-actions">
<button
v-if="uploadTaskView === 'running'"
type="button"
class="task-toolbar-btn"
:class="{ primary: batchToggleMode === 'resume' }"
:disabled="!canBatchToggle"
:title="batchToggleTitle"
@click="handleBatchToggle"
>
<span class="task-btn-icon">
<SvgIcon :name="batchToggleMode === 'pause' ? 'pause' : 'play'" :size="14" />
</span>
{{ batchToggleLabel }}
</button>
<button
type="button"
class="task-toolbar-btn danger"
:disabled="!canBatchDelete"
:title="canBatchDelete ? '删除当前页签中的任务' : '当前没有可删除的任务'"
@click="handleBatchDelete"
>
<span class="task-btn-icon"><SvgIcon name="trash-button" :size="14" /></span>
{{ batchDeleteLabel }}
</button>
</div>
<div class="upload-task-tabs">
<button
type="button"
class="upload-task-tab"
:class="{ active: uploadTaskView === 'running' }"
@click="uploadTaskView = 'running'"
>
进行中
<span class="upload-task-tab-count">{{ runningUploadTasks.length }}</span>
</button>
<button
type="button"
class="upload-task-tab"
:class="{ active: uploadTaskView === 'completed' }"
@click="uploadTaskView = 'completed'"
>
已完成
<span class="upload-task-tab-count">{{ completedUploadTasks.length }}</span>
</button>
</div>
</div>
<div v-if="uploadTaskPanelLoading" class="upload-task-loading-state">
<div class="loading-spinner"></div>
<div class="upload-task-loading-text">{{ uploadTaskPanelLoadingText }}</div>
</div>
<div v-else-if="filteredUploadTasks.length > 0" class="upload-task-list">
<div
v-for="task in filteredUploadTasks"
:key="task.task_id"
class="upload-task-item"
:class="{ completed: ['success', 'skipped'].includes(task.status) }"
>
<div class="upload-task-item-main">
<DriverIcon
class="upload-task-file-icon"
:logo="getUploadTaskDriverBadge(task).logo"
:color="getUploadTaskDriverBadge(task).color"
:name="getUploadTaskDriverBadge(task).name"
:title="getUploadTaskDriverBadge(task).title"
size="badge"
/>
<div class="upload-task-file-info">
<div class="upload-task-title-row">
<span class="upload-task-name" :title="task.file_name">{{ task.file_name }}</span>
<span
v-if="['success', 'skipped', 'failed', 'canceled'].includes(task.status)"
class="upload-task-status"
:class="`status-${task.status}`"
>
{{ getUploadTaskStatusText(task.status) }}
</span>
</div>
<div v-if="isUploadTaskActive(task)" class="upload-task-meta">
<span class="task-phase-pill is-upload">
<span class="phase-dot"></span>{{ getUploadTaskPhaseLabel(task) }}
</span>
<span v-if="getUploadTaskSpeedText(task)" class="task-chip is-speed">{{ getUploadTaskSpeedText(task) }}</span>
<span v-if="formatUploadPart(task)" class="task-chip">{{ formatUploadPart(task) }}</span>
<span v-if="shouldShowUploadTaskMetaPercent(task)" class="task-chip is-percent">{{ task.progress || 0 }}%</span>
</div>
<div v-if="task.error" class="upload-task-error">{{ task.error }}</div>
</div>
</div>
<div
v-if="shouldShowUploadTaskHairline(task)"
class="upload-task-hairline"
>
<div class="upload-task-progress-inner" :style="{ width: `${task.progress || 0}%` }"></div>
</div>
<div class="upload-task-item-actions">
<button
type="button"
class="task-row-btn"
:disabled="!canHandleUploadTaskPrimaryAction(task)"
:title="getUploadTaskPrimaryActionTitle(task)"
@click="handleUploadTaskPrimaryAction(task)"
>
<i :class="getUploadTaskPrimaryActionIconClass(task)" class="task-btn-icon font-icon"></i>
</button>
<button
type="button"
class="task-row-btn danger"
:disabled="!canDeleteUploadTask(task)"
:title="canDeleteUploadTask(task) ? '删除任务' : '当前不可删除'"
@click="handleDeleteUploadTask(task)"
>
<i class="fas fa-trash task-btn-icon font-icon"></i>
</button>
</div>
</div>
</div>
<div v-else class="upload-task-empty">
<div class="upload-task-empty-text">{{ uploadTaskEmptyText }}</div>
</div>
</template>
<template v-else>
<div class="upload-task-toolbar">
<div class="upload-task-batch-actions">
<button
type="button"
class="task-toolbar-btn danger"
:disabled="filteredRelayTasks.length === 0"
@click="handleBatchDeleteRelayTasks"
>
<span class="task-btn-icon"><SvgIcon name="trash-button" :size="14" /></span>
{{ relayTaskView === 'completed' ? '全部清空' : '全部删除' }}
</button>
</div>
<div class="upload-task-tabs">
<button
type="button"
class="upload-task-tab"
:class="{ active: relayTaskView === 'running' }"
@click="relayTaskView = 'running'"
>
进行中
<span class="upload-task-tab-count">{{ runningRelayTasks.length }}</span>
</button>
<button
type="button"
class="upload-task-tab"
:class="{ active: relayTaskView === 'completed' }"
@click="relayTaskView = 'completed'"
>
已完成
<span class="upload-task-tab-count">{{ completedRelayTasks.length }}</span>
</button>
</div>
</div>
<div v-if="filteredRelayTasks.length > 0" class="upload-task-list">
<div
v-for="task in filteredRelayTasks"
:key="task.task_id"
class="upload-task-item"
:class="{ completed: ['success', 'failed', 'canceled'].includes(task.status) }"
>
<div class="upload-task-item-main">
<DriverIcon
class="upload-task-file-icon"
:logo="getRelayTaskDriverBadge(task).logo"
:color="getRelayTaskDriverBadge(task).color"
:name="getRelayTaskDriverBadge(task).name"
:title="getRelayTaskDriverBadge(task).title"
size="badge"
/>
<div
class="upload-task-file-info"
:title="`${task.source_account_name || '源盘'} → ${task.target_account_name || '目标盘'}${task.target_display_path ? ' · ' + task.target_display_path : ''}`"
>
<div class="upload-task-title-row">
<span class="upload-task-name" :title="task.file_name">{{ task.file_name }}</span>
<span
v-if="['success', 'failed', 'canceled'].includes(task.status)"
class="upload-task-status"
:class="`status-${task.status}`"
>{{ getRelayStatusText(task) }}</span>
</div>
<div v-if="isRelayTaskActive(task)" class="upload-task-meta">
<span class="task-phase-pill" :class="task.phase === 'downloading' ? 'is-download' : 'is-upload'">
<span class="phase-dot"></span>{{ getRelayPhaseLabel(task) }}
</span>
<span v-if="formatRelaySpeed(task)" class="task-chip is-speed">{{ formatRelaySpeed(task) }}</span>
<span v-if="formatRelayPart(task)" class="task-chip">{{ formatRelayPart(task) }}</span>
<span v-if="shouldShowRelayTaskMetaPercent(task)" class="task-chip is-percent">{{ task.progress || 0 }}%</span>
</div>
<div v-if="task.error" class="upload-task-error">{{ task.error }}</div>
</div>
</div>
<div
v-if="shouldShowRelayTaskHairline(task)"
class="upload-task-hairline"
>
<div class="upload-task-progress-inner" :style="{ width: `${task.progress || 0}%` }"></div>
</div>
<div class="upload-task-item-actions">
<button
type="button"
class="task-row-btn danger"
title="删除任务"
@click="handleDeleteRelayTask(task)"
>
<i class="fas fa-trash task-btn-icon font-icon"></i>
</button>
</div>
</div>
</div>
<div v-else class="upload-task-empty">
<div class="upload-task-empty-text">{{ relayTaskView === 'completed' ? '暂无已完成跨盘任务' : '暂无进行中的跨盘任务' }}</div>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import SvgIcon from '../icons/SvgIcon.vue'
import DriverIcon from '../common/DriverIcon.vue'
const props = defineProps({
uploadApi: { type: Object, required: true },
relay: { type: Object, required: true },
})
const {
activeUploadTasks,
batchToggleMode,
canBatchToggle,
batchToggleTitle,
handleBatchToggle,
batchToggleLabel,
canBatchDelete,
handleBatchDelete,
batchDeleteLabel,
runningUploadTasks,
completedUploadTasks,
uploadTaskPanelLoading,
uploadTaskPanelLoadingText,
filteredUploadTasks,
getUploadTaskDriverBadge,
isUploadTaskActive,
getUploadTaskPhaseLabel,
getUploadTaskSpeedText,
formatUploadPart,
shouldShowUploadTaskMetaPercent,
shouldShowUploadTaskHairline,
canHandleUploadTaskPrimaryAction,
getUploadTaskPrimaryActionTitle,
handleUploadTaskPrimaryAction,
getUploadTaskPrimaryActionIconClass,
canDeleteUploadTask,
handleDeleteUploadTask,
getUploadTaskStatusText,
uploadTaskEmptyText,
openUploadNoticeFromPanel,
closeUploadTaskPanel,
getRelayTaskDriverBadge,
handleDeleteRelayTask,
handleBatchDeleteRelayTasks,
getRelayPhaseLabel,
shouldShowRelayTaskMetaPercent,
shouldShowRelayTaskHairline,
isRelayTaskActive,
} = props.uploadApi
const {
filteredRelayTasks,
runningRelayTasks,
completedRelayTasks,
activeRelayCount,
getRelayStatusText,
formatRelaySpeed,
formatRelayPart,
} = props.relay
// 模板里这三处需要写回(切换页签/视图),从 props 解构的 ref 在模板写入时不会自动落到 .value,
// 用可写 computed 显式代理,确保写入命中真实 ref。
const taskPanelCategory = computed({
get: () => props.uploadApi.taskPanelCategory.value,
set: (v) => { props.uploadApi.taskPanelCategory.value = v },
})
const uploadTaskView = computed({
get: () => props.uploadApi.uploadTaskView.value,
set: (v) => { props.uploadApi.uploadTaskView.value = v },
})
const relayTaskView = computed({
get: () => props.relay.relayTaskView.value,
set: (v) => { props.relay.relayTaskView.value = v },
})
</script>
<style scoped>
/* 面板加载动画(与首页遮罩共享同名类,scoped 下需自带一份) */
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 上传任务面板 */
.upload-task-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(780px, calc(100vw - 48px));
height: min(70vh, 720px);
max-height: min(70vh, 720px);
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 14px;
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.18);
z-index: 121;
display: flex;
flex-direction: column;
overflow: hidden;
}
.upload-task-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid #eef2f7;
font-size: 15px;
font-weight: 600;
color: #1f2937;
}
.upload-task-panel-actions {
display: flex;
align-items: center;
gap: 10px;
}
.upload-task-close-btn {
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
color: #94a3b8;
cursor: pointer;
font-size: 18px;
padding: 0;
}
.upload-task-close-btn:hover {
color: #64748b;
}
.upload-task-header-info-btn {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #dbe4ee;
border-radius: 999px;
background: #fff;
color: #94a3b8;
cursor: pointer;
font-size: 12px;
font-weight: 700;
line-height: 1;
padding: 0;
flex-shrink: 0;
}
.upload-task-header-info-btn:hover {
color: #64748b;
border-color: #cbd5e1;
background: #f8fafc;
}
.upload-task-panel-body {
flex: 1;
min-height: 0;
padding: 0;
overflow: hidden;
}
.upload-task-layout {
display: flex;
height: 100%;
min-height: 0;
}
.upload-task-sidebar {
width: 168px;
flex-shrink: 0;
border-right: 1px solid #eef2f7;
padding: 18px 10px;
background: #fbfcfe;
}
.upload-task-nav-item {
width: 100%;
display: grid;
grid-template-columns: 20px minmax(0, 1fr) 28px;
align-items: center;
column-gap: 6px;
flex-wrap: nowrap;
white-space: nowrap;
border: none;
background: transparent;
color: #1f2937;
border-radius: 10px;
padding: 10px 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
}
.upload-task-nav-label {
min-width: 0;
white-space: nowrap;
text-align: left;
}
.upload-task-nav-item.active {
background: #f3f6fb;
}
.upload-task-nav-badge {
justify-self: end;
min-width: 18px;
height: 18px;
padding: 0 6px;
border-radius: 999px;
background: rgba(76,116,223,.12);
color: #2952cc;
font-size: 11px;
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
}
.upload-task-nav-badge.is-empty {
visibility: hidden;
}
.upload-task-nav-icon {
display: inline-flex;
align-items: center;
line-height: 0;
}
.upload-task-content {
flex: 1;
min-width: 0;
min-height: 0;
padding: 18px 20px 20px;
display: flex;
flex-direction: column;
}
.upload-task-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-bottom: 14px;
}
.upload-task-batch-actions {
display: flex;
align-items: center;
gap: 10px;
}
.task-toolbar-btn {
min-height: 36px;
padding: 0 14px;
border: 1px solid #dbe4ee;
border-radius: 10px;
background: #fff;
color: #475569;
font-size: 13px;
font-weight: 500;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all 0.2s ease;
}
.task-toolbar-btn:hover:not(:disabled) {
border-color: #cbd5e1;
background: #f8fafc;
}
.task-toolbar-btn.primary {
border-color: transparent;
background: linear-gradient(135deg, #4c74df 0%, #02a6f0 100%);
color: #fff;
box-shadow: 0 8px 20px rgba(2, 166, 240, 0.18);
}
.task-toolbar-btn.primary:hover:not(:disabled) {
border-color: transparent;
background: linear-gradient(135deg, #4167d1 0%, #0295d8 100%);
}
.task-toolbar-btn:disabled {
cursor: not-allowed;
color: #cbd5e1;
background: #f8fafc;
border-color: #e5e7eb;
box-shadow: none;
}
.task-toolbar-btn.danger {
border-color: transparent;
background: linear-gradient(135deg, #ef4444 0%, #f97316 100%);
color: #fff;
}
.task-toolbar-btn.danger:hover:not(:disabled) {
border-color: transparent;
background: linear-gradient(135deg, #dc2626 0%, #ea580c 100%);
}
.task-toolbar-btn.danger:not(:disabled) {
color: #fff;
}
.upload-task-tabs {
display: flex;
align-items: center;
gap: 8px;
}
.upload-task-tab {
height: 32px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0 12px;
border: 1px solid #e5e7eb;
border-radius: 999px;
background: #fff;
color: #64748b;
cursor: pointer;
font-size: 13px;
}
.upload-task-tab.active {
color: #2563eb;
border-color: #bfdbfe;
background: #eff6ff;
}
.upload-task-tab-count {
min-width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: rgba(148, 163, 184, 0.12);
font-size: 12px;
}
.upload-task-list {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.upload-task-loading-state {
flex: 1;
min-height: 320px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
color: #64748b;
}
.upload-task-loading-text {
font-size: 14px;
color: #64748b;
}
.upload-task-item {
position: relative;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
padding: 12px 10px 13px;
border-radius: 10px;
transition: background 0.15s ease;
}
.upload-task-item:hover {
background: #f5f7fb;
}
.upload-task-item.completed {
align-items: center;
padding: 12px 10px;
}
.upload-task-item-main {
min-width: 0;
flex: 1;
display: flex;
align-items: flex-start;
gap: 12px;
}
.upload-task-item.completed .upload-task-item-main {
align-items: center;
}
.upload-task-item.completed .upload-task-title-row {
align-items: center;
}
.upload-task-file-icon {
width: 40px;
height: 40px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
align-self: center;
border: 1px solid #e2e8f0;
border-radius: 10px;
background: #f8fafc;
font-size: 18px;
}
.upload-task-driver-badge {
border: none;
border-radius: 10px;
color: #fff;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
box-shadow: none;
}
.upload-task-file-info {
min-width: 0;
flex: 1;
}
.upload-task-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.upload-task-name {
flex: 1;
font-size: 13px;
color: #111827;
font-weight: 500;
line-height: 1.3;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.upload-task-status {
flex-shrink: 0;
font-size: 11px;
padding: 2px 8px;
border-radius: 6px;
background: #e5e7eb;
color: #4b5563;
}
.upload-task-status.status-running,
.upload-task-status.status-pending {
background: #dbeafe;
color: #1d4ed8;
}
.upload-task-status.status-paused {
background: #e0e7ff;
color: #4338ca;
}
.upload-task-status.status-success {
background: #dcfce7;
color: #15803d;
}
.upload-task-status.status-skipped {
background: #fef3c7;
color: #b45309;
}
.upload-task-status.status-failed,
.upload-task-status.status-canceled {
background: #fee2e2;
color: #b91c1c;
}
.upload-task-path {
margin-top: 6px;
font-size: 12px;
color: #6b7280;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.upload-task-progress-inner {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #06b6d4);
border-radius: 999px;
transition: width 0.25s ease;
}
.upload-task-hairline {
position: absolute;
left: 4px;
right: 4px;
bottom: 7px;
height: 3px;
background: #e5e7eb;
border-radius: 999px;
overflow: hidden;
}
.upload-task-meta {
margin-top: 5px;
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: #475569;
min-width: 0;
overflow: hidden;
}
.task-phase-pill {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 10px;
font-weight: 600;
padding: 1px 7px;
height: 18px;
border-radius: 6px;
background: #eaf1ff;
color: #2563eb;
white-space: nowrap;
}
.task-phase-pill.is-download {
background: #fff1e2;
color: #c2410c;
}
.task-phase-pill.is-upload {
background: #eaf1ff;
color: #2563eb;
}
.task-phase-pill .phase-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: currentColor;
animation: taskPhasePulse 1.1s ease-in-out infinite;
}
@keyframes taskPhasePulse {
0%, 100% { opacity: 0.35; transform: scale(0.75); }
50% { opacity: 1; transform: scale(1.2); }
}
.task-chip {
flex-shrink: 0;
font-size: 10px;
padding: 1px 7px;
height: 18px;
display: inline-flex;
align-items: center;
border-radius: 6px;
background: #f1f5f9;
color: #64748b;
font-variant-numeric: tabular-nums;
}
.task-chip.is-speed {
background: #eaf1ff;
color: #2563eb;
font-weight: 600;
}
.task-chip.is-percent {
background: #eef2f7;
color: #475569;
font-weight: 600;
}
.upload-task-error {
margin-top: 8px;
font-size: 12px;
color: #b91c1c;
line-height: 1.5;
word-break: break-all;
}
.upload-task-item-actions {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 8px;
align-self: center;
opacity: 0;
transition: opacity 0.15s ease;
}
.upload-task-item:hover .upload-task-item-actions,
.upload-task-item:focus-within .upload-task-item-actions {
opacity: 1;
}
.task-row-btn {
width: 28px;
height: 28px;
border: none;
border-radius: 8px;
background: transparent;
color: #9ca3af;
cursor: not-allowed;
font-size: 13px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.task-row-btn:not(:disabled) {
color: #94a3b8;
cursor: pointer;
}
.task-row-btn:not(:disabled):hover {
color: #64748b;
background: #f1f5f9;
}
.task-row-btn.danger {
color: #cbd5e1;
}
.task-row-btn.danger:not(:disabled) {
color: #94a3b8;
}
.task-row-btn.danger:not(:disabled):hover {
color: #64748b;
background: #f1f5f9;
}
.task-btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 0;
}
.task-btn-icon.font-icon {
font-size: 13px;
line-height: 1;
}
.upload-task-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: 320px;
color: #94a3b8;
font-size: 14px;
}
</style>
+13 -10
View File
@@ -25,8 +25,14 @@
</div>
<div class="notice-tips">
<div class="notice-tip">建议电脑和服务器在同一内网,否则请使用网盘官方客户端上传。</div>
<div class="notice-tip">上传速度会受服务器网络带宽影响。</div>
<div class="notice-tip">
<span class="notice-tip-bar" aria-hidden="true"></span>
<span>建议电脑和服务器在同一内网,否则请使用网盘官方客户端上传。</span>
</div>
<div class="notice-tip">
<span class="notice-tip-bar" aria-hidden="true"></span>
<span>上传速度会受服务器网络带宽影响。</span>
</div>
</div>
<label class="notice-checkbox">
@@ -150,17 +156,14 @@ const confirmNotice = () => {
}
.notice-tip {
position: relative;
padding-left: 14px;
display: flex;
align-items: stretch;
gap: 10px;
}
.notice-tip::before {
content: '';
position: absolute;
left: 0;
top: 8px;
.notice-tip-bar {
flex-shrink: 0;
width: 4px;
height: 18px;
border-radius: 999px;
background: linear-gradient(135deg, #4c74df 0%, #02a6f0 100%);
}
+178
View File
@@ -0,0 +1,178 @@
import { ref, computed, onUnmounted } from 'vue'
import axios from 'axios'
export function useCrossRelayTasks() {
const relayTasks = ref([])
const relayTaskView = ref('running')
let relayPollingTimer = null
let relayEventSource = null
let relaySseReconnectTimer = null
const runningRelayTasks = computed(() => {
return relayTasks.value.filter(task => ['pending', 'running'].includes(task.status))
})
const completedRelayTasks = computed(() => {
return relayTasks.value.filter(task => ['success', 'failed', 'canceled'].includes(task.status))
})
const filteredRelayTasks = computed(() => {
return relayTaskView.value === 'completed' ? completedRelayTasks.value : runningRelayTasks.value
})
const activeRelayCount = computed(() => runningRelayTasks.value.length)
const applyRelayTasks = (tasks) => {
relayTasks.value = Array.isArray(tasks) ? tasks : []
}
const fetchRelayTasks = async () => {
try {
const response = await axios.get('/api/cross-transfer/relay/tasks')
if (response.data?.success) {
applyRelayTasks(response.data.data || [])
}
} catch (error) {
console.error('获取跨盘任务失败:', error)
}
}
const stopRelayPolling = () => {
if (relayPollingTimer) {
clearInterval(relayPollingTimer)
relayPollingTimer = null
}
}
const startRelayPolling = () => {
if (relayPollingTimer) return
relayPollingTimer = window.setInterval(() => {
fetchRelayTasks()
}, 4000)
}
const clearRelaySseReconnectTimer = () => {
if (relaySseReconnectTimer) {
clearTimeout(relaySseReconnectTimer)
relaySseReconnectTimer = null
}
}
const disconnectRelayStream = () => {
clearRelaySseReconnectTimer()
if (relayEventSource) {
relayEventSource.close()
relayEventSource = null
}
}
const scheduleRelayStreamReconnect = (panelOpen) => {
if (!panelOpen || relaySseReconnectTimer) return
relaySseReconnectTimer = window.setTimeout(() => {
relaySseReconnectTimer = null
connectRelayStream(panelOpen)
}, 3000)
}
const connectRelayStream = (panelOpen) => {
if (!panelOpen || relayEventSource) return
if (typeof window === 'undefined' || !window.EventSource) {
startRelayPolling()
return
}
try {
const eventSource = new window.EventSource('/api/cross-transfer/relay/tasks/stream')
relayEventSource = eventSource
eventSource.addEventListener('tasks', (event) => {
try {
const payload = JSON.parse(event.data || '{}')
applyRelayTasks(payload.tasks || [])
} catch (error) {
console.error('处理跨盘任务SSE失败:', error)
}
})
eventSource.addEventListener('ping', () => {})
eventSource.onopen = () => {
stopRelayPolling()
clearRelaySseReconnectTimer()
}
eventSource.onerror = () => {
disconnectRelayStream()
startRelayPolling()
scheduleRelayStreamReconnect(panelOpen)
}
} catch (error) {
console.error('建立跨盘任务SSE失败:', error)
startRelayPolling()
scheduleRelayStreamReconnect(panelOpen)
}
}
const openRelayMonitoring = async (panelOpenRef) => {
await fetchRelayTasks()
connectRelayStream(panelOpenRef?.value ?? true)
if (!relayEventSource) startRelayPolling()
}
const closeRelayMonitoring = () => {
disconnectRelayStream()
stopRelayPolling()
}
const batchDeleteRelayTasks = async (taskIds) => {
if (!taskIds.length) return 0
const response = await axios.post('/api/cross-transfer/relay/tasks/batch-delete', { task_ids: taskIds })
if (response.data?.success) {
await fetchRelayTasks()
return response.data.data?.removed || 0
}
throw new Error(response.data?.message || '删除跨盘任务失败')
}
const getRelayStatusText = (task) => {
const status = String(task?.status || '')
if (status === 'success') return '已完成'
if (status === 'failed') return '失败'
if (status === 'canceled') return '已取消'
if (task?.phase === 'downloading') return '下载中'
if (task?.phase === 'uploading') return '上传中'
return '等待中'
}
const formatRelaySpeed = (task) => {
const speed = Number(task?.speed_bytes_per_second || 0)
if (speed <= 0) return ''
if (speed < 1024) return `${speed.toFixed(0)} B/s`
if (speed < 1024 * 1024) return `${(speed / 1024).toFixed(1)} KB/s`
return `${(speed / 1024 / 1024).toFixed(2)} MB/s`
}
// 上传阶段从消息里提取“分片(x/y)”单独展示;驱动消息不含分片时返回空
const formatRelayPart = (task) => {
if (task?.phase !== 'uploading') return ''
const m = String(task?.message || '').match(/分片[((]\s*(\d+)\s*\/\s*(\d+)\s*[))]/)
return m ? `分片 ${m[1]}/${m[2]}` : ''
}
onUnmounted(() => {
closeRelayMonitoring()
})
return {
relayTasks,
relayTaskView,
runningRelayTasks,
completedRelayTasks,
filteredRelayTasks,
activeRelayCount,
fetchRelayTasks,
openRelayMonitoring,
closeRelayMonitoring,
connectRelayStream,
disconnectRelayStream,
batchDeleteRelayTasks,
getRelayStatusText,
formatRelaySpeed,
formatRelayPart,
}
}
+20 -9
View File
@@ -556,8 +556,9 @@ export function useIndexFileActions({
}
}
const batchMove = async () => {
if (operationLoading.value) return
const batchMove = async (options = {}) => {
const showGlobalLoading = options.showGlobalLoading !== false
if (operationLoading.value && showGlobalLoading) return
try {
const selectedCount = selectedFilesList.value.length
@@ -568,7 +569,7 @@ export function useIndexFileActions({
const selectedIdSet = new Set(selectedFilesList.value.map(id => String(id)))
const targetFiles = files.value.filter(file => selectedIdSet.has(String(file.id)))
await moveFiles(targetFiles)
await moveFiles(targetFiles, null, options)
} catch (error) {
if (error.message !== 'Modal closed') {
console.error('批量移动错误:', error)
@@ -697,8 +698,12 @@ export function useIndexFileActions({
}
}
const batchDelete = async () => {
if (operationLoading.value) return
const batchDelete = async (options = {}) => {
const showGlobalLoading = options.showGlobalLoading !== false
const onRequestStart = options.onRequestStart || null
const onRequestEnd = options.onRequestEnd || null
if (operationLoading.value && showGlobalLoading) return
try {
const selectedCount = selectedFilesList.value.length
@@ -728,12 +733,16 @@ export function useIndexFileActions({
await confirm(modalConfig)
const deletedFileIds = [...selectedFilesList.value]
try {
setOperationLoading(true, 'batch_delete', null, `${selectedCount} 个项目`)
if (showGlobalLoading) {
setOperationLoading(true, 'batch_delete', null, `${selectedCount} 个项目`)
}
onRequestStart?.()
const response = await axios.delete('/api/files/delete', {
data: {
account_id: selectedAccountId.value,
file_ids: selectedFilesList.value,
file_ids: deletedFileIds,
parent_id: currentPath.value
},
headers: {
@@ -743,7 +752,6 @@ export function useIndexFileActions({
if (response.data.success) {
window.appNotification.success(response.data.message)
const deletedFileIds = [...selectedFilesList.value]
removeFilesLocally(deletedFileIds)
selectedFilesList.value = []
refreshFilesSilently()
@@ -753,7 +761,10 @@ export function useIndexFileActions({
} catch (apiError) {
window.appNotification.error(`批量删除失败: ${apiError.response?.data?.message || apiError.message}`)
} finally {
setOperationLoading(false)
onRequestEnd?.()
if (showGlobalLoading) {
setOperationLoading(false)
}
}
} catch (error) {
if (error.message !== 'Modal closed') {
File diff suppressed because it is too large. Load diff
+11 -1
View File
@@ -24,6 +24,9 @@
<a class="nav-item" :class="{ active: currentPage === 'strm', disabled: isPageLocked('strm') }" @click="setCurrentPage('strm')">
<i class="fas fa-film nav-icon"></i> 媒体管理
</a>
<a class="nav-item" :class="{ active: currentPage === 'cross-transfer', disabled: isPageLocked('cross-transfer') }" @click="setCurrentPage('cross-transfer')">
<i class="fas fa-bolt nav-icon"></i> 跨盘秒传
</a>
<a class="nav-item" :class="{ active: currentPage === 'logs', disabled: isPageLocked('logs') }" @click="setCurrentPage('logs')">
<i class="fas fa-file-alt nav-icon"></i> 系统日志
</a>
@@ -418,7 +421,12 @@
<div v-if="currentPage === 'strm'" class="page-content">
<StrmGenerator />
</div>
<!-- 跨盘秒传 -->
<div v-if="currentPage === 'cross-transfer'" class="page-content">
<CrossDriveTransfer />
</div>
<!-- 系统日志 -->
<div v-if="currentPage === 'logs'" class="page-content">
<SystemLogs />
@@ -446,6 +454,7 @@ import AccountManagement from '../components/admin/AccountManagement.vue'
import SystemSettings from '../components/admin/SystemSettings.vue'
import CacheCenter from '../components/admin/CacheCenter.vue'
import StrmGenerator from '../components/admin/StrmGenerator.vue'
import CrossDriveTransfer from '../components/admin/CrossDriveTransfer.vue'
import SystemLogs from '../components/admin/SystemLogs.vue'
import PluginCenter from '../components/admin/PluginCenter.vue'
import DriverIcon from '../components/common/DriverIcon.vue'
@@ -493,6 +502,7 @@ const pageTitles = {
settings: '系统设置',
cache: '缓存管理',
strm: '媒体管理',
'cross-transfer': '跨盘秒传',
logs: '系统日志',
plugins: '插件中心'
}
+83 -2619
View File
File diff suppressed because it is too large. Load diff
+392 -12
View File
@@ -79,7 +79,10 @@
<el-form-item>
<div class="login-options">
<el-checkbox v-model="loginData.remember">
<el-checkbox
v-model="loginData.remember"
@keydown.enter.prevent="handleLogin"
>
保持登录(30天)
</el-checkbox>
<a href="#" class="forgot-link" @click.prevent="handleForgotPassword">忘记密码?</a>
@@ -162,30 +165,119 @@ watch(() => loginData.password, (val) => {
passwordValue.value = val || ''
})
const escapeHtml = (value) => String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
const formatCountdown = (seconds) => {
const total = Math.max(0, Math.floor(Number(seconds) || 0))
const minutes = Math.floor(total / 60)
const restSeconds = total % 60
return `${String(minutes).padStart(2, '0')}:${String(restSeconds).padStart(2, '0')}`
}
const startTempPasswordCountdown = (targetId, expiresAt) => {
const targetExpiresAt = Number(expiresAt) || 0
if (!targetExpiresAt) return null
const updateCountdown = () => {
const target = document.getElementById(targetId)
if (!target) return
const remaining = Math.max(0, targetExpiresAt - Math.floor(Date.now() / 1000))
target.textContent = remaining > 0
? `有效期剩余 ${formatCountdown(remaining)}`
: '临时密码已过期'
target.classList.toggle('is-expired', remaining <= 0)
}
updateCountdown()
return window.setInterval(updateCountdown, 1000)
}
const handleForgotPassword = () => {
ElMessageBox.confirm(
'重置密码后,系统将生成一个随机的新密码,并输出到容器的控制台日志中。您需要查看容器日志获取新密码。确定要重置吗?',
`
<div class="reset-password-confirm">
<div class="reset-password-confirm__hero">
<div class="reset-password-confirm__icon" aria-hidden="true">
<span></span>
</div>
<div>
<div class="reset-password-confirm__title">生成临时密码</div>
<div class="reset-password-confirm__desc">
系统会在容器日志中输出一枚临时管理员密码。
</div>
</div>
</div>
<div class="reset-password-confirm__facts">
<div class="reset-password-confirm__fact">
<span class="reset-password-confirm__mark"></span>
<div>
<strong>完成改密后</strong>
<p>新密码会替换原密码,临时密码立即失效。</p>
</div>
</div>
<div class="reset-password-confirm__fact">
<span class="reset-password-confirm__mark"></span>
<div>
<strong>未改密前</strong>
<p>临时密码 10 分钟内有效,且不影响原密码。</p>
</div>
</div>
</div>
</div>
`,
'重置密码确认',
{
confirmButtonText: '确定重置',
cancelButtonText: '取消',
type: 'warning',
customClass: 'custom-confirm-box'
customClass: 'custom-confirm-box reset-password-box',
dangerouslyUseHTMLString: true
}
).then(async () => {
try {
loading.value = true
const response = await axios.post('/api/auth/reset-password')
if (response.data.success) {
ElMessageBox.alert(
'密码已重置!请打开宿主机的终端,执行 <strong>docker logs litepan</strong> (或您自定义的容器名)来查看新生成的随机密码。',
'重置成功',
{
confirmButtonText: '我知道了',
type: 'success',
dangerouslyUseHTMLString: true
const serverMessage = escapeHtml(response.data.message || '')
const expiresAt = Number(response.data.data?.expires_at || 0)
const countdownId = `temp-password-countdown-${Date.now()}`
const countdownHtml = expiresAt
? `<div id="${countdownId}" class="reset-password-result__countdown">有效期剩余 --:--</div>`
: ''
const html = `
<div class="reset-password-result">
<div class="reset-password-result__hero">
<div class="reset-password-result__icon" aria-hidden="true"></div>
<div class="reset-password-result__body">
<div class="reset-password-result__title-row">
<div class="reset-password-result__title">临时密码已生成</div>
${countdownHtml}
</div>
${serverMessage ? `<div class="reset-password-result__status">${serverMessage}</div>` : ''}
</div>
</div>
<div class="reset-password-result__command-card">
<div class="reset-password-result__label">宿主机终端执行(请将litepan替换为实际容器名)</div>
<div class="reset-password-result__code"><code>docker logs litepan</code></div>
</div>
</div>
`
const alertPromise = ElMessageBox.alert(html, '重置成功', {
confirmButtonText: '我知道了',
customClass: 'custom-confirm-box reset-password-box',
dangerouslyUseHTMLString: true
})
const countdownTimer = startTempPasswordCountdown(countdownId, expiresAt)
alertPromise.then(() => {}, () => {}).finally(() => {
if (countdownTimer) {
window.clearInterval(countdownTimer)
}
)
})
} else {
window.appNotification.error(response.data.message || '重置失败')
}
@@ -212,6 +304,7 @@ onMounted(async () => {
})
const handleLogin = async () => {
if (loading.value) return
if (!loginForm.value) return
try {
@@ -274,6 +367,293 @@ const handleLogin = async () => {
border-radius: 8px;
padding: 8px 16px;
}
.reset-password-box {
width: 460px;
max-width: calc(100vw - 32px);
border-radius: 18px;
overflow: hidden;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.18);
}
.reset-password-box .el-message-box__header {
padding: 20px 22px 4px;
}
.reset-password-box .el-message-box__title {
font-size: 17px;
color: #0f172a;
}
.reset-password-box .el-message-box__content {
margin-top: 0;
padding: 12px 22px 8px;
}
.reset-password-box .el-message-box__message {
width: 100%;
}
.reset-password-box .el-message-box__btns {
padding: 12px 22px 22px;
}
.reset-password-box .el-message-box__btns .el-button {
min-width: 96px;
font-weight: 600;
}
.reset-password-confirm,
.reset-password-result {
color: #334155;
line-height: 1.6;
}
.reset-password-confirm__hero,
.reset-password-result__hero {
display: grid;
grid-template-columns: 44px 1fr;
gap: 12px;
align-items: start;
padding: 15px;
border-radius: 16px;
background:
linear-gradient(135deg, rgba(239, 246, 255, 0.98), rgba(255, 255, 255, 0.96)),
#fff;
border: 1px solid #dbeafe;
}
.reset-password-result__hero {
align-items: center;
}
.reset-password-confirm__icon,
.reset-password-result__icon {
width: 44px;
height: 44px;
border-radius: 14px;
display: grid;
place-items: center;
background: linear-gradient(135deg, #1d4ed8, #38bdf8);
box-shadow: 0 12px 22px rgba(37, 99, 235, 0.24);
position: relative;
}
.reset-password-confirm__icon span,
.reset-password-result__icon::before {
content: '';
width: 18px;
height: 18px;
border: 2px solid #fff;
border-radius: 50% 50% 6px 6px;
border-bottom-width: 6px;
display: block;
}
.reset-password-confirm__icon::after,
.reset-password-result__icon::after {
content: '';
width: 14px;
height: 8px;
border: 2px solid #fff;
border-bottom: 0;
border-radius: 9px 9px 0 0;
position: absolute;
top: 11px;
}
.reset-password-confirm__title {
font-weight: 700;
color: #0f172a;
font-size: 18px;
margin-bottom: 6px;
}
.reset-password-confirm__desc {
color: #475569;
margin: 0;
}
.reset-password-confirm__facts {
display: grid;
gap: 12px;
margin-top: 16px;
}
.reset-password-confirm__fact {
display: grid;
grid-template-columns: 10px 1fr;
gap: 10px;
align-items: start;
padding: 0 2px;
}
.reset-password-confirm__mark {
width: 4px;
height: 28px;
border-radius: 999px;
background: linear-gradient(180deg, #2563eb, #38bdf8);
margin-top: 2px;
}
.reset-password-confirm__fact strong {
display: block;
color: #0f172a;
font-size: 14px;
line-height: 1.35;
}
.reset-password-confirm__fact p {
margin: 3px 0 0;
color: #64748b;
}
.reset-password-result__body {
min-width: 0;
}
.reset-password-result__title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-width: 0;
}
.reset-password-result__title {
font-size: 18px;
font-weight: 800;
color: #0f172a;
line-height: 1.35;
}
.reset-password-result__status {
margin-top: 6px;
color: #475569;
}
.reset-password-result__countdown {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
min-height: 22px;
padding: 2px 9px;
border-radius: 999px;
background: #eff6ff;
color: #1d4ed8;
font-size: 12px;
font-weight: 700;
}
.reset-password-result__countdown.is-expired {
background: #fef2f2;
color: #b91c1c;
}
.reset-password-result__command-card {
margin-top: 16px;
padding: 0 2px;
}
.reset-password-result__label {
font-size: 12px;
font-weight: 800;
color: #1d4ed8;
margin-bottom: 7px;
}
.reset-password-result__code {
background: #0f172a;
color: #e5eefb;
border-radius: 12px;
padding: 11px 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
overflow: auto;
white-space: nowrap;
}
.reset-password-result__code code {
color: inherit;
font-size: 13px;
}
:root[data-theme="dark"] .reset-password-box {
background: #181b20;
border: 1px solid #2b3038;
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.42);
}
:root[data-theme="dark"] .reset-password-box .el-message-box__title,
:root[data-theme="dark"] .reset-password-confirm__title,
:root[data-theme="dark"] .reset-password-confirm__fact strong,
:root[data-theme="dark"] .reset-password-result__title {
color: #e7eaf0;
}
:root[data-theme="dark"] .reset-password-box .el-message-box__content,
:root[data-theme="dark"] .reset-password-confirm,
:root[data-theme="dark"] .reset-password-result {
color: #cbd2dc;
}
:root[data-theme="dark"] .reset-password-confirm__desc,
:root[data-theme="dark"] .reset-password-confirm__fact p,
:root[data-theme="dark"] .reset-password-result__status {
color: #9099a8;
}
:root[data-theme="dark"] .reset-password-result__countdown {
background: rgba(37, 99, 235, 0.18);
color: #93c5fd;
}
:root[data-theme="dark"] .reset-password-result__countdown.is-expired {
background: rgba(239, 68, 68, 0.14);
color: #fca5a5;
}
:root[data-theme="dark"] .reset-password-confirm__hero,
:root[data-theme="dark"] .reset-password-result__hero {
background:
linear-gradient(135deg, rgba(30, 64, 175, 0.22), rgba(15, 23, 42, 0.42)),
#1d222a;
border-color: rgba(96, 165, 250, 0.22);
}
:root[data-theme="dark"] .reset-password-result__label {
color: #93c5fd;
}
:root[data-theme="dark"] .reset-password-result__code {
background: #090d14;
color: #dbeafe;
border: 1px solid rgba(148, 163, 184, 0.18);
}
:root[data-theme="dark"] .reset-password-box .el-message-box__btns .el-button:not(.el-button--primary) {
background: #20252d;
border-color: #343b46;
color: #cbd2dc;
}
:root[data-theme="dark"] .reset-password-box .el-message-box__btns .el-button:not(.el-button--primary):hover {
background: #262d37;
border-color: #475569;
color: #e7eaf0;
}
@media (max-width: 560px) {
.reset-password-confirm__hero,
.reset-password-result__hero {
grid-template-columns: 1fr;
}
.reset-password-result__title-row {
align-items: flex-start;
flex-direction: column;
gap: 6px;
}
}
</style>
<style scoped>
-1
View File
@@ -1 +0,0 @@
import{ah as a,r as e,c as t,f as l,C as s,v as n,x as i,A as o,I as c,O as r,V as u,a2 as d,G as v,a8 as p,H as m,P as f,a7 as h,n as _,F as g,aa as b,a1 as y,X as w,M as k,ai as x,w as C,L as N,u as M,R as $,W as S,af as z,Q as V,aj as T,a9 as E,ag as A,ak as j}from"./vendor-6oHsDC_h.js";import{_ as q}from"./logo-Bw7MxNqG.js";import{a as D,_ as U,u as B,A as I,i as P,b as O}from"./main-o8DXkHjp.js";import{D as L,S as R,b as H,c as F,F as W,a as K}from"./DriverIcon-B0fZKxF3.js";const J=a("admin",()=>{const a=e([]),t=e({}),l=e(!1),s=async()=>{try{l.value=!0;const e=await D.get("/api/admin/accounts");e.data.success&&(a.value=e.data.data.map(a=>({...a,showDropdown:!1})))}catch(e){throw e}finally{l.value=!1}};return{accounts:a,drivers:t,loading:l,fetchDrivers:async()=>{try{l.value=!0;const a=await D.get("/api/admin/drivers");a.data.success&&(t.value=a.data.data)}catch(a){throw a}finally{l.value=!1}},getDriverConfigSchema:async a=>{try{const e=await D.get(`/api/admin/drivers/${a}/config-schema`);if(e.data.success)return e.data.data;throw new Error("获取配置结构失败")}catch(e){throw e}},fetchAccounts:s,createAccount:async a=>{try{l.value=!0;const e=await D.post("/api/admin/accounts",a);if(e.data.success)return await s(),window.appNotification&&window.appNotification.success("账号添加成功!"),e.data;const t=e.data.message||"创建账号失败";throw window.appNotification&&window.appNotification.error(t),new Error(t)}catch(e){let a="未知错误";throw e.response&&e.response.data?e.response.data.detail?a=e.response.data.detail:e.response.data.message&&(a=e.response.data.message):e.message&&(a=e.message),window.appNotification&&window.appNotification.error(a),e}finally{l.value=!1}},updateAccount:async(a,e)=>{try{l.value=!0;const t=await D.put(`/api/admin/accounts/${a}`,e);if(t.data.success)return await s(),window.appNotification&&window.appNotification.success("账号更新成功!"),t.data;const n=t.data.message||"更新账号失败";throw window.appNotification&&window.appNotification.error("更新失败: "+n),new Error(n)}catch(t){let a="更新失败: 未知错误";throw t.response&&t.response.data?t.response.data.detail?a=`更新失败: ${t.response.data.detail}`:t.response.data.message&&(a=`更新失败: ${t.response.data.message}`):t.message&&(a=`更新失败: ${t.message}`),window.appNotification&&window.appNotification.error(a),t}finally{l.value=!1}},deleteAccount:async a=>{try{l.value=!0;const e=await D.delete(`/api/admin/accounts/${a}`);if(e.data.success)return await s(),e.data;throw new Error(e.data.message||"删除账号失败")}catch(e){throw e}finally{l.value=!1}},testAccountConnection:async a=>{try{return(await D.post(`/api/admin/accounts/${a}/test`)).data}catch(e){throw e}}}}),G={class:"select-value"},Y=["data-value","onClick"],Z={__name:"CustomSelect",props:{modelValue:{type:[String,Number],default:""},options:{type:Array,required:!0},placeholder:{type:String,default:"请选择"}},emits:["update:modelValue","change"],setup(a,{emit:g}){const b=a,y=g,w=e(!1),k=e(null),x=e(null),C=t(()=>{const a=b.options.find(a=>a.value===b.modelValue);return a?a.label:b.placeholder}),N=t(()=>{if(!w.value||!k.value)return{};const a=k.value.querySelector(".select-trigger");if(!a)return{};const e=a.getBoundingClientRect();return{position:"fixed",top:`${e.bottom+2}px`,left:`${e.left}px`,width:`${e.width}px`,zIndex:"100000"}}),M=()=>{w.value=!w.value,w.value&&_(()=>{})},$=a=>{w.value&&(k.value&&k.value.contains(a.target)||x.value&&x.value.contains(a.target)||(w.value=!1))};return l(()=>{document.addEventListener("click",$,!0),document.addEventListener("mousedown",$,!0),document.addEventListener("touchstart",$,!0)}),s(()=>{document.removeEventListener("click",$,!0),document.removeEventListener("mousedown",$,!0),document.removeEventListener("touchstart",$,!0)}),(e,t)=>(i(),n("div",{class:m(["custom-select",{open:w.value}]),ref_key:"selectRef",ref:k},[o("div",{class:"select-trigger",onClick:M},[o("span",G,r(C.value),1),t[0]||(t[0]=o("span",{class:"select-arrow"},"▼",-1))]),(i(),c(h,{to:"body"},[u(o("div",{class:"select-dropdown",ref_key:"dropdownRef",ref:x,style:f(N.value)},[(i(!0),n(v,null,p(a.options,e=>(i(),n("div",{key:e.value,class:m(["select-option",{selected:e.value===a.modelValue}]),"data-value":e.value,onClick:a=>(a=>{y("update:modelValue",a.value),y("change",a),w.value=!1})(e)},r(e.label),11,Y))),128))],4),[[d,w.value]])]))],2))}},Q=U(Z,[["__scopeId","data-v-8ee89843"]]),X={class:"ldb-header"},aa={class:"ldb-pathbar"},ea=["disabled"],ta={key:0,class:"ldb-error"},la={class:"ldb-quick"},sa=["onClick"],na={class:"ldb-list"},ia={key:0,class:"ldb-state"},oa={key:1,class:"ldb-state"},ca=["onClick","onDblclick"],ra={class:"ldb-row-name"},ua={class:"ldb-footer"},da={class:"ldb-footer-current"},va={class:"ldb-actions"},pa=["disabled"],ma=U({__name:"LocalDirBrowserModal",props:{initialPath:{type:String,default:""}},emits:["resolve","cancel"],setup(a,{emit:t}){const s=a,c=t,d=e(!1),m=e(""),f=e([]),h=e("")Line truncated
@@ -1 +1 @@
.custom-select[data-v-8ee89843]{position:relative;display:inline-block;width:100%}.select-trigger[data-v-8ee89843]{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border:1px solid #d1d5db;border-radius:8px;background:#fff;cursor:pointer;transition:all .2s ease;height:40px;box-sizing:border-box;font-size:13px;line-height:1.4}.select-trigger[data-v-8ee89843]:hover{border-color:#9ca3af}.custom-select.open .select-trigger[data-v-8ee89843]{border-color:#4c74df;box-shadow:0 0 0 2px #4c74df1a}.select-value[data-v-8ee89843]{flex:1;color:#374151;font-size:14px}.select-arrow[data-v-8ee89843]{color:#6b7280;font-size:12px;transition:transform .2s ease}.custom-select.open .select-arrow[data-v-8ee89843]{transform:rotate(180deg)}.select-dropdown[data-v-8ee89843]{position:fixed;background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 4px 16px #0000001a;z-index:100000;max-height:200px;overflow-y:auto}.select-option[data-v-8ee89843]{padding:10px 12px;cursor:pointer;font-size:14px;color:#374151;transition:background-color .2s ease}.select-option[data-v-8ee89843]:hover{background-color:#f8fafc}.select-option[data-v-8ee89843]:first-child{border-top-left-radius:8px;border-top-right-radius:8px}.select-option[data-v-8ee89843]:last-child{border-bottom-left-radius:8px;border-bottom-right-radius:8px}.ldb-overlay[data-v-b2631e0c]{position:fixed;top:0;right:0;bottom:0;left:0;background:#0f172a73;z-index:200000;display:flex;align-items:center;justify-content:center;padding:24px}.ldb-modal[data-v-b2631e0c]{background:#fff;border-radius:12px;width:560px;max-width:100%;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 20px 50px #0f172a40}.ldb-header[data-v-b2631e0c]{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #e5e7eb}.ldb-header h3[data-v-b2631e0c]{margin:0;font-size:16px;font-weight:600;color:#1f2937}.ldb-close[data-v-b2631e0c]{background:none;border:0;font-size:22px;color:#6b7280;cursor:pointer;line-height:1}.ldb-pathbar[data-v-b2631e0c]{display:flex;gap:8px;padding:12px 20px}.ldb-path-input[data-v-b2631e0c]{flex:1;height:34px;padding:0 10px;font-size:13px;border:1px solid #d1d5db;border-radius:8px;outline:none;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.ldb-path-input[data-v-b2631e0c]:focus{border-color:#3b82f6}.ldb-btn[data-v-b2631e0c]{height:34px;padding:0 12px;font-size:13px;background:#fff;border:1px solid #d1d5db;border-radius:8px;cursor:pointer;white-space:nowrap;color:#374151}.ldb-btn[data-v-b2631e0c]:hover:not(:disabled){background:#f3f4f6}.ldb-btn[data-v-b2631e0c]:disabled{opacity:.5;cursor:not-allowed}.ldb-btn.primary[data-v-b2631e0c]{background:#2563eb;border-color:#2563eb;color:#fff}.ldb-btn.primary[data-v-b2631e0c]:hover:not(:disabled){background:#1d4ed8}.ldb-btn.ghost[data-v-b2631e0c]{background:transparent}.ldb-error[data-v-b2631e0c]{margin:0 20px 8px;padding:6px 10px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;color:#b91c1c;font-size:12px}.ldb-quick[data-v-b2631e0c]{display:flex;flex-wrap:wrap;gap:6px;padding:0 20px 8px;align-items:center}.ldb-quick-label[data-v-b2631e0c]{font-size:12px;color:#6b7280;margin-right:2px}.ldb-chip[data-v-b2631e0c]{font-size:12px;padding:2px 10px;background:#f3f4f6;border:1px solid #e5e7eb;border-radius:999px;color:#374151;cursor:pointer;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.ldb-chip[data-v-b2631e0c]:hover{background:#e5e7eb}.ldb-list[data-v-b2631e0c]{flex:1;overflow-y:auto;padding:4px 12px;margin:0 8px;min-height:200px}.ldb-state[data-v-b2631e0c]{padding:20px;text-align:center;color:#9ca3af;font-size:13px}.ldb-row[data-v-b2631e0c]{display:flex;align-items:center;gap:10px;width:100%;padding:8px 12px;background:transparent;border:0;border-radius:6px;cursor:pointer;font-size:13px;color:#374151;text-align:left}.ldb-row[data-v-b2631e0c]:hover{background:#f3f4f6}.ldb-row i[data-v-b2631e0c]{color:#fbbf24;width:16px}.ldb-row-name[data-v-b2631e0c]{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ldb-footer[data-v-b2631e0c]{padding:12px 20px;border-top:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center;gap:12px}.ldb-footer-current[data-v-b2631e0c]{font-size:12px;color:#6b7280;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.ldb-footer-current code[data-v-b2631e0c]{background:#f3f4f6;padding:1px 6px;border-radius:4px;color:#374151;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.ldb-actions[data-v-b2631e0c]{display:flex;gap:8px}.dialog-overlay[data-v-8adc7c45]{position:fixed;top:0;left:0;right:0;bottom:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:100000;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.dialog[data-v-8adc7c45]{background:#fff;border-radius:16px;box-shadow:0 20px 60px #00000026;max-width:700px;width:90%;max-height:90vh;display:flex;flex-direction:column;animation:dialogSlideIn-8adc7c45 .3s ease-outLine truncated
.custom-select[data-v-8ee89843]{position:relative;display:inline-block;width:100%}.select-trigger[data-v-8ee89843]{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border:1px solid #d1d5db;border-radius:8px;background:#fff;cursor:pointer;transition:all .2s ease;height:40px;box-sizing:border-box;font-size:13px;line-height:1.4}.select-trigger[data-v-8ee89843]:hover{border-color:#9ca3af}.custom-select.open .select-trigger[data-v-8ee89843]{border-color:#4c74df;box-shadow:0 0 0 2px #4c74df1a}.select-value[data-v-8ee89843]{flex:1;color:#374151;font-size:14px}.select-arrow[data-v-8ee89843]{color:#6b7280;font-size:12px;transition:transform .2s ease}.custom-select.open .select-arrow[data-v-8ee89843]{transform:rotate(180deg)}.select-dropdown[data-v-8ee89843]{position:fixed;background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 4px 16px #0000001a;z-index:100000;max-height:200px;overflow-y:auto}.select-option[data-v-8ee89843]{padding:10px 12px;cursor:pointer;font-size:14px;color:#374151;transition:background-color .2s ease}.select-option[data-v-8ee89843]:hover{background-color:#f8fafc}.select-option[data-v-8ee89843]:first-child{border-top-left-radius:8px;border-top-right-radius:8px}.select-option[data-v-8ee89843]:last-child{border-bottom-left-radius:8px;border-bottom-right-radius:8px}.ldb-overlay[data-v-b2631e0c]{position:fixed;top:0;right:0;bottom:0;left:0;background:#0f172a73;z-index:200000;display:flex;align-items:center;justify-content:center;padding:24px}.ldb-modal[data-v-b2631e0c]{background:#fff;border-radius:12px;width:560px;max-width:100%;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 20px 50px #0f172a40}.ldb-header[data-v-b2631e0c]{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #e5e7eb}.ldb-header h3[data-v-b2631e0c]{margin:0;font-size:16px;font-weight:600;color:#1f2937}.ldb-close[data-v-b2631e0c]{background:none;border:0;font-size:22px;color:#6b7280;cursor:pointer;line-height:1}.ldb-pathbar[data-v-b2631e0c]{display:flex;gap:8px;padding:12px 20px}.ldb-path-input[data-v-b2631e0c]{flex:1;height:34px;padding:0 10px;font-size:13px;border:1px solid #d1d5db;border-radius:8px;outline:none;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.ldb-path-input[data-v-b2631e0c]:focus{border-color:#3b82f6}.ldb-btn[data-v-b2631e0c]{height:34px;padding:0 12px;font-size:13px;background:#fff;border:1px solid #d1d5db;border-radius:8px;cursor:pointer;white-space:nowrap;color:#374151}.ldb-btn[data-v-b2631e0c]:hover:not(:disabled){background:#f3f4f6}.ldb-btn[data-v-b2631e0c]:disabled{opacity:.5;cursor:not-allowed}.ldb-btn.primary[data-v-b2631e0c]{background:#2563eb;border-color:#2563eb;color:#fff}.ldb-btn.primary[data-v-b2631e0c]:hover:not(:disabled){background:#1d4ed8}.ldb-btn.ghost[data-v-b2631e0c]{background:transparent}.ldb-error[data-v-b2631e0c]{margin:0 20px 8px;padding:6px 10px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;color:#b91c1c;font-size:12px}.ldb-quick[data-v-b2631e0c]{display:flex;flex-wrap:wrap;gap:6px;padding:0 20px 8px;align-items:center}.ldb-quick-label[data-v-b2631e0c]{font-size:12px;color:#6b7280;margin-right:2px}.ldb-chip[data-v-b2631e0c]{font-size:12px;padding:2px 10px;background:#f3f4f6;border:1px solid #e5e7eb;border-radius:999px;color:#374151;cursor:pointer;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.ldb-chip[data-v-b2631e0c]:hover{background:#e5e7eb}.ldb-list[data-v-b2631e0c]{flex:1;overflow-y:auto;padding:4px 12px;margin:0 8px;min-height:200px}.ldb-state[data-v-b2631e0c]{padding:20px;text-align:center;color:#9ca3af;font-size:13px}.ldb-row[data-v-b2631e0c]{display:flex;align-items:center;gap:10px;width:100%;padding:8px 12px;background:transparent;border:0;border-radius:6px;cursor:pointer;font-size:13px;color:#374151;text-align:left}.ldb-row[data-v-b2631e0c]:hover{background:#f3f4f6}.ldb-row i[data-v-b2631e0c]{color:#fbbf24;width:16px}.ldb-row-name[data-v-b2631e0c]{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ldb-footer[data-v-b2631e0c]{padding:12px 20px;border-top:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center;gap:12px}.ldb-footer-current[data-v-b2631e0c]{font-size:12px;color:#6b7280;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.ldb-footer-current code[data-v-b2631e0c]{background:#f3f4f6;padding:1px 6px;border-radius:4px;color:#374151;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.ldb-actions[data-v-b2631e0c]{display:flex;gap:8px}.dialog-overlay[data-v-8adc7c45]{position:fixed;top:0;left:0;right:0;bottom:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:100000;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.dialog[data-v-8adc7c45]{background:#fff;border-radius:16px;box-shadow:0 20px 60px #00000026;max-width:700px;width:90%;max-height:90vh;display:flex;flex-direction:column;animation:dialogSlideIn-8adc7c45 .3s ease-outLine truncated
+1
View File
@@ -0,0 +1 @@
import{ai as a,r as e,c as t,f as l,C as s,v as n,x as i,A as o,I as c,O as r,V as u,a2 as d,G as v,a8 as p,H as m,P as f,a7 as h,n as g,F as _,aa as b,a1 as y,X as w,M as k,aj as x,w as C,L as N,u as M,R as $,W as S,af as z,Q as T,ak as V,a0 as E,ah as A,J as I,a3 as j,a9 as q,ag as D}from"./vendor-CVFEPmGb.js";import{_ as B}from"./logo-Bw7MxNqG.js";import{a as U,_ as P,u as O,A as L,i as R,b as F}from"./main-BChklWkf.js";import{D as H,S as W,b as J,c as K,F as G,a as Q}from"./DriverIcon-DCBDFADZ.js";const Y=a("admin",()=>{const a=e([]),t=e({}),l=e(!1),s=async()=>{try{l.value=!0;const e=await U.get("/api/admin/accounts");e.data.success&&(a.value=e.data.data.map(a=>({...a,showDropdown:!1})))}catch(e){throw e}finally{l.value=!1}};return{accounts:a,drivers:t,loading:l,fetchDrivers:async()=>{try{l.value=!0;const a=await U.get("/api/admin/drivers");a.data.success&&(t.value=a.data.data)}catch(a){throw a}finally{l.value=!1}},getDriverConfigSchema:async a=>{try{const e=await U.get(`/api/admin/drivers/${a}/config-schema`);if(e.data.success)return e.data.data;throw new Error("获取配置结构失败")}catch(e){throw e}},fetchAccounts:s,createAccount:async a=>{try{l.value=!0;const e=await U.post("/api/admin/accounts",a);if(e.data.success)return await s(),window.appNotification&&window.appNotification.success("账号添加成功!"),e.data;const t=e.data.message||"创建账号失败";throw window.appNotification&&window.appNotification.error(t),new Error(t)}catch(e){let a="未知错误";throw e.response&&e.response.data?e.response.data.detail?a=e.response.data.detail:e.response.data.message&&(a=e.response.data.message):e.message&&(a=e.message),window.appNotification&&window.appNotification.error(a),e}finally{l.value=!1}},updateAccount:async(a,e)=>{try{l.value=!0;const t=await U.put(`/api/admin/accounts/${a}`,e);if(t.data.success)return await s(),window.appNotification&&window.appNotification.success("账号更新成功!"),t.data;const n=t.data.message||"更新账号失败";throw window.appNotification&&window.appNotification.error("更新失败: "+n),new Error(n)}catch(t){let a="更新失败: 未知错误";throw t.response&&t.response.data?t.response.data.detail?a=`更新失败: ${t.response.data.detail}`:t.response.data.message&&(a=`更新失败: ${t.response.data.message}`):t.message&&(a=`更新失败: ${t.message}`),window.appNotification&&window.appNotification.error(a),t}finally{l.value=!1}},deleteAccount:async a=>{try{l.value=!0;const e=await U.delete(`/api/admin/accounts/${a}`);if(e.data.success)return await s(),e.data;throw new Error(e.data.message||"删除账号失败")}catch(e){throw e}finally{l.value=!1}},testAccountConnection:async a=>{try{return(await U.post(`/api/admin/accounts/${a}/test`)).data}catch(e){throw e}}}}),Z={class:"select-value"},X=["data-value","onClick"],aa={__name:"CustomSelect",props:{modelValue:{type:[String,Number],default:""},options:{type:Array,required:!0},placeholder:{type:String,default:"请选择"}},emits:["update:modelValue","change"],setup(a,{emit:_}){const b=a,y=_,w=e(!1),k=e(null),x=e(null),C=t(()=>{const a=b.options.find(a=>a.value===b.modelValue);return a?a.label:b.placeholder}),N=t(()=>{if(!w.value||!k.value)return{};const a=k.value.querySelector(".select-trigger");if(!a)return{};const e=a.getBoundingClientRect();return{position:"fixed",top:`${e.bottom+2}px`,left:`${e.left}px`,width:`${e.width}px`,zIndex:"100000"}}),M=()=>{w.value=!w.value,w.value&&g(()=>{})},$=a=>{w.value&&(k.value&&k.value.contains(a.target)||x.value&&x.value.contains(a.target)||(w.value=!1))};return l(()=>{document.addEventListener("click",$,!0),document.addEventListener("mousedown",$,!0),document.addEventListener("touchstart",$,!0)}),s(()=>{document.removeEventListener("click",$,!0),document.removeEventListener("mousedown",$,!0),document.removeEventListener("touchstart",$,!0)}),(e,t)=>(i(),n("div",{class:m(["custom-select",{open:w.value}]),ref_key:"selectRef",ref:k},[o("div",{class:"select-trigger",onClick:M},[o("span",Z,r(C.value),1),t[0]||(t[0]=o("span",{class:"select-arrow"},"▼",-1))]),(i(),c(h,{to:"body"},[u(o("div",{class:"select-dropdown",ref_key:"dropdownRef",ref:x,style:f(N.value)},[(i(!0),n(v,null,p(a.options,e=>(i(),n("div",{key:e.value,class:m(["select-option",{selected:e.value===a.modelValue}]),"data-value":e.value,onClick:a=>(a=>{y("update:modelValue",a.value),y("change",a),w.value=!1})(e)},r(e.label),11,X))),128))],4),[[d,w.value]])]))],2))}},ea=P(aa,[["__scopeId","data-v-8ee89843"]]),ta={class:"ldb-header"},la={class:"ldb-pathbar"},sa=["disabled"],na={key:0,class:"ldb-error"},ia={class:"ldb-quick"},oa=["onClick"],ca={class:"ldb-list"},ra={key:0,class:"ldb-state"},ua={key:1,class:"ldb-state"},da=["onClick","onDblclick"],va={class:"ldb-row-name"},pa={class:"ldb-footer"},ma={class:"ldb-footer-current"},fa={class:"ldb-actions"},ha=["disabled"],ga=P({__name:"LocalDirBrowserModal",props:{initialPath:{type:String,default:""}},emits:["resolve","cancel"],setup(a,{emit:t}){const s=a,c=t,d=e(Line truncated
@@ -1 +1 @@
import{c as e,v as a,A as l,P as t,H as n,x as i,r as s,w as o,f as r,C as u,O as d,V as c,F as v,aa as f,M as m,G as p,a8 as g,L as b,a1 as y,X as w,n as h}from"./vendor-6oHsDC_h.js";import{_ as k,g as S,c as N,a as x}from"./main-o8DXkHjp.js";const C={key:0,class:"lp-iconfont-use",xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink",width:"100%",height:"100%",focusable:"false",preserveAspectRatio:"xMidYMid meet"},_=["href","xlink:href"],F=["innerHTML"],M=k({__name:"SvgIcon",props:{name:{type:String,required:!0},size:{type:[Number,String],default:18},className:{type:String,default:""}},setup(s){const o=s,r=e(()=>{const e=Number(o.size);return Number.isFinite(e)&&e>0?e:18}),u=e(()=>({width:`${r.value}px`,height:`${r.value}px`})),d=e(()=>S(o.name)),c=e(()=>d.value?`#${d.value}`:""),v=e(()=>d.value?"":N(o.name));return(e,o)=>(i(),a("span",{class:n(["lp-svg-icon",s.className]),style:t(u.value),"aria-hidden":"true"},[d.value?(i(),a("svg",C,[l("use",{href:c.value,"xlink:href":c.value},null,8,_)])):(i(),a("span",{key:1,class:"lp-svg-fallback",innerHTML:v.value},null,8,F))],6))}},[["__scopeId","data-v-e40085f1"]]),$={class:"folder-selector-shell"},z={class:"folder-selector-modal-header"},I={class:"folder-selector-modal-title"},B={class:"folder-selector-search",title:"仅筛选当前目录下已加载的文件夹"},L=["disabled"],T={class:"folder-selector-content"},D={key:0,class:"breadcrumb",style:{"margin-bottom":"2px"}},E={key:0,class:"breadcrumb-dropdown"},R=["title","onClick"],P=["data-idx","title","onClick"],H={class:"breadcrumb-item-label"},j={key:1,class:"folder-filter-tip"},A={key:2,class:"folder-table-header",role:"row"},U=["onClick"],V={id:"move-folder-list",class:"file-list folder-table-body",style:{"margin-bottom":"0"}},q={key:0,class:"file-row move-folder-input folder-row folder-table-row"},G={class:"icon"},K=["disabled"],W=["disabled"],Y=["disabled"],O={key:1,class:"folder-state"},X={key:2,class:"folder-state error"},J={key:3,class:"folder-state"},Q=["onClick"],Z={class:"folder-name-cell"},ee={class:"icon"},ae={key:0,class:"folder-name-tooltip"},le={class:"folder-time-cell"},te={class:"folder-selector-modal-footer"},ne=["disabled"],ie=["disabled"],se=["disabled"],oe=k({__name:"FolderSelectorModal",props:{accountId:{type:[Number,String],required:!0},title:{type:String,default:"选择目录"},confirmText:{type:String,default:"选择当前目录"},rootId:{type:[Number,String],default:"0"},initialPath:{type:String,default:""},excludedFolderIds:{type:Array,default:()=>[]},allowCreateFolder:{type:Boolean,default:!1},showRefreshButton:{type:Boolean,default:!0}},emits:["resolve","cancel"],setup(t,{emit:k}){const S=t,N=k,C=s(!1),_=s(!1),F=s(!1),oe=s(""),re=s([]),ue=s("root"),de=s("/"),ce=s([{id:"root",name:"根目录"}]),ve=s(!1),fe=s(!1),me=s(""),pe=s(null),ge=s(new Set),be=s(""),ye=s("name"),we=s("asc");let he=null;const ke=new Map,Se=e(()=>String(S.rootId??"0")),Ne=e(()=>new Set((S.excludedFolderIds||[]).map(e=>String(e)))),xe=e(()=>"root"===ue.value?Se.value:ue.value),Ce=[{key:"name",label:"名称"},{key:"modified_time",label:"修改时间"}],_e=e(()=>ce.value.length<=3?[]:ce.value.slice(1,-2).map((e,a)=>({...e,index:a+1}))),Fe=e(()=>{const e=_e.value.length?ce.value.length-2:1;return ce.value.slice(e).map((a,l)=>({...a,index:e+l}))}),Me=e(()=>re.value.filter(e=>!Ne.value.has(String(e.id)))),$e=e(()=>{const e=be.value.trim().toLowerCase();return e?Me.value.filter(a=>String(a.name||"").toLowerCase().includes(e)):Me.value}),ze=e(()=>{const e="desc"===we.value?-1:1;return[...$e.value].sort((a,l)=>Ie(a,l,ye.value,e))}),Ie=(e,a,l,t)=>"modified_time"===l?Be(Le(null==e?void 0:e.modified_time),Le(null==a?void 0:a.modified_time),t):String((null==e?void 0:e.name)||"").localeCompare(String((null==a?void 0:a.name)||""),"zh-CN",{numeric:!0,sensitivity:"base"})*t,Be=(e,a,l)=>{const t=Number.isFinite(e),n=Number.isFinite(a);return t||n?t?n?(e-a)*l:-1:1:0},Le=e=>{if(!e)return NaN;const a=new Date(e).getTime();return Number.isFinite(a)?a:NaN},Te=e=>{if(!e)return"-";const a=new Date(e);if(Number.isNaN(a.getTime()))return"-";const l=e=>String(e).padStart(2,"0");return`${a.getFullYear()}-${l(a.getMonth()+1)}-${l(a.getDate())} ${l(a.getHours())}:${l(a.getMinutes())}`},De=()=>{const e="/"+ce.value.slice(1).map(e=>String(e.name||"").trim()).filter(Boolean).join("/");de.value="/"===e?e:e.replace(/\/+$/,"")},Ee=()=>{fe.value=!1,me.value=""},Re=()=>{be.value=""},Pe=e=>ge.value.has(String(e)),He=async()=>{await h();const e=new Set;ke.forEach((a,l)=>{a&&a.scrollWidth>a.clientWidth+1&&e.add(l)}),ge.value=e},je=()=>{ue.value="root",de.value="/",ce.value=[{id:"root",name:"根目录"}],Ee(),Re()},Ae=async(e="root",a={})=>{const l="root"===e?Se.value:String(e),t=new URLSearchParams({parent_id:l});a.forceRefresh&&t.set("force_refresh","true");const n=await x.get(`/api/cache-retention/accounts/${S.accountId}/directories?${t.toString()}`);if(!n.data.success)throw new Error(n.data.message||"加载失败");return n.Line truncated
import{c as e,v as a,x as l,A as t,P as n,H as i,r as s,w as o,f as r,C as u,O as d,V as c,F as v,aa as f,M as m,G as p,a8 as g,L as b,a1 as y,X as w,n as h}from"./vendor-CVFEPmGb.js";import{_ as k,g as S,c as N,a as x}from"./main-BChklWkf.js";const C={key:0,class:"lp-iconfont-use",xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink",width:"100%",height:"100%",focusable:"false",preserveAspectRatio:"xMidYMid meet"},_=["href","xlink:href"],F=["innerHTML"],M=k({__name:"SvgIcon",props:{name:{type:String,required:!0},size:{type:[Number,String],default:18},className:{type:String,default:""}},setup(s){const o=s,r=e(()=>{const e=Number(o.size);return Number.isFinite(e)&&e>0?e:18}),u=e(()=>({width:`${r.value}px`,height:`${r.value}px`})),d=e(()=>S(o.name)),c=e(()=>d.value?`#${d.value}`:""),v=e(()=>d.value?"":N(o.name));return(e,o)=>(l(),a("span",{class:i(["lp-svg-icon",s.className]),style:n(u.value),"aria-hidden":"true"},[d.value?(l(),a("svg",C,[t("use",{href:c.value,"xlink:href":c.value},null,8,_)])):(l(),a("span",{key:1,class:"lp-svg-fallback",innerHTML:v.value},null,8,F))],6))}},[["__scopeId","data-v-e40085f1"]]),$={class:"folder-selector-shell"},z={class:"folder-selector-modal-header"},I={class:"folder-selector-modal-title"},B={class:"folder-selector-search",title:"仅筛选当前目录下已加载的文件夹"},L=["disabled"],T={class:"folder-selector-content"},D={key:0,class:"breadcrumb",style:{"margin-bottom":"2px"}},E={key:0,class:"breadcrumb-dropdown"},R=["title","onClick"],P=["data-idx","title","onClick"],H={class:"breadcrumb-item-label"},j={key:1,class:"folder-filter-tip"},A={key:2,class:"folder-table-header",role:"row"},U=["onClick"],V={id:"move-folder-list",class:"file-list folder-table-body",style:{"margin-bottom":"0"}},q={key:0,class:"file-row move-folder-input folder-row folder-table-row"},G={class:"icon"},K=["disabled"],W=["disabled"],Y=["disabled"],O={key:1,class:"folder-state"},X={key:2,class:"folder-state error"},J={key:3,class:"folder-state"},Q=["onClick"],Z={class:"folder-name-cell"},ee={class:"icon"},ae={key:0,class:"folder-name-tooltip"},le={class:"folder-time-cell"},te={class:"folder-selector-modal-footer"},ne=["disabled"],ie=["disabled"],se=["disabled"],oe=k({__name:"FolderSelectorModal",props:{accountId:{type:[Number,String],required:!0},title:{type:String,default:"选择目录"},confirmText:{type:String,default:"选择当前目录"},rootId:{type:[Number,String],default:"0"},initialPath:{type:String,default:""},excludedFolderIds:{type:Array,default:()=>[]},allowCreateFolder:{type:Boolean,default:!1},showRefreshButton:{type:Boolean,default:!0}},emits:["resolve","cancel"],setup(n,{emit:k}){const S=n,N=k,C=s(!1),_=s(!1),F=s(!1),oe=s(""),re=s([]),ue=s("root"),de=s("/"),ce=s([{id:"root",name:"根目录"}]),ve=s(!1),fe=s(!1),me=s(""),pe=s(null),ge=s(new Set),be=s(""),ye=s("name"),we=s("asc");let he=null;const ke=new Map,Se=e(()=>String(S.rootId??"0")),Ne=e(()=>new Set((S.excludedFolderIds||[]).map(e=>String(e)))),xe=e(()=>"root"===ue.value?Se.value:ue.value),Ce=[{key:"name",label:"名称"},{key:"modified_time",label:"修改时间"}],_e=e(()=>ce.value.length<=3?[]:ce.value.slice(1,-2).map((e,a)=>({...e,index:a+1}))),Fe=e(()=>{const e=_e.value.length?ce.value.length-2:1;return ce.value.slice(e).map((a,l)=>({...a,index:e+l}))}),Me=e(()=>re.value.filter(e=>!Ne.value.has(String(e.id)))),$e=e(()=>{const e=be.value.trim().toLowerCase();return e?Me.value.filter(a=>String(a.name||"").toLowerCase().includes(e)):Me.value}),ze=e(()=>{const e="desc"===we.value?-1:1;return[...$e.value].sort((a,l)=>Ie(a,l,ye.value,e))}),Ie=(e,a,l,t)=>"modified_time"===l?Be(Le(null==e?void 0:e.modified_time),Le(null==a?void 0:a.modified_time),t):String((null==e?void 0:e.name)||"").localeCompare(String((null==a?void 0:a.name)||""),"zh-CN",{numeric:!0,sensitivity:"base"})*t,Be=(e,a,l)=>{const t=Number.isFinite(e),n=Number.isFinite(a);return t||n?t?n?(e-a)*l:-1:1:0},Le=e=>{if(!e)return NaN;const a=new Date(e).getTime();return Number.isFinite(a)?a:NaN},Te=e=>{if(!e)return"-";const a=new Date(e);if(Number.isNaN(a.getTime()))return"-";const l=e=>String(e).padStart(2,"0");return`${a.getFullYear()}-${l(a.getMonth()+1)}-${l(a.getDate())} ${l(a.getHours())}:${l(a.getMinutes())}`},De=()=>{const e="/"+ce.value.slice(1).map(e=>String(e.name||"").trim()).filter(Boolean).join("/");de.value="/"===e?e:e.replace(/\/+$/,"")},Ee=()=>{fe.value=!1,me.value=""},Re=()=>{be.value=""},Pe=e=>ge.value.has(String(e)),He=async()=>{await h();const e=new Set;ke.forEach((a,l)=>{a&&a.scrollWidth>a.clientWidth+1&&e.add(l)}),ge.value=e},je=()=>{ue.value="root",de.value="/",ce.value=[{id:"root",name:"根目录"}],Ee(),Re()},Ae=async(e="root",a={})=>{const l="root"===e?Se.value:String(e),t=new URLSearchParams({parent_id:l});a.forceRefresh&&t.set("force_refresh","true");const n=await x.get(`/api/cache-retention/accounts/${S.accountId}/directories?${t.toString()}`);if(!n.data.success)throw new Error(n.data.message||"加载失败");return n.Line truncated
+1
View File
@@ -0,0 +1 @@
import{_ as e}from"./logo-Bw7MxNqG.js";import{c as a,v as t,x as l,A as n,L as s,O as i,r as o,C as r,af as d,V as u,W as c,F as v,H as p,a2 as m,M as f,G as g,a8 as y,f as h,w as k,I as w,X as b,aa as _,a1 as C,u as S,P as T,a7 as x,n as F,ag as N,ah as $,a0 as P,J as I}from"./vendor-CVFEPmGb.js";import{_ as L,a as A,A as R,b as U,u as M}from"./main-BChklWkf.js";import{S as D,F as B,f as E,a as z,b as q,D as j}from"./DriverIcon-DCBDFADZ.js";function K(e){var a;if(!e||e.is_dir)return"folder";return{pdf:"pdf",doc:"doc",docx:"doc",xls:"sheet",xlsx:"sheet",ppt:"slide",pptx:"slide",jpg:"image",jpeg:"image",png:"image",gif:"image",webp:"image",bmp:"image",svg:"image",mp4:"video",avi:"video",mkv:"video",mov:"video",webm:"video",flv:"video",mp3:"audio",wav:"audio",flac:"audio",aac:"audio",m4a:"audio",ogg:"audio",zip:"archive",rar:"archive","7z":"archive",tar:"archive",gz:"archive",tgz:"archive",txt:"text",md:"text",log:"text",json:"text",xml:"text",csv:"text",yml:"text",yaml:"text"}[(null==(a=e.name.split(".").pop())?void 0:a.toLowerCase())||""]||"file"}const O={class:"file-action-dialog"},G={class:"file-block"},V={class:"file-icon-wrap","aria-hidden":"true"},H=["title"],X=L({__name:"FileActionDialog",props:{fileName:{type:String,default:""},file:{type:Object,default:null}},emits:["resolve","cancel"],setup(e,{emit:o}){const r=e,d=o,u=a(()=>K(r.file||{name:r.fileName,is_dir:!1}));return(a,o)=>(l(),t("div",O,[n("div",G,[n("div",V,[s(D,{name:u.value,size:30},null,8,["name"])]),n("p",{class:"file-name",title:e.fileName},i(e.fileName),9,H)]),n("button",{type:"button",class:"download-btn",onClick:o[0]||(o[0]=e=>d("resolve",{action:"download"}))},o[1]||(o[1]=[n("svg",{class:"download-svg",viewBox:"0 0 24 24",width:"16",height:"16","aria-hidden":"true",xmlns:"http://www.w3.org/2000/svg"},[n("path",{fill:"currentColor",d:"M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"})],-1),n("span",null,"下载",-1)]))]))}},[["__scopeId","data-v-32dc01b8"]]),W=(e,a)=>{const t=new Error(a);return t.field=e,t},Y=/[<>:"/\\|?*\x00-\x1F]/,J=(e,a="name")=>{const t=e.trim();if(!t)throw W(a,"请输入名称");if(t.length>100)throw W(a,"名称不能超过100个字符");if("."===t||".."===t)throw W(a,"名称不能为 . 或 ..");if(Y.test(t))throw W(a,'名称不能包含 < > : " / \\ | ? * 等特殊字符');if(/[.\s]$/.test(t))throw W(a,"名称不能以空格或点结尾");return t};function Q({accounts:e,files:a,selectedAccountId:t,selectedFilesList:l,currentPath:n,currentDirectoryInitialPath:s,operationLoading:i,setOperationLoading:o,loadFiles:r,refreshFiles:d,refreshAfterCreateFolder:u,naturalSort:c,confirm:v,form:p,custom:m,closeModal:f}){const g=()=>e.value.find(e=>e.id===t.value),y=()=>{var e,a;return(null==(a=null==(e=g())?void 0:e.config)?void 0:a.root_folder_id)||"0"},h=()=>{const e=g();return(null==e?void 0:e.config)&&"delete"===e.config.delete_mode?"permanent":"recycle"},k=e=>String((null==e?void 0:e.id)??(null==e?void 0:e.name)??""),w=()=>{Promise.resolve(d(!0)).catch(e=>{})},b=e=>{const t=new Set((e||[]).map(e=>String(e)));0!==t.size&&(a.value=a.value.filter(e=>!t.has(k(e))),l.value=l.value.filter(e=>!t.has(String(e))))},_=e=>{const t=J(e,"folderName"),l=a.value.find(e=>e.name.toLowerCase()===t.toLowerCase());if(l)throw l.is_dir?window.appNotification.warning("当前目录已存在同名文件夹,请重新输入"):window.appNotification.warning("当前目录已存在同名文件,请重新输入"),new Error("");return t},C=(e,t)=>{const l=J(t,"newName");if(l===e.name)throw W("newName","新名称不能与原名称相同");const n=a.value.find(a=>a.id!==e.id&&a.name.toLowerCase()===l.toLowerCase());if(n)throw n.is_dir?window.appNotification.warning("当前目录已存在同名文件夹,请重新输入"):window.appNotification.warning("当前目录已存在同名文件,请重新输入"),new Error("");return l},S=e=>!(!(null==e?void 0:e.field)||!e.message)&&(window.appNotification.warning(e.message),!0);window.downloadFileAction=null;const T=async e=>{if(!i.value)try{o(!0,"download_file",null,e.name);const a=encodeURIComponent(e.id),l=`/api/files/download/${t.value}/${a}?user_agent=${encodeURIComponent(navigator.userAgent)}&file_name=${encodeURIComponent(e.name||"")}`,n=document.createElement("a");n.href=l,n.download=e.name,n.style.display="none",document.body.appendChild(n),n.click(),document.body.removeChild(n),window.appNotification.success(`开始下载文件:${e.name}`)}catch(a){window.appNotification.error(`下载文件失败: ${a.message}`)}finally{o(!1)}},x=async(e,i=null,r={})=>{var d,u;const c=!1!==r.showGlobalLoading,v=r.onRequestStart||null,p=r.onRequestEnd||null,f=e.filter(Boolean);if(0===f.length)return window.appNotification.warning("请先选择要移动的文件"),!1;const g=await(async(e=null)=>{const n=(e||a.value.filter(e=>l.value.includes(e.id))).filter(e=>e.is_dir).map(e=>String(e.id)),i=await m({title:"",size:null,closable:!1,hideFooter:!0,modalClass:"modal-folder-selector",bodyClass:"modal-body-flush",componenLine truncated
+1
View File
@@ -0,0 +1 @@
.file-action-dialog[data-v-32dc01b8]{padding:14px 20px 16px;min-width:260px;max-width:min(380px,92vw);margin:0 auto;display:flex;flex-direction:column;align-items:stretch;gap:14px}.file-block[data-v-32dc01b8]{display:flex;flex-direction:column;align-items:center;gap:6px;width:100%}.file-icon-wrap[data-v-32dc01b8]{line-height:0;display:inline-flex;align-items:center;justify-content:center}.file-name[data-v-32dc01b8]{margin:0;width:100%;text-align:center;font-size:.9375rem;font-weight:600;color:#334155;line-height:1.5;word-break:break-word;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}.download-btn[data-v-32dc01b8]{align-self:center;width:100%;max-width:200px;background:linear-gradient(135deg,#4c74df,#2563eb);color:#fff;border:none;border-radius:8px;padding:9px 20px;font-size:.8125rem;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:7px;transition:filter .2s,box-shadow .2s;box-shadow:0 2px 8px #2563eb38}.download-btn[data-v-32dc01b8]:hover{filter:brightness(1.05);box-shadow:0 4px 12px #2563eb59}.download-svg[data-v-32dc01b8]{flex-shrink:0;opacity:.95}.notice-dialog[data-v-7f43a3c4]{width:100%;max-width:100%;box-sizing:border-box}.notice-title-row[data-v-7f43a3c4]{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.notice-title[data-v-7f43a3c4]{font-size:17px;font-weight:700;color:#111827;line-height:1.5}.notice-close[data-v-7f43a3c4]{width:24px;height:24px;border:none;background:transparent;color:#64748b;font-size:24px;line-height:1;cursor:pointer}.notice-card[data-v-7f43a3c4]{padding:14px 16px;border-radius:14px;background:#f8fafc;border:1px solid #e2e8f0;overflow:hidden}.notice-flow[data-v-7f43a3c4]{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1.2fr) auto minmax(0,1fr);align-items:center;gap:10px}.notice-node[data-v-7f43a3c4]{min-width:0;display:flex;flex-direction:column;align-items:center;gap:6px;padding:10px 8px;border-radius:12px;background:#fff;border:1px solid #e5e7eb;color:#334155}.notice-node.wide[data-v-7f43a3c4]{min-width:0}.notice-icon[data-v-7f43a3c4]{line-height:0;display:inline-flex;align-items:center;justify-content:center}.notice-arrow[data-v-7f43a3c4]{color:#94a3b8;font-size:20px}.notice-tips[data-v-7f43a3c4]{margin-top:14px;display:flex;flex-direction:column;gap:8px;color:#475569;font-size:14px;line-height:1.8}.notice-tip[data-v-7f43a3c4]{display:flex;align-items:stretch;gap:10px}.notice-tip-bar[data-v-7f43a3c4]{flex-shrink:0;width:4px;border-radius:999px;background:linear-gradient(135deg,#4c74df,#02a6f0)}.notice-checkbox[data-v-7f43a3c4]{display:flex;align-items:center;gap:8px;margin-top:18px;color:#64748b;font-size:14px;cursor:pointer}.notice-actions[data-v-7f43a3c4]{display:flex;justify-content:center;margin-top:18px}.notice-confirm[data-v-7f43a3c4]{min-width:168px;height:38px;border:none;border-radius:10px;background:linear-gradient(135deg,#4c74df,#02a6f0);color:#fff;cursor:pointer;font-size:14px}@media (max-width: 640px){.notice-dialog[data-v-7f43a3c4]{width:100%;max-width:100%}.notice-flow[data-v-7f43a3c4]{grid-template-columns:1fr}.notice-arrow[data-v-7f43a3c4]{justify-self:center;transform:rotate(90deg)}}.conflict-dialog[data-v-6d317901]{min-width:min(520px,calc(100vw - 48px))}.dialog-head[data-v-6d317901],.dialog-actions[data-v-6d317901]{display:flex;align-items:center;justify-content:space-between}.dialog-title[data-v-6d317901]{font-size:28px;font-weight:700;color:#111827}.dialog-close[data-v-6d317901]{width:24px;height:24px;border:none;background:transparent;color:#64748b;font-size:24px;line-height:1;cursor:pointer}.dialog-body[data-v-6d317901]{margin-top:18px;color:#475569;font-size:15px;line-height:1.8}.file-name[data-v-6d317901]{margin-top:4px;color:#111827;word-break:break-all}.body-subtitle[data-v-6d317901]{margin-top:4px}.dialog-checkbox[data-v-6d317901]{display:flex;align-items:center;gap:8px;margin-top:14px;color:#64748b;font-size:14px;cursor:pointer}.dialog-actions[data-v-6d317901]{justify-content:flex-end;gap:10px;margin-top:22px}.dialog-btn[data-v-6d317901]{min-width:88px;height:36px;border-radius:10px;cursor:pointer;border:1px solid #dbe3ee}.dialog-btn.secondary[data-v-6d317901]{background:#fff;color:#475569}.dialog-btn.primary[data-v-6d317901]{min-width:100px;border:none;background:linear-gradient(135deg,#4c74df,#02a6f0);color:#fff}.task-delete-dialog[data-v-c9e8d5c6]{min-width:min(520px,calc(100vw - 48px));width:100%;box-sizing:border-box}.dialog-head[data-v-c9e8d5c6]{display:flex;align-items:center;justify-content:space-between;padding:20px 22px 0}.dialog-title[data-v-c9e8d5c6]{font-size:28px;font-weight:700;color:#111827}.dialog-close[data-v-c9e8d5c6]{width:24px;height:24px;border:none;background:transparent;color:#64748b;font-size:24px;line-height:1;cursor:pointer}.dialog-body[data-v-c9e8d5c6]{padding:18px 22px 16px;color:#475569;font-size:15px;line-height:1.8}.dialog-name[data-v-c9e8d5c6]{margin-top:4px;cLine truncated
-1
View File
@@ -1 +0,0 @@
.file-action-dialog[data-v-32dc01b8]{padding:14px 20px 16px;min-width:260px;max-width:min(380px,92vw);margin:0 auto;display:flex;flex-direction:column;align-items:stretch;gap:14px}.file-block[data-v-32dc01b8]{display:flex;flex-direction:column;align-items:center;gap:6px;width:100%}.file-icon-wrap[data-v-32dc01b8]{line-height:0;display:inline-flex;align-items:center;justify-content:center}.file-name[data-v-32dc01b8]{margin:0;width:100%;text-align:center;font-size:.9375rem;font-weight:600;color:#334155;line-height:1.5;word-break:break-word;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}.download-btn[data-v-32dc01b8]{align-self:center;width:100%;max-width:200px;background:linear-gradient(135deg,#4c74df,#2563eb);color:#fff;border:none;border-radius:8px;padding:9px 20px;font-size:.8125rem;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:7px;transition:filter .2s,box-shadow .2s;box-shadow:0 2px 8px #2563eb38}.download-btn[data-v-32dc01b8]:hover{filter:brightness(1.05);box-shadow:0 4px 12px #2563eb59}.download-svg[data-v-32dc01b8]{flex-shrink:0;opacity:.95}.toolbar-left[data-v-6bc5d97e]{display:flex;align-items:center;gap:10px;min-width:0}.toolbar-right[data-v-6bc5d97e]{display:flex;align-items:center;gap:10px;margin-left:auto;min-width:0}.create-menu[data-v-6bc5d97e]{position:relative}.create-menu[data-v-6bc5d97e]:after{content:"";position:absolute;left:0;right:0;top:100%;height:8px}.create-menu-trigger[data-v-6bc5d97e]{display:inline-flex!important;align-items:center;justify-content:center;gap:8px;min-width:96px}.create-menu-main[data-v-6bc5d97e]{display:inline-flex;align-items:center;justify-content:center;gap:8px;line-height:1}.create-menu-icon[data-v-6bc5d97e]{transform:translateY(1px)}.create-menu-label[data-v-6bc5d97e]{display:inline-flex;align-items:center;line-height:1;transform:translateY(2px)}.create-menu-arrow[data-v-6bc5d97e]{margin-left:0;transition:transform .2s ease;display:inline-flex;align-items:center;justify-content:center;color:#ffffffeb;line-height:0;flex-shrink:0}.create-menu-arrow.open[data-v-6bc5d97e]{transform:rotate(180deg)}.create-menu-dropdown[data-v-6bc5d97e]{position:absolute;top:calc(100% + 6px);left:0;min-width:160px;padding:6px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;box-shadow:0 8px 24px #0f172a1f;z-index:20}.create-menu-item[data-v-6bc5d97e]{width:100%;display:flex;align-items:center;gap:8px;padding:9px 10px;border:none;background:transparent;border-radius:8px;color:#334155;cursor:pointer;font-size:14px;text-align:left}.create-menu-item[data-v-6bc5d97e]:hover{background:#f8fafc}.batch-actions[data-v-6bc5d97e]{display:flex;gap:10px;margin-left:20px;padding-left:20px;border-left:1px solid #ddd}.transfer-menu[data-v-6bc5d97e]{position:relative}.transfer-menu[data-v-6bc5d97e]:after{content:"";position:absolute;left:0;right:0;top:100%;height:8px}.transfer-menu-arrow[data-v-6bc5d97e]{transition:transform .2s ease;display:inline-flex;align-items:center;justify-content:center;color:var(--text-regular);line-height:0;flex-shrink:0}.transfer-menu-arrow.open[data-v-6bc5d97e]{transform:rotate(180deg)}.transfer-menu-dropdown[data-v-6bc5d97e]{position:absolute;top:calc(100% + 6px);left:0;min-width:160px;padding:6px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;box-shadow:0 8px 24px #0f172a1f;z-index:20}.transfer-menu-item[data-v-6bc5d97e]{width:100%;display:flex;align-items:center;gap:8px;padding:9px 10px;border:none;background:transparent;border-radius:8px;color:#334155;cursor:pointer;font-size:14px;text-align:left}.transfer-menu-item[data-v-6bc5d97e]:hover{background:#f8fafc}.toolbar-rect[data-v-6bc5d97e]{height:40px;border:1px solid #e2e8f0;border-radius:8px;background:#fff;box-shadow:0 1px 2px #0000000d}.transfer-status-chip[data-v-6bc5d97e]{position:relative;height:40px;display:inline-flex;align-items:center;gap:8px;padding:0 14px 0 12px;border:1px solid #e2e8f0;border-radius:8px;background:#fff;color:#64748b;cursor:pointer;transition:all .2s ease;white-space:nowrap;box-shadow:0 1px 2px #0000000d}.transfer-status-chip[data-v-6bc5d97e]:hover{color:#475569;border-color:#cbd5e1;background:#f8fafc;box-shadow:0 2px 4px #00000014}.transfer-status-chip.active[data-v-6bc5d97e]{color:#2563eb;border-color:#bfdbfe;background:#eff6ff}.transfer-status-chip.failed[data-v-6bc5d97e]{color:#dc2626;border-color:#fecaca;background:#fef2f2}.transfer-status-chip.success[data-v-6bc5d97e]{color:#475569;border-color:#e2e8f0;background:#fff}.transfer-status-icon-wrap[data-v-6bc5d97e]{position:relative;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center}.transfer-status-icon[data-v-6bc5d97e]{font-size:14px;line-height:1;transform:translateY(0);will-change:transform,opacity}.transfer-status-icon-svg[data-v-6bc5d97e]{font-size:0;line-height:0;display:inline-flex;align-items:center;justify-content:center}.transfer-status-chip.active .transfer-Line truncated
-1
View File
@@ -1 +0,0 @@
import{_ as e}from"./logo-Bw7MxNqG.js";import{c as a,v as t,x as l,A as n,L as i,O as s,F as o,H as r,V as u,a2 as d,M as c,G as v,a8 as p,r as m,f,C as g,w,I as y,X as h,aa as k,a1 as b,u as _,af as C,P as S,a7 as x,n as N,W as F,ag as $,a0 as I,J as L}from"./vendor-6oHsDC_h.js";import{_ as A,a as M,A as T,b as P,u as E}from"./main-o8DXkHjp.js";import{S as z,F as B,f as D,a as R,b as U,D as j}from"./DriverIcon-B0fZKxF3.js";function K(e){var a;if(!e||e.is_dir)return"folder";return{pdf:"pdf",doc:"doc",docx:"doc",xls:"sheet",xlsx:"sheet",ppt:"slide",pptx:"slide",jpg:"image",jpeg:"image",png:"image",gif:"image",webp:"image",bmp:"image",svg:"image",mp4:"video",avi:"video",mkv:"video",mov:"video",webm:"video",flv:"video",mp3:"audio",wav:"audio",flac:"audio",aac:"audio",m4a:"audio",ogg:"audio",zip:"archive",rar:"archive","7z":"archive",tar:"archive",gz:"archive",tgz:"archive",txt:"text",md:"text",log:"text",json:"text",xml:"text",csv:"text",yml:"text",yaml:"text"}[(null==(a=e.name.split(".").pop())?void 0:a.toLowerCase())||""]||"file"}const q={class:"file-action-dialog"},G={class:"file-block"},O={class:"file-icon-wrap","aria-hidden":"true"},V=["title"],X=A({__name:"FileActionDialog",props:{fileName:{type:String,default:""},file:{type:Object,default:null}},emits:["resolve","cancel"],setup(e,{emit:o}){const r=e,u=o,d=a(()=>K(r.file||{name:r.fileName,is_dir:!1}));return(a,o)=>(l(),t("div",q,[n("div",G,[n("div",O,[i(z,{name:d.value,size:30},null,8,["name"])]),n("p",{class:"file-name",title:e.fileName},s(e.fileName),9,V)]),n("button",{type:"button",class:"download-btn",onClick:o[0]||(o[0]=e=>u("resolve",{action:"download"}))},o[1]||(o[1]=[n("svg",{class:"download-svg",viewBox:"0 0 24 24",width:"16",height:"16","aria-hidden":"true",xmlns:"http://www.w3.org/2000/svg"},[n("path",{fill:"currentColor",d:"M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"})],-1),n("span",null,"下载",-1)]))]))}},[["__scopeId","data-v-32dc01b8"]]),W=(e,a)=>{const t=new Error(a);return t.field=e,t},Y=/[<>:"/\\|?*\x00-\x1F]/,H=(e,a="name")=>{const t=e.trim();if(!t)throw W(a,"请输入名称");if(t.length>100)throw W(a,"名称不能超过100个字符");if("."===t||".."===t)throw W(a,"名称不能为 . 或 ..");if(Y.test(t))throw W(a,'名称不能包含 < > : " / \\ | ? * 等特殊字符');if(/[.\s]$/.test(t))throw W(a,"名称不能以空格或点结尾");return t};function J({accounts:e,files:a,selectedAccountId:t,selectedFilesList:l,currentPath:n,currentDirectoryInitialPath:i,operationLoading:s,setOperationLoading:o,loadFiles:r,refreshFiles:u,refreshAfterCreateFolder:d,naturalSort:c,confirm:v,form:p,custom:m,closeModal:f}){const g=()=>e.value.find(e=>e.id===t.value),w=()=>{var e,a;return(null==(a=null==(e=g())?void 0:e.config)?void 0:a.root_folder_id)||"0"},y=()=>{const e=g();return(null==e?void 0:e.config)&&"delete"===e.config.delete_mode?"permanent":"recycle"},h=e=>String((null==e?void 0:e.id)??(null==e?void 0:e.name)??""),k=()=>{Promise.resolve(u(!0)).catch(e=>{})},b=e=>{const t=new Set((e||[]).map(e=>String(e)));0!==t.size&&(a.value=a.value.filter(e=>!t.has(h(e))),l.value=l.value.filter(e=>!t.has(String(e))))},_=e=>{const t=H(e,"folderName"),l=a.value.find(e=>e.name.toLowerCase()===t.toLowerCase());if(l)throw l.is_dir?window.appNotification.warning("当前目录已存在同名文件夹,请重新输入"):window.appNotification.warning("当前目录已存在同名文件,请重新输入"),new Error("");return t},C=(e,t)=>{const l=H(t,"newName");if(l===e.name)throw W("newName","新名称不能与原名称相同");const n=a.value.find(a=>a.id!==e.id&&a.name.toLowerCase()===l.toLowerCase());if(n)throw n.is_dir?window.appNotification.warning("当前目录已存在同名文件夹,请重新输入"):window.appNotification.warning("当前目录已存在同名文件,请重新输入"),new Error("");return l},S=e=>!(!(null==e?void 0:e.field)||!e.message)&&(window.appNotification.warning(e.message),!0);window.downloadFileAction=null;const x=async e=>{if(!s.value)try{o(!0,"download_file",null,e.name);const a=encodeURIComponent(e.id),l=`/api/files/download/${t.value}/${a}?user_agent=${encodeURIComponent(navigator.userAgent)}&file_name=${encodeURIComponent(e.name||"")}`,n=document.createElement("a");n.href=l,n.download=e.name,n.style.display="none",document.body.appendChild(n),n.click(),document.body.removeChild(n),window.appNotification.success(`开始下载文件:${e.name}`)}catch(a){window.appNotification.error(`下载文件失败: ${a.message}`)}finally{o(!1)}},N=async(e,s=null,r={})=>{var u,d;const c=!1!==r.showGlobalLoading,v=r.onRequestStart||null,p=r.onRequestEnd||null,f=e.filter(Boolean);if(0===f.length)return window.appNotification.warning("请先选择要移动的文件"),!1;const g=await(async(e=null)=>{const n=(e||a.value.filter(e=>l.value.includes(e.id))).filter(e=>e.is_dir).map(e=>String(e.id)),s=await m({title:"",size:null,closable:!1,hideFooter:!0,modalClass:"modal-folder-selector",bodyClass:"modal-body-flush",component:B,componentPropsLine truncated
@@ -1 +1 @@
@charset "UTF-8";:root{--el-color-white:#ffffff;--el-color-black:#000000;--el-color-primary-rgb:64,158,255;--el-color-success-rgb:103,194,58;--el-color-warning-rgb:230,162,60;--el-color-danger-rgb:245,108,108;--el-color-error-rgb:245,108,108;--el-color-info-rgb:144,147,153;--el-font-size-extra-large:20px;--el-font-size-large:18px;--el-font-size-medium:16px;--el-font-size-base:14px;--el-font-size-small:13px;--el-font-size-extra-small:12px;--el-font-family:"Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;--el-font-weight-primary:500;--el-font-line-height-primary:24px;--el-index-normal:1;--el-index-top:1000;--el-index-popper:2000;--el-border-radius-base:4px;--el-border-radius-small:2px;--el-border-radius-round:20px;--el-border-radius-circle:100%;--el-transition-duration:.3s;--el-transition-duration-fast:.2s;--el-transition-function-ease-in-out-bezier:cubic-bezier(.645,.045,.355,1);--el-transition-function-fast-bezier:cubic-bezier(.23,1,.32,1);--el-transition-all:all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);--el-transition-fade:opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);--el-transition-md-fade:transform var(--el-transition-duration) var(--el-transition-function-fast-bezier),opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);--el-transition-fade-linear:opacity var(--el-transition-duration-fast) linear;--el-transition-border:border-color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-transition-box-shadow:box-shadow var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-transition-color:color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-component-size-large:40px;--el-component-size:32px;--el-component-size-small:24px;color-scheme:light;--el-color-primary:#409eff;--el-color-primary-light-3:rgb(121,187,255);--el-color-primary-light-5:rgb(160,207,255);--el-color-primary-light-7:rgb(198,226,255);--el-color-primary-light-8:rgb(217,236,255);--el-color-primary-light-9:rgb(236,245,255);--el-color-primary-dark-2:rgb(51,126,204);--el-color-success:#67c23a;--el-color-success-light-3:rgb(149,212,117);--el-color-success-light-5:rgb(179,225,157);--el-color-success-light-7:rgb(209,237,196);--el-color-success-light-8:rgb(225,243,216);--el-color-success-light-9:rgb(240,249,235);--el-color-success-dark-2:rgb(82,155,46);--el-color-warning:#e6a23c;--el-color-warning-light-3:rgb(238,190,119);--el-color-warning-light-5:rgb(243,209,158);--el-color-warning-light-7:rgb(248,227,197);--el-color-warning-light-8:rgb(250,236,216);--el-color-warning-light-9:rgb(253,246,236);--el-color-warning-dark-2:rgb(184,130,48);--el-color-danger:#f56c6c;--el-color-danger-light-3:rgb(248,152,152);--el-color-danger-light-5:rgb(250,182,182);--el-color-danger-light-7:rgb(252,211,211);--el-color-danger-light-8:rgb(253,226,226);--el-color-danger-light-9:rgb(254,240,240);--el-color-danger-dark-2:rgb(196,86,86);--el-color-error:#f56c6c;--el-color-error-light-3:rgb(248,152,152);--el-color-error-light-5:rgb(250,182,182);--el-color-error-light-7:rgb(252,211,211);--el-color-error-light-8:rgb(253,226,226);--el-color-error-light-9:rgb(254,240,240);--el-color-error-dark-2:rgb(196,86,86);--el-color-info:#909399;--el-color-info-light-3:rgb(177,179,184);--el-color-info-light-5:rgb(200,201,204);--el-color-info-light-7:rgb(222,223,224);--el-color-info-light-8:rgb(233,233,235);--el-color-info-light-9:rgb(244,244,245);--el-color-info-dark-2:rgb(115,118,122);--el-bg-color:#ffffff;--el-bg-color-page:#f2f3f5;--el-bg-color-overlay:#ffffff;--el-text-color-primary:#303133;--el-text-color-regular:#606266;--el-text-color-secondary:#909399;--el-text-color-placeholder:#a8abb2;--el-text-color-disabled:#c0c4cc;--el-border-color:#dcdfe6;--el-border-color-light:#e4e7ed;--el-border-color-lighter:#ebeef5;--el-border-color-extra-light:#f2f6fc;--el-border-color-dark:#d4d7de;--el-border-color-darker:#cdd0d6;--el-fill-color:#f0f2f5;--el-fill-color-light:#f5f7fa;--el-fill-color-lighter:#fafafa;--el-fill-color-extra-light:#fafcff;--el-fill-color-dark:#ebedf0;--el-fill-color-darker:#e6e8eb;--el-fill-color-blank:#ffffff;--el-box-shadow:0px 12px 32px 4px rgba(0,0,0,.04),0px 8px 20px rgba(0,0,0,.08);--el-box-shadow-light:0px 0px 12px rgba(0,0,0,.12);--el-box-shadow-lighter:0px 0px 6px rgba(0,0,0,.12);--el-box-shadow-dark:0px 16px 48px 16px rgba(0,0,0,.08),0px 12px 32px rgba(0,0,0,.12),0px 8px 16px -8px rgba(0,0,0,.16);--el-disabled-bg-color:var(--el-fill-color-light);--el-disabled-text-color:var(--el-text-color-placeholder);--el-disabled-border-color:var(--el-border-color-light);--el-overlay-color:rgba(0,0,0,.8);--el-overlay-color-light:rgba(0,0,0,.7);--el-overlay-color-lighter:rgba(0,0,0,.5);--el-mask-color:rgba(255,255,255,.9);--el-mask-color-extra-light:rgba(255,255,255,.3);--el-border-width:1px;--el-border-style:solid;Line truncated
@charset "UTF-8";:root{--el-color-white:#ffffff;--el-color-black:#000000;--el-color-primary-rgb:64,158,255;--el-color-success-rgb:103,194,58;--el-color-warning-rgb:230,162,60;--el-color-danger-rgb:245,108,108;--el-color-error-rgb:245,108,108;--el-color-info-rgb:144,147,153;--el-font-size-extra-large:20px;--el-font-size-large:18px;--el-font-size-medium:16px;--el-font-size-base:14px;--el-font-size-small:13px;--el-font-size-extra-small:12px;--el-font-family:"Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;--el-font-weight-primary:500;--el-font-line-height-primary:24px;--el-index-normal:1;--el-index-top:1000;--el-index-popper:2000;--el-border-radius-base:4px;--el-border-radius-small:2px;--el-border-radius-round:20px;--el-border-radius-circle:100%;--el-transition-duration:.3s;--el-transition-duration-fast:.2s;--el-transition-function-ease-in-out-bezier:cubic-bezier(.645,.045,.355,1);--el-transition-function-fast-bezier:cubic-bezier(.23,1,.32,1);--el-transition-all:all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);--el-transition-fade:opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);--el-transition-md-fade:transform var(--el-transition-duration) var(--el-transition-function-fast-bezier),opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);--el-transition-fade-linear:opacity var(--el-transition-duration-fast) linear;--el-transition-border:border-color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-transition-box-shadow:box-shadow var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-transition-color:color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-component-size-large:40px;--el-component-size:32px;--el-component-size-small:24px;color-scheme:light;--el-color-primary:#409eff;--el-color-primary-light-3:rgb(121,187,255);--el-color-primary-light-5:rgb(160,207,255);--el-color-primary-light-7:rgb(198,226,255);--el-color-primary-light-8:rgb(217,236,255);--el-color-primary-light-9:rgb(236,245,255);--el-color-primary-dark-2:rgb(51,126,204);--el-color-success:#67c23a;--el-color-success-light-3:rgb(149,212,117);--el-color-success-light-5:rgb(179,225,157);--el-color-success-light-7:rgb(209,237,196);--el-color-success-light-8:rgb(225,243,216);--el-color-success-light-9:rgb(240,249,235);--el-color-success-dark-2:rgb(82,155,46);--el-color-warning:#e6a23c;--el-color-warning-light-3:rgb(238,190,119);--el-color-warning-light-5:rgb(243,209,158);--el-color-warning-light-7:rgb(248,227,197);--el-color-warning-light-8:rgb(250,236,216);--el-color-warning-light-9:rgb(253,246,236);--el-color-warning-dark-2:rgb(184,130,48);--el-color-danger:#f56c6c;--el-color-danger-light-3:rgb(248,152,152);--el-color-danger-light-5:rgb(250,182,182);--el-color-danger-light-7:rgb(252,211,211);--el-color-danger-light-8:rgb(253,226,226);--el-color-danger-light-9:rgb(254,240,240);--el-color-danger-dark-2:rgb(196,86,86);--el-color-error:#f56c6c;--el-color-error-light-3:rgb(248,152,152);--el-color-error-light-5:rgb(250,182,182);--el-color-error-light-7:rgb(252,211,211);--el-color-error-light-8:rgb(253,226,226);--el-color-error-light-9:rgb(254,240,240);--el-color-error-dark-2:rgb(196,86,86);--el-color-info:#909399;--el-color-info-light-3:rgb(177,179,184);--el-color-info-light-5:rgb(200,201,204);--el-color-info-light-7:rgb(222,223,224);--el-color-info-light-8:rgb(233,233,235);--el-color-info-light-9:rgb(244,244,245);--el-color-info-dark-2:rgb(115,118,122);--el-bg-color:#ffffff;--el-bg-color-page:#f2f3f5;--el-bg-color-overlay:#ffffff;--el-text-color-primary:#303133;--el-text-color-regular:#606266;--el-text-color-secondary:#909399;--el-text-color-placeholder:#a8abb2;--el-text-color-disabled:#c0c4cc;--el-border-color:#dcdfe6;--el-border-color-light:#e4e7ed;--el-border-color-lighter:#ebeef5;--el-border-color-extra-light:#f2f6fc;--el-border-color-dark:#d4d7de;--el-border-color-darker:#cdd0d6;--el-fill-color:#f0f2f5;--el-fill-color-light:#f5f7fa;--el-fill-color-lighter:#fafafa;--el-fill-color-extra-light:#fafcff;--el-fill-color-dark:#ebedf0;--el-fill-color-darker:#e6e8eb;--el-fill-color-blank:#ffffff;--el-box-shadow:0px 12px 32px 4px rgba(0,0,0,.04),0px 8px 20px rgba(0,0,0,.08);--el-box-shadow-light:0px 0px 12px rgba(0,0,0,.12);--el-box-shadow-lighter:0px 0px 6px rgba(0,0,0,.12);--el-box-shadow-dark:0px 16px 48px 16px rgba(0,0,0,.08),0px 12px 32px rgba(0,0,0,.12),0px 8px 16px -8px rgba(0,0,0,.16);--el-disabled-bg-color:var(--el-fill-color-light);--el-disabled-text-color:var(--el-text-color-placeholder);--el-disabled-border-color:var(--el-border-color-light);--el-overlay-color:rgba(0,0,0,.8);--el-overlay-color-light:rgba(0,0,0,.7);--el-overlay-color-lighter:rgba(0,0,0,.5);--el-mask-color:rgba(255,255,255,.9);--el-mask-color-extra-light:rgba(255,255,255,.3);--el-border-width:1px;--el-border-style:solid;Line truncated
@@ -1,4 +1,4 @@
import{r as t,c as e,f as r,Q as i,w as n,v as a,x as s,A as o,R as u,ag as l,L as h,af as p,J as c,M as f,V as d,a1 as _,aa as m,a9 as v,X as g,O as y}from"./vendor-6oHsDC_h.js";import{_ as w}from"./logo-Bw7MxNqG.js";import{_ as b,a as x}from"./main-o8DXkHjp.js";import{E as T,a as k,b as M,c as O,d as C}from"./elementPlus-CM9o6iOw.js";function S(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function A(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}
import{r as t,c as e,f as r,Q as i,w as n,v as a,x as s,A as o,R as u,ah as l,L as h,af as c,J as p,M as d,V as f,a1 as _,aa as m,a9 as v,X as g,O as y}from"./vendor-CVFEPmGb.js";import{_ as w}from"./logo-Bw7MxNqG.js";import{_ as x,a as b}from"./main-BChklWkf.js";import{E as T,a as k,b as M,c as S,d as O}from"./elementPlus-D4GhVLmS.js";function C(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function A(t,e){t.prototype=Object.create(e.prototype),t.prototype.constructor=t,t.__proto__=e}
/*!
* GSAP 3.15.0
* https://gsap.com
@@ -6,7 +6,7 @@ import{r as t,c as e,f as r,Q as i,w as n,v as a,x as s,A as o,R as u,ag as l,L
* @license Copyright 2008-2026, GreenSock. All rights reserved.
* Subject to the terms at https://gsap.com/standard-license
* @author: Jack Doyle, jack@greensock.com
*/var F,D,R,E,P,z,L,q,X,B,Y,I,N,U,V,j,G,H,Q,W,Z,$,K,J,tt,et,rt,it,nt,at,st,ot,ut={autoSleep:120,force3D:"auto",nullTargetWarn:1,units:{lineHeight:""}},lt={duration:.5,overwrite:!1,delay:0},ht=1e8,pt=1e-8,ct=2*Math.PI,ft=ct/4,dt=0,_t=Math.sqrt,mt=Math.cos,vt=Math.sin,gt=function(t){return"string"==typeof t},yt=function(t){return"function"==typeof t},wt=function(t){return"number"==typeof t},bt=function(t){return void 0===t},xt=function(t){return"object"==typeof t},Tt=function(t){return!1!==t},kt=function(){return"undefined"!=typeof window},Mt=function(t){return yt(t)||gt(t)},Ot="function"==typeof ArrayBuffer&&ArrayBuffer.isView||function(){},Ct=Array.isArray,St=/random\([^)]+\)/g,At=/,\s*/g,Ft=/(?:-?\.?\d|\.)+/gi,Dt=/[-+=.]*\d+[.e\-+]*\d*[e\-+]*\d*/g,Rt=/[-+=.]*\d+[.e-]*\d*[a-z%]*/g,Et=/[-+=.]*\d+\.?\d*(?:e-|e\+)?\d*/gi,Pt=/[+-]=-?[.\d]+/,zt=/[^,'"\[\]\s]+/gi,Lt=/^[+\-=e\s\d]*\d+[.\d]*([a-z]*|%)\s*$/i,qt={},Xt={},Bt=function(t){return(Xt=_e(t,qt))&&di},Yt=function(t,e){},It=function(t,e){return!e&&void 0},Nt=function(t,e){return t&&(qt[t]=e)&&Xt&&(Xt[t]=e)||qt},Ut=function(){return 0},Vt={suppressEvents:!0,isStart:!0,kill:!1},jt={suppressEvents:!0,kill:!1},Gt={suppressEvents:!0},Ht={},Qt=[],Wt={},Zt={},$t={},Kt=30,Jt=[],te="",ee=function(t){var e,r,i=t[0];if(xt(i)||yt(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=Jt.length;r--&&!Jt[r].targetTest(i););e=Jt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Sr(t[r],e)))||t.splice(r,1);return t},re=function(t){return t._gsap||ee(Ge(t))[0]._gsap},ie=function(t,e,r){return(r=t[e])&&yt(r)?t[e]():bt(r)&&t.getAttribute&&t.getAttribute(e)||r},ne=function(t,e){return(t=t.split(",")).forEach(e)||t},ae=function(t){return Math.round(1e5*t)/1e5||0},se=function(t){return Math.round(1e7*t)/1e7||0},oe=function(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i},ue=function(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++i<r;);return i<r},le=function(){var t,e,r=Qt.length,i=Qt.slice(0);for(Wt={},Qt.length=0,t=0;t<r;t++)(e=i[t])&&e._lazy&&(e.render(e._lazy[0],e._lazy[1],!0)._lazy=0)},he=function(t){return!!(t._initted||t._startAt||t.add)},pe=function(t,e,r,i){Qt.length&&!D&&le(),t.render(e,r,!!(D&&e<0&&he(t))),Qt.length&&!D&&le()},ce=function(t){var e=parseFloat(t);return(e||0===e)&&(t+"").match(zt).length<2?e:gt(t)?t.trim():t},fe=function(t){return t},de=function(t,e){for(var r in e)r in t||(t[r]=e[r]);return t},_e=function(t,e){for(var r in e)t[r]=e[r];return t},me=function t(e,r){for(var i in r)"__proto__"!==i&&"constructor"!==i&&"prototype"!==i&&(e[i]=xt(r[i])?t(e[i]||(e[i]={}),r[i]):r[i]);return e},ve=function(t,e){var r,i={};for(r in t)r in e||(i[r]=t[r]);return i},ge=function(t){var e,r=t.parent||E,i=t.keyframes?(e=Ct(t.keyframes),function(t,r){for(var i in r)i in t||"duration"===i&&e||"ease"===i||(t[i]=r[i])}):de;if(Tt(t.inherit))for(;r;)i(t,r.vars.defaults),r=r.parent||r._dp;return t},ye=function(t,e,r,i,n){var a,s=t[i];if(n)for(a=e[n];s&&s[n]>a;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e},we=function(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null},be=function(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0},xe=function(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t},Te=function(t,e,r,i){return t._startAt&&(D?t._startAt.revert(jt):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))},ke=function t(e){return!e||e._ts&&t(e.parent)},Me=function(t){return t._repeat?Oe(t._tTime,t=t.duration()+t._rDelay)*t:0},Oe=function(t,e){var r=Math.floor(t=se(t/e));return t&&r===t?r-1:r},Ce=function(t,e){return(t-e._start)*e._ts+(e._ts>=0?0:e._dirty?e.totalDuration():e._tDur)},Se=function(t){return t._end=se(t._start+(t._tDur/Math.abs(t._ts||t._rts||pt)||0))},Ae=function(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=se(r._time-(t._ts>0?e/t._ts:((t._dirty?t.totalDuration():t._tDur)-e)/-t._ts)),Se(t),r._dirty||xe(r,t)),t},Fe=function(t,e){var r;if((e._time||!e._dur&&e._initted||e._start<t._time&&(e._dur||!e.add))&&(r=Ce(t.rawTime(),e),(!e._dur||Ne(0,e.totalDuration(),r)-e._tTime>pt)&&e.render(r,!0)),xe(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur<t.duration())for(r=t;r._dp;)r.rawTime()>=0&&r.totalTime(r._tTime),r=r._dp;t._zTime=-1e-8}},De=function(t,e,r,i){return e.parent&&be(e),e._start=se((wt(r)?r:r||t!==E?Be(t,r,e):t._time)+e._delay),e._end=se(e._start+(e.totalDuration()/Math.abs(e.timeScale())||0)),ye(t,e,"_first","_last",t._sort?"_start":0),ze(e)||(t._recent=e),i||Fe(t,e),t._ts<0&&Ae(t,t._tTime),t},Re=function(t,e){return qt.ScrollTrigger?qt.ScrollTrigger.create(e,t):void 0},Ee=function(t,e,r,i,n){return Lr(t,e,n),t._initted?!r&&t._pt&&!D&&(t._dur&&!1!==t.vars.lazy||!t._dLine truncated
*/var F,D,R,E,P,z,L,q,X,B,Y,I,N,U,V,j,G,H,$,Q,W,K,Z,J,tt,et,rt,it,nt,at,st,ot,ut={autoSleep:120,force3D:"auto",nullTargetWarn:1,units:{lineHeight:""}},lt={duration:.5,overwrite:!1,delay:0},ht=1e8,ct=1e-8,pt=2*Math.PI,dt=pt/4,ft=0,_t=Math.sqrt,mt=Math.cos,vt=Math.sin,gt=function(t){return"string"==typeof t},yt=function(t){return"function"==typeof t},wt=function(t){return"number"==typeof t},xt=function(t){return void 0===t},bt=function(t){return"object"==typeof t},Tt=function(t){return!1!==t},kt=function(){return"undefined"!=typeof window},Mt=function(t){return yt(t)||gt(t)},St="function"==typeof ArrayBuffer&&ArrayBuffer.isView||function(){},Ot=Array.isArray,Ct=/random\([^)]+\)/g,At=/,\s*/g,Ft=/(?:-?\.?\d|\.)+/gi,Dt=/[-+=.]*\d+[.e\-+]*\d*[e\-+]*\d*/g,Rt=/[-+=.]*\d+[.e-]*\d*[a-z%]*/g,Et=/[-+=.]*\d+\.?\d*(?:e-|e\+)?\d*/gi,Pt=/[+-]=-?[.\d]+/,zt=/[^,'"\[\]\s]+/gi,Lt=/^[+\-=e\s\d]*\d+[.\d]*([a-z]*|%)\s*$/i,qt={},Xt={},Bt=function(t){return(Xt=_e(t,qt))&&fi},Yt=function(t,e){},It=function(t,e){return!e&&void 0},Nt=function(t,e){return t&&(qt[t]=e)&&Xt&&(Xt[t]=e)||qt},Ut=function(){return 0},Vt={suppressEvents:!0,isStart:!0,kill:!1},jt={suppressEvents:!0,kill:!1},Gt={suppressEvents:!0},Ht={},$t=[],Qt={},Wt={},Kt={},Zt=30,Jt=[],te="",ee=function(t){var e,r,i=t[0];if(bt(i)||yt(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=Jt.length;r--&&!Jt[r].targetTest(i););e=Jt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Cr(t[r],e)))||t.splice(r,1);return t},re=function(t){return t._gsap||ee(Ge(t))[0]._gsap},ie=function(t,e,r){return(r=t[e])&&yt(r)?t[e]():xt(r)&&t.getAttribute&&t.getAttribute(e)||r},ne=function(t,e){return(t=t.split(",")).forEach(e)||t},ae=function(t){return Math.round(1e5*t)/1e5||0},se=function(t){return Math.round(1e7*t)/1e7||0},oe=function(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i},ue=function(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++i<r;);return i<r},le=function(){var t,e,r=$t.length,i=$t.slice(0);for(Qt={},$t.length=0,t=0;t<r;t++)(e=i[t])&&e._lazy&&(e.render(e._lazy[0],e._lazy[1],!0)._lazy=0)},he=function(t){return!!(t._initted||t._startAt||t.add)},ce=function(t,e,r,i){$t.length&&!D&&le(),t.render(e,r,!!(D&&e<0&&he(t))),$t.length&&!D&&le()},pe=function(t){var e=parseFloat(t);return(e||0===e)&&(t+"").match(zt).length<2?e:gt(t)?t.trim():t},de=function(t){return t},fe=function(t,e){for(var r in e)r in t||(t[r]=e[r]);return t},_e=function(t,e){for(var r in e)t[r]=e[r];return t},me=function t(e,r){for(var i in r)"__proto__"!==i&&"constructor"!==i&&"prototype"!==i&&(e[i]=bt(r[i])?t(e[i]||(e[i]={}),r[i]):r[i]);return e},ve=function(t,e){var r,i={};for(r in t)r in e||(i[r]=t[r]);return i},ge=function(t){var e,r=t.parent||E,i=t.keyframes?(e=Ot(t.keyframes),function(t,r){for(var i in r)i in t||"duration"===i&&e||"ease"===i||(t[i]=r[i])}):fe;if(Tt(t.inherit))for(;r;)i(t,r.vars.defaults),r=r.parent||r._dp;return t},ye=function(t,e,r,i,n){var a,s=t[i];if(n)for(a=e[n];s&&s[n]>a;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e},we=function(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null},xe=function(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0},be=function(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t},Te=function(t,e,r,i){return t._startAt&&(D?t._startAt.revert(jt):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))},ke=function t(e){return!e||e._ts&&t(e.parent)},Me=function(t){return t._repeat?Se(t._tTime,t=t.duration()+t._rDelay)*t:0},Se=function(t,e){var r=Math.floor(t=se(t/e));return t&&r===t?r-1:r},Oe=function(t,e){return(t-e._start)*e._ts+(e._ts>=0?0:e._dirty?e.totalDuration():e._tDur)},Ce=function(t){return t._end=se(t._start+(t._tDur/Math.abs(t._ts||t._rts||ct)||0))},Ae=function(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=se(r._time-(t._ts>0?e/t._ts:((t._dirty?t.totalDuration():t._tDur)-e)/-t._ts)),Ce(t),r._dirty||be(r,t)),t},Fe=function(t,e){var r;if((e._time||!e._dur&&e._initted||e._start<t._time&&(e._dur||!e.add))&&(r=Oe(t.rawTime(),e),(!e._dur||Ne(0,e.totalDuration(),r)-e._tTime>ct)&&e.render(r,!0)),be(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur<t.duration())for(r=t;r._dp;)r.rawTime()>=0&&r.totalTime(r._tTime),r=r._dp;t._zTime=-1e-8}},De=function(t,e,r,i){return e.parent&&xe(e),e._start=se((wt(r)?r:r||t!==E?Be(t,r,e):t._time)+e._delay),e._end=se(e._start+(e.totalDuration()/Math.abs(e.timeScale())||0)),ye(t,e,"_first","_last",t._sort?"_start":0),ze(e)||(t._recent=e),i||Fe(t,e),t._ts<0&&Ae(t,t._tTime),t},Re=function(t,e){return qt.ScrollTrigger?qt.ScrollTrigger.create(e,t):void 0},Ee=function(t,e,r,i,n){return Lr(t,e,n),t._initted?!r&&t._pt&&!D&&(t._dur&&!1!==t.vars.lazy||!t._dLine truncated
/*!
* CSSPlugin 3.15.0
* https://gsap.com
@@ -15,4 +15,4 @@ import{r as t,c as e,f as r,Q as i,w as n,v as a,x as s,A as o,R as u,ag as l,L
* Subject to the terms at https://gsap.com/standard-license
* @author: Jack Doyle, jack@greensock.com
*/
var _i,mi,vi,gi,yi,wi,bi,xi,Ti={},ki=180/Math.PI,Mi=Math.PI/180,Oi=Math.atan2,Ci=/([A-Z])/g,Si=/(left|right|width|margin|padding|x)/i,Ai=/[\s,\(]\S/,Fi={autoAlpha:"opacity,visibility",scale:"scaleX,scaleY",alpha:"opacity"},Di=function(t,e){return e.set(e.t,e.p,Math.round(1e4*(e.s+e.c*t))/1e4+e.u,e)},Ri=function(t,e){return e.set(e.t,e.p,1===t?e.e:Math.round(1e4*(e.s+e.c*t))/1e4+e.u,e)},Ei=function(t,e){return e.set(e.t,e.p,t?Math.round(1e4*(e.s+e.c*t))/1e4+e.u:e.b,e)},Pi=function(t,e){return e.set(e.t,e.p,1===t?e.e:t?Math.round(1e4*(e.s+e.c*t))/1e4+e.u:e.b,e)},zi=function(t,e){var r=e.s+e.c*t;e.set(e.t,e.p,~~(r+(r<0?-.5:.5))+e.u,e)},Li=function(t,e){return e.set(e.t,e.p,t?e.e:e.b,e)},qi=function(t,e){return e.set(e.t,e.p,1!==t?e.b:e.e,e)},Xi=function(t,e,r){return t.style[e]=r},Bi=function(t,e,r){return t.style.setProperty(e,r)},Yi=function(t,e,r){return t._gsap[e]=r},Ii=function(t,e,r){return t._gsap.scaleX=t._gsap.scaleY=r},Ni=function(t,e,r,i,n){var a=t._gsap;a.scaleX=a.scaleY=r,a.renderTransform(n,a)},Ui=function(t,e,r,i,n){var a=t._gsap;a[e]=r,a.renderTransform(n,a)},Vi="transform",ji=Vi+"Origin",Gi=function t(e,r){var i=this,n=this.target,a=n.style,s=n._gsap;if(e in Ti&&a){if(this.tfm=this.tfm||{},"transform"===e)return Fi.transform.split(",").forEach(function(e){return t.call(i,e,r)});if(~(e=Fi[e]||e).indexOf(",")?e.split(",").forEach(function(t){return i.tfm[t]=pn(n,t)}):this.tfm[e]=s.x?s[e]:pn(n,e),e===ji&&(this.tfm.zOrigin=s.zOrigin),this.props.indexOf(Vi)>=0)return;s.svg&&(this.svgo=n.getAttribute("data-svg-origin"),this.props.push(ji,r,"")),e=Vi}(a||r)&&this.props.push(e,r,a[e])},Hi=function(t){t.translate&&(t.removeProperty("translate"),t.removeProperty("scale"),t.removeProperty("rotate"))},Qi=function(){var t,e,r=this.props,i=this.target,n=i.style,a=i._gsap;for(t=0;t<r.length;t+=3)r[t+1]?2===r[t+1]?i[r[t]](r[t+2]):i[r[t]]=r[t+2]:r[t+2]?n[r[t]]=r[t+2]:n.removeProperty("--"===r[t].substr(0,2)?r[t]:r[t].replace(Ci,"-$1").toLowerCase());if(this.tfm){for(e in this.tfm)a[e]=this.tfm[e];a.svg&&(a.renderTransform(),i.setAttribute("data-svg-origin",this.svgo||"")),(t=bi())&&t.isStart||n[Vi]||(Hi(n),a.zOrigin&&n[ji]&&(n[ji]+=" "+a.zOrigin+"px",a.zOrigin=0,a.renderTransform()),a.uncache=1)}},Wi=function(t,e){var r={target:t,props:[],revert:Qi,save:Gi};return t._gsap||di.core.getCache(t),e&&t.style&&t.nodeType&&e.split(",").forEach(function(t){return r.save(t)}),r},Zi=function(t,e){var r=mi.createElementNS?mi.createElementNS((e||"http://www.w3.org/1999/xhtml").replace(/^https/,"http"),t):mi.createElement(t);return r&&r.style?r:mi.createElement(t)},$i=function t(e,r,i){var n=getComputedStyle(e);return n[r]||n.getPropertyValue(r.replace(Ci,"-$1").toLowerCase())||n.getPropertyValue(r)||!i&&t(e,Ji(r)||r,1)||""},Ki="O,Moz,ms,Ms,Webkit".split(","),Ji=function(t,e,r){var i=(e||yi).style,n=5;if(t in i&&!r)return t;for(t=t.charAt(0).toUpperCase()+t.substr(1);n--&&!(Ki[n]+t in i););return n<0?null:(3===n?"ms":n>=0?Ki[n]:"")+t},tn=function(){"undefined"!=typeof window&&window.document&&(_i=window,mi=_i.document,vi=mi.documentElement,yi=Zi("div")||{style:{}},Zi("div"),Vi=Ji(Vi),ji=Vi+"Origin",yi.style.cssText="border-width:0;line-height:0;position:absolute;padding:0",xi=!!Ji("perspective"),bi=di.core.reverting,gi=1)},en=function(t){var e,r=t.ownerSVGElement,i=Zi("svg",r&&r.getAttribute("xmlns")||"http://www.w3.org/2000/svg"),n=t.cloneNode(!0);n.style.display="block",i.appendChild(n),vi.appendChild(i);try{e=n.getBBox()}catch(a){}return i.removeChild(n),vi.removeChild(i),e},rn=function(t,e){for(var r=e.length;r--;)if(t.hasAttribute(e[r]))return t.getAttribute(e[r])},nn=function(t){var e,r;try{e=t.getBBox()}catch(i){e=en(t),r=1}return e&&(e.width||e.height)||r||(e=en(t)),!e||e.width||e.x||e.y?e:{x:+rn(t,["x","cx","x1"])||0,y:+rn(t,["y","cy","y1"])||0,width:0,height:0}},an=function(t){return!(!t.getCTM||t.parentNode&&!t.ownerSVGElement||!nn(t))},sn=function(t,e){if(e){var r,i=t.style;e in Ti&&e!==ji&&(e=Vi),i.removeProperty?("ms"!==(r=e.substr(0,2))&&"webkit"!==e.substr(0,6)||(e="-"+e),i.removeProperty("--"===r?e:e.replace(Ci,"-$1").toLowerCase())):i.removeAttribute(e)}},on=function(t,e,r,i,n,a){var s=new ei(t._pt,e,r,0,1,a?qi:Li);return t._pt=s,s.b=i,s.e=n,t._props.push(r),s},un={deg:1,rad:1,turn:1},ln={grid:1,flex:1},hn=function t(e,r,i,n){var a,s,o,u,l=parseFloat(i)||0,h=(i+"").trim().substr((l+"").length)||"px",p=yi.style,c=Si.test(r),f="svg"===e.tagName.toLowerCase(),d=(f?"client":"offset")+(c?"Width":"Height"),_=100,m="px"===n,v="%"===n;if(n===h||!l||un[n]||un[h])return l;if("px"!==h&&!m&&(l=t(e,r,i,"px")),u=e.getCTM&&an(e),(v||"%"===h)&&(Ti[r]||~r.indexOf("adius")))return a=u?e.getBBox()[c?"width":"height"]:e[d],ae(v?l/a*_:l/100*a);if(p[c?"width":"height"]=_+(m?h:n),s="rem"!==n&&~r.indexOf("adius")||"em"===n&&e.appendChild&&!f?e:e.parentNode,u&&(s=(e.ownerSVGElement||{}).parentNode),s&&s!==mi&&s.appendChild||(s=mi.body),(o=s._gsap)&&v&&o.width&&c&&o.time===mr.time&&!o.uncache)return ae(l/o.width*_);if(!v||"heightLine truncated
var _i,mi,vi,gi,yi,wi,xi,bi,Ti={},ki=180/Math.PI,Mi=Math.PI/180,Si=Math.atan2,Oi=/([A-Z])/g,Ci=/(left|right|width|margin|padding|x)/i,Ai=/[\s,\(]\S/,Fi={autoAlpha:"opacity,visibility",scale:"scaleX,scaleY",alpha:"opacity"},Di=function(t,e){return e.set(e.t,e.p,Math.round(1e4*(e.s+e.c*t))/1e4+e.u,e)},Ri=function(t,e){return e.set(e.t,e.p,1===t?e.e:Math.round(1e4*(e.s+e.c*t))/1e4+e.u,e)},Ei=function(t,e){return e.set(e.t,e.p,t?Math.round(1e4*(e.s+e.c*t))/1e4+e.u:e.b,e)},Pi=function(t,e){return e.set(e.t,e.p,1===t?e.e:t?Math.round(1e4*(e.s+e.c*t))/1e4+e.u:e.b,e)},zi=function(t,e){var r=e.s+e.c*t;e.set(e.t,e.p,~~(r+(r<0?-.5:.5))+e.u,e)},Li=function(t,e){return e.set(e.t,e.p,t?e.e:e.b,e)},qi=function(t,e){return e.set(e.t,e.p,1!==t?e.b:e.e,e)},Xi=function(t,e,r){return t.style[e]=r},Bi=function(t,e,r){return t.style.setProperty(e,r)},Yi=function(t,e,r){return t._gsap[e]=r},Ii=function(t,e,r){return t._gsap.scaleX=t._gsap.scaleY=r},Ni=function(t,e,r,i,n){var a=t._gsap;a.scaleX=a.scaleY=r,a.renderTransform(n,a)},Ui=function(t,e,r,i,n){var a=t._gsap;a[e]=r,a.renderTransform(n,a)},Vi="transform",ji=Vi+"Origin",Gi=function t(e,r){var i=this,n=this.target,a=n.style,s=n._gsap;if(e in Ti&&a){if(this.tfm=this.tfm||{},"transform"===e)return Fi.transform.split(",").forEach(function(e){return t.call(i,e,r)});if(~(e=Fi[e]||e).indexOf(",")?e.split(",").forEach(function(t){return i.tfm[t]=cn(n,t)}):this.tfm[e]=s.x?s[e]:cn(n,e),e===ji&&(this.tfm.zOrigin=s.zOrigin),this.props.indexOf(Vi)>=0)return;s.svg&&(this.svgo=n.getAttribute("data-svg-origin"),this.props.push(ji,r,"")),e=Vi}(a||r)&&this.props.push(e,r,a[e])},Hi=function(t){t.translate&&(t.removeProperty("translate"),t.removeProperty("scale"),t.removeProperty("rotate"))},$i=function(){var t,e,r=this.props,i=this.target,n=i.style,a=i._gsap;for(t=0;t<r.length;t+=3)r[t+1]?2===r[t+1]?i[r[t]](r[t+2]):i[r[t]]=r[t+2]:r[t+2]?n[r[t]]=r[t+2]:n.removeProperty("--"===r[t].substr(0,2)?r[t]:r[t].replace(Oi,"-$1").toLowerCase());if(this.tfm){for(e in this.tfm)a[e]=this.tfm[e];a.svg&&(a.renderTransform(),i.setAttribute("data-svg-origin",this.svgo||"")),(t=xi())&&t.isStart||n[Vi]||(Hi(n),a.zOrigin&&n[ji]&&(n[ji]+=" "+a.zOrigin+"px",a.zOrigin=0,a.renderTransform()),a.uncache=1)}},Qi=function(t,e){var r={target:t,props:[],revert:$i,save:Gi};return t._gsap||fi.core.getCache(t),e&&t.style&&t.nodeType&&e.split(",").forEach(function(t){return r.save(t)}),r},Wi=function(t,e){var r=mi.createElementNS?mi.createElementNS((e||"http://www.w3.org/1999/xhtml").replace(/^https/,"http"),t):mi.createElement(t);return r&&r.style?r:mi.createElement(t)},Ki=function t(e,r,i){var n=getComputedStyle(e);return n[r]||n.getPropertyValue(r.replace(Oi,"-$1").toLowerCase())||n.getPropertyValue(r)||!i&&t(e,Ji(r)||r,1)||""},Zi="O,Moz,ms,Ms,Webkit".split(","),Ji=function(t,e,r){var i=(e||yi).style,n=5;if(t in i&&!r)return t;for(t=t.charAt(0).toUpperCase()+t.substr(1);n--&&!(Zi[n]+t in i););return n<0?null:(3===n?"ms":n>=0?Zi[n]:"")+t},tn=function(){"undefined"!=typeof window&&window.document&&(_i=window,mi=_i.document,vi=mi.documentElement,yi=Wi("div")||{style:{}},Wi("div"),Vi=Ji(Vi),ji=Vi+"Origin",yi.style.cssText="border-width:0;line-height:0;position:absolute;padding:0",bi=!!Ji("perspective"),xi=fi.core.reverting,gi=1)},en=function(t){var e,r=t.ownerSVGElement,i=Wi("svg",r&&r.getAttribute("xmlns")||"http://www.w3.org/2000/svg"),n=t.cloneNode(!0);n.style.display="block",i.appendChild(n),vi.appendChild(i);try{e=n.getBBox()}catch(a){}return i.removeChild(n),vi.removeChild(i),e},rn=function(t,e){for(var r=e.length;r--;)if(t.hasAttribute(e[r]))return t.getAttribute(e[r])},nn=function(t){var e,r;try{e=t.getBBox()}catch(i){e=en(t),r=1}return e&&(e.width||e.height)||r||(e=en(t)),!e||e.width||e.x||e.y?e:{x:+rn(t,["x","cx","x1"])||0,y:+rn(t,["y","cy","y1"])||0,width:0,height:0}},an=function(t){return!(!t.getCTM||t.parentNode&&!t.ownerSVGElement||!nn(t))},sn=function(t,e){if(e){var r,i=t.style;e in Ti&&e!==ji&&(e=Vi),i.removeProperty?("ms"!==(r=e.substr(0,2))&&"webkit"!==e.substr(0,6)||(e="-"+e),i.removeProperty("--"===r?e:e.replace(Oi,"-$1").toLowerCase())):i.removeAttribute(e)}},on=function(t,e,r,i,n,a){var s=new ei(t._pt,e,r,0,1,a?qi:Li);return t._pt=s,s.b=i,s.e=n,t._props.push(r),s},un={deg:1,rad:1,turn:1},ln={grid:1,flex:1},hn=function t(e,r,i,n){var a,s,o,u,l=parseFloat(i)||0,h=(i+"").trim().substr((l+"").length)||"px",c=yi.style,p=Ci.test(r),d="svg"===e.tagName.toLowerCase(),f=(d?"client":"offset")+(p?"Width":"Height"),_=100,m="px"===n,v="%"===n;if(n===h||!l||un[n]||un[h])return l;if("px"!==h&&!m&&(l=t(e,r,i,"px")),u=e.getCTM&&an(e),(v||"%"===h)&&(Ti[r]||~r.indexOf("adius")))return a=u?e.getBBox()[p?"width":"height"]:e[f],ae(v?l/a*_:l/100*a);if(c[p?"width":"height"]=_+(m?h:n),s="rem"!==n&&~r.indexOf("adius")||"em"===n&&e.appendChild&&!d?e:e.parentNode,u&&(s=(e.ownerSVGElement||{}).parentNode),s&&s!==mi&&s.appendChild||(s=mi.body),(o=s._gsap)&&v&&o.width&&p&&o.time===mr.time&&!o.uncache)return ae(l/o.width*_);if(!v||"heightLine truncated
@@ -1,2 +1,2 @@
import{g as e,i as t,r as n,c as r,u as o,a,w as l,s as i,b as s,d as u,e as c,o as d,f,n as p,h as v,j as h,k as m,l as b,m as g,p as y,q as w,N as x,t as S,v as k,x as E,y as _,z as C,A as j,B as O,C as A,D as F,E as I,F as M,G as L,H as B,I as P,J as T,K as z,L as N,M as $,O as R,P as V,Q as q,T as D,R as H,S as U,U as W,V as K,W as Y,X as G,Y as J,Z as Q,_ as X,$ as Z,a0 as ee,a1 as te,a2 as ne,a3 as re,a4 as oe,a5 as ae,a6 as le}from"./vendor-6oHsDC_h.js";const ie=Symbol(),se="el",ue=(e,t,n,r,o)=>{let a=`${e}-${t}`;return n&&(a+=`-${n}`),r&&(a+=`__${r}`),o&&(a+=`--${o}`),a},ce=Symbol("namespaceContextKey"),de=a=>{const l=a||(e()?t(ce,n(se)):n(se));return r(()=>o(l)||se)},fe=(e,t)=>{const n=de(t);return{namespace:n,b:(t="")=>ue(n.value,e,t,"",""),e:t=>t?ue(n.value,e,"",t,""):"",m:t=>t?ue(n.value,e,"","",t):"",be:(t,r)=>t&&r?ue(n.value,e,t,r,""):"",em:(t,r)=>t&&r?ue(n.value,e,"",t,r):"",bm:(t,r)=>t&&r?ue(n.value,e,t,"",r):"",bem:(t,r,o)=>t&&r&&o?ue(n.value,e,t,r,o):"",is:(e,...t)=>{const n=!(t.length>=1)||t[0];return e&&n?`is-${e}`:""},cssVar:e=>{const t={};for(const r in e)e[r]&&(t[`--${n.value}-${r}`]=e[r]);return t},cssVarName:e=>`--${n.value}-${e}`,cssVarBlock:t=>{const r={};for(const o in t)t[o]&&(r[`--${n.value}-${e}-${o}`]=t[o]);return r},cssVarBlockName:t=>`--${n.value}-${e}-${t}`}};var pe="object"==typeof global&&global&&global.Object===Object&&global,ve="object"==typeof self&&self&&self.Object===Object&&self,he=pe||ve||Function("return this")(),me=he.Symbol,be=Object.prototype,ge=be.hasOwnProperty,ye=be.toString,we=me?me.toStringTag:void 0;var xe=Object.prototype.toString;var Se=me?me.toStringTag:void 0;function ke(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":Se&&Se in Object(e)?function(e){var t=ge.call(e,we),n=e[we];try{e[we]=void 0;var r=!0}catch(a){}var o=ye.call(e);return r&&(t?e[we]=n:delete e[we]),o}(e):function(e){return xe.call(e)}(e)}function Ee(e){return null!=e&&"object"==typeof e}function _e(e){return"symbol"==typeof e||Ee(e)&&"[object Symbol]"==ke(e)}var Ce=Array.isArray,je=me?me.prototype:void 0,Oe=je?je.toString:void 0;function Ae(e){if("string"==typeof e)return e;if(Ce(e))return function(e,t){for(var n=-1,r=null==e?0:e.length,o=Array(r);++n<r;)o[n]=t(e[n],n,e);return o}(e,Ae)+"";if(_e(e))return Oe?Oe.call(e):"";var t=e+"";return"0"==t&&1/e==-1/0?"-0":t}function Fe(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}function Ie(e){if(!Fe(e))return!1;var t=ke(e);return"[object Function]"==t||"[object GeneratorFunction]"==t||"[object AsyncFunction]"==t||"[object Proxy]"==t}var Me,Le=he["__core-js_shared__"],Be=(Me=/[^.]+$/.exec(Le&&Le.keys&&Le.keys.IE_PROTO||""))?"Symbol(src)_1."+Me:"";var Pe=Function.prototype.toString;function Te(e){if(null!=e){try{return Pe.call(e)}catch(t){}try{return e+""}catch(t){}}return""}var ze=/^\[object .+?Constructor\]$/,Ne=Function.prototype,$e=Object.prototype,Re=Ne.toString,Ve=$e.hasOwnProperty,qe=RegExp("^"+Re.call(Ve).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function De(e){return!(!Fe(e)||(t=e,Be&&Be in t))&&(Ie(e)?qe:ze).test(Te(e));var t}function He(e,t){var n=function(e,t){return null==e?void 0:e[t]}(e,t);return De(n)?n:void 0}var Ue=He(he,"WeakMap"),We=Object.create,Ke=function(){function e(){}return function(t){if(!Fe(t))return{};if(We)return We(t);e.prototype=t;var n=new e;return e.prototype=void 0,n}}();var Ye=Date.now;var Ge,Je,Qe,Xe=function(){try{var e=He(Object,"defineProperty");return e({},"",{}),e}catch(t){}}(),Ze=Xe?function(e,t){return Xe(e,"toString",{configurable:!0,enumerable:!1,value:(n=t,function(){return n}),writable:!0});var n}:function(e){return e},et=(Ge=Ze,Je=0,Qe=0,function(){var e=Ye(),t=16-(e-Qe);if(Qe=e,t>0){if(++Je>=800)return arguments[0]}else Je=0;return Ge.apply(void 0,arguments)});var tt=/^(?:0|[1-9]\d*)$/;function nt(e,t){var n=typeof e;return!!(t=null==t?9007199254740991:t)&&("number"==n||"symbol"!=n&&tt.test(e))&&e>-1&&e%1==0&&e<t}function rt(e,t,n){"__proto__"==t&&Xe?Xe(e,t,{configurable:!0,enumerable:!0,value:n,writable:!0}):e[t]=n}function ot(e,t){return e===t||e!=e&&t!=t}var at=Object.prototype.hasOwnProperty;function lt(e,t,n){var r=e[t];at.call(e,t)&&ot(r,n)&&(void 0!==n||t in e)||rt(e,t,n)}function it(e,t,n,r){var o=!n;n||(n={});for(var a=-1,l=t.length;++a<l;){var i=t[a],s=void 0;void 0===s&&(s=e[i]),o?rt(n,i,s):lt(n,i,s)}return n}var st=Math.max;function ut(e){return"number"==typeof e&&e>-1&&e%1==0&&e<=9007199254740991}function ct(e){return null!=e&&ut(e.length)&&!Ie(e)}var dt=Object.prototype;function ft(e){var t=e&&e.constructor;return e===("function"==typeof t&&t.prototype||dt)}function pt(e){return Ee(e)&&"[object Arguments]"==ke(e)}var vt=Object.prototype,ht=vt.hasOwnProperty,mt=vt.propertyIsEnumerable,bt=pt(function(){return arguments}())?pt:function(e){return Ee(e)&&ht.call(e,"callee")&&!mt.call(e,"callee")};var gt="object"==typeof exports&&exports&&!exports.nodeType&&exports,yt=gt&&"objecLine truncated
import{g as e,i as t,r as n,c as r,u as o,a,w as l,s as i,b as s,d as u,e as c,o as d,f,n as p,h as v,j as h,k as m,l as b,m as g,p as y,q as w,N as x,t as S,v as k,x as E,y as _,z as C,A as j,B as O,C as A,D as F,E as I,F as M,G as L,H as B,I as P,J as T,K as z,L as N,M as $,O as R,P as V,Q as q,T as D,R as H,S as U,U as W,V as K,W as Y,X as G,Y as J,Z as Q,_ as X,$ as Z,a0 as ee,a1 as te,a2 as ne,a3 as re,a4 as oe,a5 as ae,a6 as le}from"./vendor-CVFEPmGb.js";const ie=Symbol(),se="el",ue=(e,t,n,r,o)=>{let a=`${e}-${t}`;return n&&(a+=`-${n}`),r&&(a+=`__${r}`),o&&(a+=`--${o}`),a},ce=Symbol("namespaceContextKey"),de=a=>{const l=a||(e()?t(ce,n(se)):n(se));return r(()=>o(l)||se)},fe=(e,t)=>{const n=de(t);return{namespace:n,b:(t="")=>ue(n.value,e,t,"",""),e:t=>t?ue(n.value,e,"",t,""):"",m:t=>t?ue(n.value,e,"","",t):"",be:(t,r)=>t&&r?ue(n.value,e,t,r,""):"",em:(t,r)=>t&&r?ue(n.value,e,"",t,r):"",bm:(t,r)=>t&&r?ue(n.value,e,t,"",r):"",bem:(t,r,o)=>t&&r&&o?ue(n.value,e,t,r,o):"",is:(e,...t)=>{const n=!(t.length>=1)||t[0];return e&&n?`is-${e}`:""},cssVar:e=>{const t={};for(const r in e)e[r]&&(t[`--${n.value}-${r}`]=e[r]);return t},cssVarName:e=>`--${n.value}-${e}`,cssVarBlock:t=>{const r={};for(const o in t)t[o]&&(r[`--${n.value}-${e}-${o}`]=t[o]);return r},cssVarBlockName:t=>`--${n.value}-${e}-${t}`}};var pe="object"==typeof global&&global&&global.Object===Object&&global,ve="object"==typeof self&&self&&self.Object===Object&&self,he=pe||ve||Function("return this")(),me=he.Symbol,be=Object.prototype,ge=be.hasOwnProperty,ye=be.toString,we=me?me.toStringTag:void 0;var xe=Object.prototype.toString;var Se=me?me.toStringTag:void 0;function ke(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":Se&&Se in Object(e)?function(e){var t=ge.call(e,we),n=e[we];try{e[we]=void 0;var r=!0}catch(a){}var o=ye.call(e);return r&&(t?e[we]=n:delete e[we]),o}(e):function(e){return xe.call(e)}(e)}function Ee(e){return null!=e&&"object"==typeof e}function _e(e){return"symbol"==typeof e||Ee(e)&&"[object Symbol]"==ke(e)}var Ce=Array.isArray,je=me?me.prototype:void 0,Oe=je?je.toString:void 0;function Ae(e){if("string"==typeof e)return e;if(Ce(e))return function(e,t){for(var n=-1,r=null==e?0:e.length,o=Array(r);++n<r;)o[n]=t(e[n],n,e);return o}(e,Ae)+"";if(_e(e))return Oe?Oe.call(e):"";var t=e+"";return"0"==t&&1/e==-1/0?"-0":t}function Fe(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}function Ie(e){if(!Fe(e))return!1;var t=ke(e);return"[object Function]"==t||"[object GeneratorFunction]"==t||"[object AsyncFunction]"==t||"[object Proxy]"==t}var Me,Le=he["__core-js_shared__"],Be=(Me=/[^.]+$/.exec(Le&&Le.keys&&Le.keys.IE_PROTO||""))?"Symbol(src)_1."+Me:"";var Pe=Function.prototype.toString;function Te(e){if(null!=e){try{return Pe.call(e)}catch(t){}try{return e+""}catch(t){}}return""}var ze=/^\[object .+?Constructor\]$/,Ne=Function.prototype,$e=Object.prototype,Re=Ne.toString,Ve=$e.hasOwnProperty,qe=RegExp("^"+Re.call(Ve).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function De(e){return!(!Fe(e)||(t=e,Be&&Be in t))&&(Ie(e)?qe:ze).test(Te(e));var t}function He(e,t){var n=function(e,t){return null==e?void 0:e[t]}(e,t);return De(n)?n:void 0}var Ue=He(he,"WeakMap"),We=Object.create,Ke=function(){function e(){}return function(t){if(!Fe(t))return{};if(We)return We(t);e.prototype=t;var n=new e;return e.prototype=void 0,n}}();var Ye=Date.now;var Ge,Je,Qe,Xe=function(){try{var e=He(Object,"defineProperty");return e({},"",{}),e}catch(t){}}(),Ze=Xe?function(e,t){return Xe(e,"toString",{configurable:!0,enumerable:!1,value:(n=t,function(){return n}),writable:!0});var n}:function(e){return e},et=(Ge=Ze,Je=0,Qe=0,function(){var e=Ye(),t=16-(e-Qe);if(Qe=e,t>0){if(++Je>=800)return arguments[0]}else Je=0;return Ge.apply(void 0,arguments)});var tt=/^(?:0|[1-9]\d*)$/;function nt(e,t){var n=typeof e;return!!(t=null==t?9007199254740991:t)&&("number"==n||"symbol"!=n&&tt.test(e))&&e>-1&&e%1==0&&e<t}function rt(e,t,n){"__proto__"==t&&Xe?Xe(e,t,{configurable:!0,enumerable:!0,value:n,writable:!0}):e[t]=n}function ot(e,t){return e===t||e!=e&&t!=t}var at=Object.prototype.hasOwnProperty;function lt(e,t,n){var r=e[t];at.call(e,t)&&ot(r,n)&&(void 0!==n||t in e)||rt(e,t,n)}function it(e,t,n,r){var o=!n;n||(n={});for(var a=-1,l=t.length;++a<l;){var i=t[a],s=void 0;void 0===s&&(s=e[i]),o?rt(n,i,s):lt(n,i,s)}return n}var st=Math.max;function ut(e){return"number"==typeof e&&e>-1&&e%1==0&&e<=9007199254740991}function ct(e){return null!=e&&ut(e.length)&&!Ie(e)}var dt=Object.prototype;function ft(e){var t=e&&e.constructor;return e===("function"==typeof t&&t.prototype||dt)}function pt(e){return Ee(e)&&"[object Arguments]"==ke(e)}var vt=Object.prototype,ht=vt.hasOwnProperty,mt=vt.propertyIsEnumerable,bt=pt(function(){return arguments}())?pt:function(e){return Ee(e)&&ht.call(e,"callee")&&!mt.call(e,"callee")};var gt="object"==typeof exports&&exports&&!exports.nodeType&&exports,yt=gt&&"objecLine truncated
/*! Element Plus Icons Vue v2.3.2 */var qo=S({name:"CircleCheck",__name:"circle-check",setup:e=>(e,t)=>(E(),k("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[j("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),j("path",{fill:"currentColor",d:"M745.344 361.344a32 32 0 0 1 45.312 45.312l-288 288a32 32 0 0 1-45.312 0l-160-160a32 32 0 1 1 45.312-45.312L480 626.752z"})]))}),Do=S({name:"CircleCloseFilled",__name:"circle-close-filled",setup:e=>(e,t)=>(E(),k("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[j("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 393.664L407.936 353.6a38.4 38.4 0 1 0-54.336 54.336L457.664 512 353.6 616.064a38.4 38.4 0 1 0 54.336 54.336L512 566.336 616.064 670.4a38.4 38.4 0 1 0 54.336-54.336L566.336 512 670.4 407.936a38.4 38.4 0 1 0-54.336-54.336z"})]))}),Ho=S({name:"CircleClose",__name:"circle-close",setup:e=>(e,t)=>(E(),k("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[j("path",{fill:"currentColor",d:"m466.752 512-90.496-90.496a32 32 0 0 1 45.248-45.248L512 466.752l90.496-90.496a32 32 0 1 1 45.248 45.248L557.248 512l90.496 90.496a32 32 0 1 1-45.248 45.248L512 557.248l-90.496 90.496a32 32 0 0 1-45.248-45.248z"}),j("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"})]))}),Uo=S({name:"Close",__name:"close",setup:e=>(e,t)=>(E(),k("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[j("path",{fill:"currentColor",d:"M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"})]))}),Wo=S({name:"Hide",__name:"hide",setup:e=>(e,t)=>(E(),k("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[j("path",{fill:"currentColor",d:"M876.8 156.8c0-9.6-3.2-16-9.6-22.4s-12.8-9.6-22.4-9.6-16 3.2-22.4 9.6L736 220.8c-64-32-137.6-51.2-224-60.8-160 16-288 73.6-377.6 176S0 496 0 512s48 73.6 134.4 176c22.4 25.6 44.8 48 73.6 67.2l-86.4 89.6c-6.4 6.4-9.6 12.8-9.6 22.4s3.2 16 9.6 22.4 12.8 9.6 22.4 9.6 16-3.2 22.4-9.6l704-710.4c3.2-6.4 6.4-12.8 6.4-22.4m-646.4 528Q115.2 579.2 76.8 512q43.2-72 153.6-172.8C304 272 400 230.4 512 224c64 3.2 124.8 19.2 176 44.8l-54.4 54.4C598.4 300.8 560 288 512 288c-64 0-115.2 22.4-160 64s-64 96-64 160c0 48 12.8 89.6 35.2 124.8L256 707.2c-9.6-6.4-19.2-16-25.6-22.4m140.8-96Q352 555.2 352 512c0-44.8 16-83.2 48-112s67.2-48 112-48c28.8 0 54.4 6.4 73.6 19.2zM889.599 336c-12.8-16-28.8-28.8-41.6-41.6l-48 48c73.6 67.2 124.8 124.8 150.4 169.6q-43.2 72-153.6 172.8c-73.6 67.2-172.8 108.8-284.8 115.2-51.2-3.2-99.2-12.8-140.8-28.8l-48 48c57.6 22.4 118.4 38.4 188.8 44.8 160-16 288-73.6 377.6-176S1024 528 1024 512s-48.001-73.6-134.401-176"}),j("path",{fill:"currentColor",d:"M511.998 672c-12.8 0-25.6-3.2-38.4-6.4l-51.2 51.2c28.8 12.8 57.6 19.2 89.6 19.2 64 0 115.2-22.4 160-64 41.6-41.6 64-96 64-160 0-32-6.4-64-19.2-89.6l-51.2 51.2c3.2 12.8 6.4 25.6 6.4 38.4 0 44.8-16 83.2-48 112s-67.2 48-112 48"})]))}),Ko=S({name:"InfoFilled",__name:"info-filled",setup:e=>(e,t)=>(E(),k("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[j("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896.064A448 448 0 0 1 512 64m67.2 275.072c33.28 0 60.288-23.104 60.288-57.344s-27.072-57.344-60.288-57.344c-33.28 0-60.16 23.104-60.16 57.344s26.88 57.344 60.16 57.344M590.912 699.2c0-6.848 2.368-24.64 1.024-34.752l-52.608 60.544c-10.88 11.456-24.512 19.392-30.912 17.28a12.99 12.99 0 0 1-8.256-14.72l87.68-276.992c7.168-35.136-12.544-67.2-54.336-71.296-44.096 0-108.992 44.736-148.48 101.504 0 6.784-1.28 23.68.064 33.792l52.544-60.608c10.88-11.328 23.552-19.328 29.952-17.152a12.8 12.8 0 0 1 7.808 16.128L388.48 728.576c-10.048 32.256 8.96 63.872 55.04 71.04 67.84 0 107.904-43.648 147.456-100.416z"})]))}),Yo=S({name:"Loading",__name:"loading",setup:e=>(e,t)=>(E(),k("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[j("path",{fill:"currentColor",d:"M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32m0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32m448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32m-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32M195.2 195.2a32 32 0 0 1 45.248 0L376.32 331.008a32 32 0 0 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248m452.544 452.544a32 32 0 0 1 45.248 0L828.8 783.552a32 32 0 0 1-45.248 45.248L647.744 692.992a32 32 0 0 1 0-45.248M828.8 195.264a32 32 0 0 1 0 45.184L692.992 376.32a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0m-452.544 452.48a32 32 0 0 1 0 45.248L240.448 828.8a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0"})]))}),Go=S({name:"SuccessFilled",__name:"success-filled",setup:e=>(e,t)=>(E(),k(Line truncated
+2
View File
@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/Index-9fEPfBBv.js","assets/logo-Bw7MxNqG.js","assets/vendor-CVFEPmGb.js","assets/DriverIcon-DCBDFADZ.js","assets/DriverIcon-Cx1JoYDo.css","assets/Index-B7imuiqo.css","assets/Login-CLBgxkvB.js","assets/elementPlus-D4GhVLmS.js","assets/Login-BwvsASuf.css","assets/Admin-DjVSFtZx.js","assets/Admin-CuAO_qRG.css"])))=>i.map(i=>d[i]);
import{R as e,a4 as t,w as n,f as o,C as r,c as i,I as a,L as s,J as l,a3 as c,a7 as d,n as h,x as u,v as f,F as p,u as m,A as v,M as y,H as g,O as b,G as w,a8 as E,V as F,a1 as M,a9 as z,aa as x,W as B,K as C,z as A,X as k,a0 as O,ab as S,ac as R,ad as L,ae as T}from"./vendor-CVFEPmGb.js";!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))t(e);new MutationObserver(e=>{for(const n of e)if("childList"===n.type)for(const e of n.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&t(e)}).observe(document,{childList:!0,subtree:!0})}function t(e){if(e.ep)return;e.ep=!0;const t=function(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?t.credentials="include":"anonymous"===e.crossOrigin?t.credentials="omit":t.credentials="same-origin",t}(e);fetch(e.href,t)}}();const H=e({isVisible:!1,title:"",content:"",type:"default",size:"medium",closable:!0,maskClosable:!1,loading:!1,data:null,resolve:null,reject:null}),_=e({fields:[],values:{},errors:{}});let D=null;const j=()=>{D&&(clearTimeout(D),D=null)};function V(){const e=e=>new Promise((t,n)=>{j(),Object.assign(H,{...e,isVisible:!0,resolve:t,reject:n})}),n=()=>{j(),D=setTimeout(()=>{Object.assign(H,{title:"",content:"",type:"default",size:"medium",modalClass:"",bodyClass:"",closable:!0,maskClosable:!1,loading:!1,data:null,resolve:null,reject:null,confirmClass:null,confirmText:null,cancelText:null,icon:null,hideCancelButton:!1,checkboxLabel:null,checkboxChecked:!1,hideFooter:!1}),Object.assign(_,{fields:[],values:{},errors:{}}),D=null},300)};return{modalState:H,formState:_,showModal:e,closeModal:()=>{H.isVisible=!1,H.reject&&H.reject(new Error("Modal closed")),n()},confirmModal:e=>{H.isVisible=!1,H.resolve&&H.resolve(e),n()},confirm:t=>e({type:"confirm",size:"medium",hideCancelButton:(null==t?void 0:t.hideCancelButton)??!0,...t}),form:t=>(_.fields=t.fields||[],_.values={},_.errors={},_.fields.forEach(e=>{_.values[e.name]=e.defaultValue||""}),e({type:"form",size:t.size||"medium",...t,data:t})),custom:n=>{const o={...n,component:n.component?t(n.component):null,componentProps:n.componentProps||null};return e({type:"custom",hideFooter:n.hideFooter??!0,...n,data:o})},setLoading:e=>{H.loading=e}}}const N=(e,t)=>{const n=e.__vccOpts||e;for(const[o,r]of t)n[o]=r;return n},P={key:0,class:"modern-modal-overlay"},U={key:0,class:"modern-modal-header"},q={class:"modern-modal-title"},$={key:1,class:"modern-modal-close-absolute"},I=["for"],J=["id","name","type","placeholder","readonly","autofocus","onUpdate:modelValue","onInput"],W=["id","name","placeholder","readonly","onUpdate:modelValue","onInput"],K={key:2,class:"form-error"},X={key:1,class:"modern-confirm"},Z={class:"confirm-icon"},G={key:0,class:"fas fa-exclamation-triangle warning-icon"},Q={key:1,class:"fas fa-times-circle error-icon"},Y={key:2,class:"fas fa-trash-alt danger-icon"},ee={key:3,class:"fas fa-question-circle question-icon"},te={key:4,class:"fas fa-info-circle info-icon"},ne={class:"confirm-content"},oe={class:"confirm-message"},re={key:0,class:"confirm-checkbox"},ie={key:2},ae={key:1,class:"custom-content-text"},se={key:3},le={key:2,class:"modern-modal-footer"},ce=["disabled"],de=["disabled"],he={key:0,class:"loading-spinner-small"},ue=N({__name:"ModernModal",setup(e){const{modalState:t,formState:O,closeModal:S,confirmModal:R}=V();n(()=>t.isVisible,e=>{e&&"form"===t.type&&h(()=>{const e=O.fields.find(e=>e.autofocus);if(e){const t=document.getElementById(e.name);if(t&&(t.focus(),e.selectText&&e.defaultValue)){const n=(e=>{const t=e.lastIndexOf(".");return-1===t?e:e.substring(0,t)})(e.defaultValue);n&&setTimeout(()=>{t.setSelectionRange(0,n.length)},50)}}})});const L=e=>{var n;"Escape"===e.key&&t.isVisible&&(e.preventDefault(),e.stopPropagation(),null==(n=e.stopImmediatePropagation)||n.call(e),S())};o(()=>{document.addEventListener("keydown",L)}),r(()=>{document.removeEventListener("keydown",L)});const T=i(()=>{const e=[];return t.size&&e.push(`modal-${t.size}`),t.loading&&e.push("modal-loading"),t.modalClass&&e.push(t.modalClass),e}),H=i(()=>!t.hideFooter&&("confirm"===t.type||"form"===t.type)),_=e=>{O.errors[e]&&delete O.errors[e]},D=()=>{const e=t.icon;return"warning"===e?"warning-bg":"error"===e?"error-bg":"trash"===e?"danger-bg":"question"===e?"question-bg":"info-bg"},j=async()=>{var e,n;if(!t.loading)try{if("form"===t.type){const o=null==(e=t.data)?void 0:e.validate;o&&await o(O.values);for(const e of O.fields)if(e.required&&!(null==(n=O.values[e.name])?void 0:n.trim()))return void(O.errors[e.name]=`请输入${e.label}`);R(O.values)}else"confirm"===t.type?t.checkboxLabel?R({checked:!!t.checkboxChecked}):R(!0):R()}catch(o){if("form"===t.type)if(o.field&&o.message)O.errors[o.field]=o.message;else if(o.message&&o.message.trim()){const e=O.fields[0];e&&(O.errors[e.name]=o.message)}}};returLine truncated
@@ -1 +1 @@
.custom-content-text[data-v-4867ccc9]{white-space:pre-wrap;word-break:break-word;color:#374151;line-height:1.7}.modal-fade-enter-active[data-v-4867ccc9],.modal-fade-leave-active[data-v-4867ccc9]{transition:opacity .3s ease}.modal-fade-enter-from[data-v-4867ccc9],.modal-fade-leave-to[data-v-4867ccc9]{opacity:0}.modal-scale-enter-active[data-v-4867ccc9],.modal-scale-leave-active[data-v-4867ccc9]{transition:all .3s ease}.modal-scale-enter-from[data-v-4867ccc9],.modal-scale-leave-to[data-v-4867ccc9]{transform:scale(.9);opacity:0}.confirm-checkbox[data-v-4867ccc9]{display:inline-flex;align-items:center;gap:8px;margin-top:16px;font-size:14px;color:#475569;cursor:pointer}.confirm-checkbox input[data-v-4867ccc9]{width:16px;height:16px;cursor:pointer}.confirm-content[data-v-4867ccc9]{display:flex;flex-direction:column;align-items:flex-start;justify-content:center}.confirm-message[data-v-4867ccc9]{width:100%}body{font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Arial,sans-serif;background-color:#f5f5f5;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app{width:100%;min-height:100vh}.page-container{padding:20px;background-color:#fff;border-radius:8px;box-shadow:0 2px 12px #0000001a;margin:20px}.page-header{margin-bottom:20px;padding-bottom:20px;border-bottom:1px solid #e6e6e6}.page-title{font-size:24px;font-weight:600;color:#303133;margin-bottom:8px}.page-description{color:#909399;font-size:14px}.btn-group{display:flex;gap:12px;margin-bottom:20px}.table-container{margin-top:20px}.form-container{max-width:600px}.form-footer{text-align:right;margin-top:20px;padding-top:20px;border-top:1px solid #e6e6e6}.status-tag{font-size:12px}@media (max-width: 768px){.page-container{margin:10px;padding:15px}.btn-group{flex-direction:column}.form-container{max-width:100%}}:root{--primary-color: #4C74DF;--primary-color-end: #02A6F0;--sidebar-bg-start: #4C74DF;--sidebar-bg-end: #02A6F0;--app-bg: #F5F7FA;--card-bg: #fff;--text-main: #1e293b;--text-regular: #374151;--text-secondary: #64748b;--border-color: #e2e8f0;--success-color: #10b981;--warning-color: #f59e0b;--danger-color: #ef4444}html{-webkit-text-size-adjust:100%;color-scheme:light}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Helvetica Neue,PingFang SC,Microsoft YaHei,sans-serif;background-color:var(--app-bg);color:var(--text-main);line-height:1.6;font-size:14px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}button,input[type=button],input[type=submit],input[type=reset],input[type=text],input[type=password],input[type=number],input[type=email],input[type=url],input[type=search],textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none}.container{width:100%;margin:0;padding:20px}.content-container{width:100%;max-width:1200px;margin:0 auto;padding:20px}.content-container-fluid{width:85%;margin:0 auto;padding:20px}@media (max-width: 768px){.content-container{padding:15px}.content-container-fluid{width:90%;padding:15px}}@media (max-width: 480px){.content-container{padding:10px}.content-container-fluid{width:95%;padding:10px}}h1{font-size:24px;margin-bottom:20px;color:var(--text-main);font-weight:600}.btn{display:inline-flex;align-items:center;gap:8px;padding:10px 16px;border:none;border-radius:8px;background:var(--card-bg);color:var(--text-regular);font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;text-decoration:none;white-space:nowrap;box-shadow:0 1px 2px #0000000d}.btn:hover{background:#f8fafc;border-color:#cbd5e1;color:var(--text-main);box-shadow:0 2px 4px #0000001a}.btn-primary{background:linear-gradient(135deg,var(--primary-color),var(--primary-color-end));color:#fff;box-shadow:0 2px 4px #4c74df4d}.btn-primary:hover{background:linear-gradient(135deg,#3b5bdb,#1e88e5);color:#fff;box-shadow:0 4px 12px #4c74df66;transition:all .2s ease}.btn-secondary{background:#f8f9fa;color:#495057;box-shadow:0 2px 4px #0000001a}.btn-secondary:hover{background:#e9ecef;color:#212529;box-shadow:0 4px 12px #00000026}.btn-danger{background:var(--danger-color);border-color:var(--danger-color);color:#fff;box-shadow:0 2px 8px #ef44444d}.btn-danger:hover{background:#dc2626;border-color:#dc2626;color:#fff;box-shadow:0 4px 12px #ef444466}.btn-reset{padding:6px 14px;font-size:14px;background:transparent;border-color:var(--border-color);color:var(--text-secondary)}.btn-reset:hover{background:#f8fafc;border-color:#cbd5e1;color:var(--text-main)}.btn-sm{padding:6px 12px;font-size:12px}table{width:100%;border-collapse:collapse;background:var(--card-bg)}th,td{padding:16px 12px;text-align:left;border-bottom:1px solid #f1f5f9}th{background:#fafbfc;font-weight:600;color:var(--text-regular);font-size:14px;border-bottom:1px solid var(--border-color)}tr:hover{background:#f8fafc}tr:last-child td{border-bottom:none}input[type=text],input[type=password],input[type=email],select,textarea{width:100%;padding:10px 12px;border:1px solid var(--border-color);bordLine truncated
.custom-content-text[data-v-4867ccc9]{white-space:pre-wrap;word-break:break-word;color:#374151;line-height:1.7}.modal-fade-enter-active[data-v-4867ccc9],.modal-fade-leave-active[data-v-4867ccc9]{transition:opacity .3s ease}.modal-fade-enter-from[data-v-4867ccc9],.modal-fade-leave-to[data-v-4867ccc9]{opacity:0}.modal-scale-enter-active[data-v-4867ccc9],.modal-scale-leave-active[data-v-4867ccc9]{transition:all .3s ease}.modal-scale-enter-from[data-v-4867ccc9],.modal-scale-leave-to[data-v-4867ccc9]{transform:scale(.9);opacity:0}.confirm-checkbox[data-v-4867ccc9]{display:inline-flex;align-items:center;gap:8px;margin-top:16px;font-size:14px;color:#475569;cursor:pointer}.confirm-checkbox input[data-v-4867ccc9]{width:16px;height:16px;cursor:pointer}.confirm-content[data-v-4867ccc9]{display:flex;flex-direction:column;align-items:flex-start;justify-content:center}.confirm-message[data-v-4867ccc9]{width:100%}body{font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Arial,sans-serif;background-color:#f5f5f5;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app{width:100%;min-height:100vh}.page-container{padding:20px;background-color:#fff;border-radius:8px;box-shadow:0 2px 12px #0000001a;margin:20px}.page-header{margin-bottom:20px;padding-bottom:20px;border-bottom:1px solid #e6e6e6}.page-title{font-size:24px;font-weight:600;color:#303133;margin-bottom:8px}.page-description{color:#909399;font-size:14px}.btn-group{display:flex;gap:12px;margin-bottom:20px}.table-container{margin-top:20px}.form-container{max-width:600px}.form-footer{text-align:right;margin-top:20px;padding-top:20px;border-top:1px solid #e6e6e6}.status-tag{font-size:12px}@media (max-width: 768px){.page-container{margin:10px;padding:15px}.btn-group{flex-direction:column}.form-container{max-width:100%}}:root{--primary-color: #4C74DF;--primary-color-end: #02A6F0;--sidebar-bg-start: #4C74DF;--sidebar-bg-end: #02A6F0;--app-bg: #F5F7FA;--card-bg: #fff;--text-main: #1e293b;--text-regular: #374151;--text-secondary: #64748b;--border-color: #e2e8f0;--success-color: #10b981;--warning-color: #f59e0b;--danger-color: #ef4444}html{-webkit-text-size-adjust:100%;color-scheme:light}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Helvetica Neue,PingFang SC,Microsoft YaHei,sans-serif;background-color:var(--app-bg);color:var(--text-main);line-height:1.6;font-size:14px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}button,input[type=button],input[type=submit],input[type=reset],input[type=text],input[type=password],input[type=number],input[type=email],input[type=url],input[type=search],textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none}.container{width:100%;margin:0;padding:20px}.content-container{width:100%;max-width:1200px;margin:0 auto;padding:20px}.content-container-fluid{width:85%;margin:0 auto;padding:20px}@media (max-width: 768px){.content-container{padding:15px}.content-container-fluid{width:90%;padding:15px}}@media (max-width: 480px){.content-container{padding:10px}.content-container-fluid{width:95%;padding:10px}}h1{font-size:24px;margin-bottom:20px;color:var(--text-main);font-weight:600}.btn{display:inline-flex;align-items:center;gap:8px;padding:10px 16px;border:none;border-radius:8px;background:var(--card-bg);color:var(--text-regular);font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;text-decoration:none;white-space:nowrap;box-shadow:0 1px 2px #0000000d}.btn:hover{background:#f8fafc;border-color:#cbd5e1;color:var(--text-main);box-shadow:0 2px 4px #0000001a}.btn-primary{background:linear-gradient(135deg,var(--primary-color),var(--primary-color-end));color:#fff;box-shadow:0 2px 4px #4c74df4d}.btn-primary:hover{background:linear-gradient(135deg,#3b5bdb,#1e88e5);color:#fff;box-shadow:0 4px 12px #4c74df66;transition:all .2s ease}.btn-secondary{background:#f8f9fa;color:#495057;box-shadow:0 2px 4px #0000001a}.btn-secondary:hover{background:#e9ecef;color:#212529;box-shadow:0 4px 12px #00000026}.btn-danger{background:var(--danger-color);border-color:var(--danger-color);color:#fff;box-shadow:0 2px 8px #ef44444d}.btn-danger:hover{background:#dc2626;border-color:#dc2626;color:#fff;box-shadow:0 4px 12px #ef444466}.btn-reset{padding:6px 14px;font-size:14px;background:transparent;border-color:var(--border-color);color:var(--text-secondary)}.btn-reset:hover{background:#f8fafc;border-color:#cbd5e1;color:var(--text-main)}.btn-sm{padding:6px 12px;font-size:12px}table{width:100%;border-collapse:collapse;background:var(--card-bg)}th,td{padding:16px 12px;text-align:left;border-bottom:1px solid #f1f5f9}th{background:#fafbfc;font-weight:600;color:var(--text-regular);font-size:14px;border-bottom:1px solid var(--border-color)}tr:hover{background:#f8fafc}tr:last-child td{border-bottom:none}input[type=text],input[type=password],input[type=email],select,textarea{width:100%;padding:10px 12px;border:1px solid var(--border-color);bordLine truncated
-2
View File
@@ -1,2 +0,0 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/Index-zE6Vgya0.js","assets/logo-Bw7MxNqG.js","assets/vendor-6oHsDC_h.js","assets/DriverIcon-B0fZKxF3.js","assets/DriverIcon-Cx1JoYDo.css","assets/Index-C-h4RPQD.css","assets/Login-B1q5OFmm.js","assets/elementPlus-CM9o6iOw.js","assets/Login-DSG4_oID.css","assets/Admin-BBMEb1To.js","assets/Admin-yVJSJUqR.css"])))=>i.map(i=>d[i]);
import{R as e,a4 as t,w as n,f as o,C as r,c as i,I as a,L as s,J as l,a3 as c,a7 as d,n as h,x as u,v as f,F as p,u as m,A as v,M as y,H as g,O as b,G as w,a8 as E,V as F,a1 as M,a9 as z,aa as x,W as C,K as B,z as A,X as O,a0 as k,ab as S,ac as R,ad as L,ae as T}from"./vendor-6oHsDC_h.js";!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))t(e);new MutationObserver(e=>{for(const n of e)if("childList"===n.type)for(const e of n.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&t(e)}).observe(document,{childList:!0,subtree:!0})}function t(e){if(e.ep)return;e.ep=!0;const t=function(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?t.credentials="include":"anonymous"===e.crossOrigin?t.credentials="omit":t.credentials="same-origin",t}(e);fetch(e.href,t)}}();const H=e({isVisible:!1,title:"",content:"",type:"default",size:"medium",closable:!0,maskClosable:!1,loading:!1,data:null,resolve:null,reject:null}),_=e({fields:[],values:{},errors:{}});let D=null;const j=()=>{D&&(clearTimeout(D),D=null)};function V(){const e=e=>new Promise((t,n)=>{j(),Object.assign(H,{...e,isVisible:!0,resolve:t,reject:n})}),n=()=>{j(),D=setTimeout(()=>{Object.assign(H,{title:"",content:"",type:"default",size:"medium",modalClass:"",bodyClass:"",closable:!0,maskClosable:!1,loading:!1,data:null,resolve:null,reject:null,confirmClass:null,confirmText:null,cancelText:null,icon:null,hideCancelButton:!1,checkboxLabel:null,checkboxChecked:!1,hideFooter:!1}),Object.assign(_,{fields:[],values:{},errors:{}}),D=null},300)};return{modalState:H,formState:_,showModal:e,closeModal:()=>{H.isVisible=!1,H.reject&&H.reject(new Error("Modal closed")),n()},confirmModal:e=>{H.isVisible=!1,H.resolve&&H.resolve(e),n()},confirm:t=>e({type:"confirm",size:"medium",hideCancelButton:(null==t?void 0:t.hideCancelButton)??!0,...t}),form:t=>(_.fields=t.fields||[],_.values={},_.errors={},_.fields.forEach(e=>{_.values[e.name]=e.defaultValue||""}),e({type:"form",size:t.size||"medium",...t,data:t})),custom:n=>{const o={...n,component:n.component?t(n.component):null,componentProps:n.componentProps||null};return e({type:"custom",hideFooter:n.hideFooter??!0,...n,data:o})},setLoading:e=>{H.loading=e}}}const N=(e,t)=>{const n=e.__vccOpts||e;for(const[o,r]of t)n[o]=r;return n},P={key:0,class:"modern-modal-overlay"},U={key:0,class:"modern-modal-header"},q={class:"modern-modal-title"},$={key:1,class:"modern-modal-close-absolute"},I=["for"],J=["id","name","type","placeholder","readonly","autofocus","onUpdate:modelValue","onInput"],W=["id","name","placeholder","readonly","onUpdate:modelValue","onInput"],K={key:2,class:"form-error"},X={key:1,class:"modern-confirm"},Z={class:"confirm-icon"},G={key:0,class:"fas fa-exclamation-triangle warning-icon"},Q={key:1,class:"fas fa-times-circle error-icon"},Y={key:2,class:"fas fa-trash-alt danger-icon"},ee={key:3,class:"fas fa-question-circle question-icon"},te={key:4,class:"fas fa-info-circle info-icon"},ne={class:"confirm-content"},oe={class:"confirm-message"},re={key:0,class:"confirm-checkbox"},ie={key:2},ae={key:1,class:"custom-content-text"},se={key:3},le={key:2,class:"modern-modal-footer"},ce=["disabled"],de=["disabled"],he={key:0,class:"loading-spinner-small"},ue=N({__name:"ModernModal",setup(e){const{modalState:t,formState:k,closeModal:S,confirmModal:R}=V();n(()=>t.isVisible,e=>{e&&"form"===t.type&&h(()=>{const e=k.fields.find(e=>e.autofocus);if(e){const t=document.getElementById(e.name);if(t&&(t.focus(),e.selectText&&e.defaultValue)){const n=(e=>{const t=e.lastIndexOf(".");return-1===t?e:e.substring(0,t)})(e.defaultValue);n&&setTimeout(()=>{t.setSelectionRange(0,n.length)},50)}}})});const L=e=>{var n;"Escape"===e.key&&t.isVisible&&(e.preventDefault(),e.stopPropagation(),null==(n=e.stopImmediatePropagation)||n.call(e),S())};o(()=>{document.addEventListener("keydown",L)}),r(()=>{document.removeEventListener("keydown",L)});const T=i(()=>{const e=[];return t.size&&e.push(`modal-${t.size}`),t.loading&&e.push("modal-loading"),t.modalClass&&e.push(t.modalClass),e}),H=i(()=>!t.hideFooter&&("confirm"===t.type||"form"===t.type)),_=e=>{k.errors[e]&&delete k.errors[e]},D=()=>{const e=t.icon;return"warning"===e?"warning-bg":"error"===e?"error-bg":"trash"===e?"danger-bg":"question"===e?"question-bg":"info-bg"},j=async()=>{var e,n;if(!t.loading)try{if("form"===t.type){const o=null==(e=t.data)?void 0:e.validate;o&&await o(k.values);for(const e of k.fields)if(e.required&&!(null==(n=k.values[e.name])?void 0:n.trim()))return void(k.errors[e.name]=`请输入${e.label}`);R(k.values)}else"confirm"===t.type?t.checkboxLabel?R({checked:!!t.checkboxChecked}):R(!0):R()}catch(o){if("form"===t.type)if(o.field&&o.message)k.errors[o.field]=o.message;else if(o.message&&o.message.trim()){const e=k.fields[0];e&&(k.errors[e.name]=o.message)}}};returLine truncated
@@ -33,4 +33,4 @@ let gs;const ms="undefined"!=typeof window&&window.trustedTypes;if(ms)try{gs=ms.
* (c) 2025 Eduardo San Martin Morote
* @license MIT
*/let Sc;const Cc=e=>Sc=e,kc=Symbol();function Ec(e){return e&&"object"==typeof e&&"[object Object]"===Object.prototype.toString.call(e)&&"function"!=typeof e.toJSON}var Oc,Ac;function Tc(){const e=te(!0),t=e.run(()=>St({}));let n=[],r=[];const o=bt({install(e){Cc(o),o._a=e,e.provide(kc,o),e.config.globalProperties.$pinia=o,r.forEach(e=>n.push(e)),r=[]},use(e){return this._a?n.push(e):r.push(e),this},_p:n,_a:null,_e:e,_s:new Map,state:t});return o}(Ac=Oc||(Oc={})).direct="direct",Ac.patchObject="patch object",Ac.patchFunction="patch function";const Pc=()=>{};function jc(e,t,n,r=Pc){e.push(t);const o=()=>{const n=e.indexOf(t);n>-1&&(e.splice(n,1),r())};return!n&&ne()&&re(o),o}function Rc(e,...t){e.slice().forEach(e=>{e(...t)})}const $c=e=>e(),Mc=Symbol(),Lc=Symbol();function Fc(e,t){e instanceof Map&&t instanceof Map?t.forEach((t,n)=>e.set(n,t)):e instanceof Set&&t instanceof Set&&t.forEach(e.add,e);for(const n in t){if(!t.hasOwnProperty(n))continue;const r=t[n],o=e[n];Ec(o)&&Ec(r)&&e.hasOwnProperty(n)&&!wt(r)&&!ht(r)?e[n]=Fc(o,r):e[n]=r}return e}const Ic=Symbol();function Dc(e){return!Ec(e)||!e.hasOwnProperty(Ic)}const{assign:Vc}=Object;function Nc(e){return!(!wt(e)||!e.effect)}function Uc(e,t,n={},r,o,s){let i;const l=Vc({actions:{}},n),c={deep:!0};let a,u,f,p=[],d=[];const h=r.state.value[e];let v;function g(t){let n;a=u=!1,"function"==typeof t?(t(r.state.value[e]),n={type:Oc.patchFunction,storeId:e,events:f}):(Fc(r.state.value[e],t),n={type:Oc.patchObject,payload:t,storeId:e,events:f});const o=v=Symbol();Zt().then(()=>{v===o&&(a=!0)}),u=!0,Rc(p,n,r.state.value[e])}s||h||(r.state.value[e]={}),St({});const m=s?function(){const{state:e}=n,t=e?e():{};this.$patch(e=>{Vc(e,t)})}:Pc;const y=(t,n="")=>{if(Mc in t)return t[Lc]=n,t;const o=function(){Cc(r);const n=Array.from(arguments),s=[],i=[];let l;Rc(d,{args:n,name:o[Lc],store:b,after:function(e){s.push(e)},onError:function(e){i.push(e)}});try{l=t.apply(this&&this.$id===e?this:b,n)}catch(c){throw Rc(i,c),c}return l instanceof Promise?l.then(e=>(Rc(s,e),e)).catch(e=>(Rc(i,e),Promise.reject(e))):(Rc(s,l),l)};return o[Mc]=!0,o[Lc]=n,o},b=ut({_p:r,$id:e,$onAction:jc.bind(null,d),$patch:g,$reset:m,$subscribe(t,n={}){const o=jc(p,t,n.detached,()=>s()),s=i.run(()=>ao(()=>r.state.value[e],r=>{("sync"===n.flush?u:a)&&t({storeId:e,type:Oc.direct,events:f},r)},Vc({},c,n)));return o},$dispose:function(){i.stop(),p=[],d=[],r._s.delete(e)}});r._s.set(e,b);const _=(r._a&&r._a.runWithContext||$c)(()=>r._e.run(()=>(i=te()).run(()=>t({action:y}))));for(const x in _){const t=_[x];if(wt(t)&&!Nc(t)||ht(t))s||(h&&Dc(t)&&(wt(t)?t.value=h[x]:Fc(t,h[x])),r.state.value[e][x]=t);else if("function"==typeof t){const e=y(t,x);_[x]=e,l.actions[x]=t}}return Vc(b,_),Vc(yt(b),_),Object.defineProperty(b,"$state",{get:()=>r.state.value[e],set:e=>{g(t=>{Vc(t,e)})}}),r._p.forEach(e=>{Vc(b,i.run(()=>e({store:b,app:r._a,pinia:r,options:l})))}),h&&s&&n.hydrate&&n.hydrate(b.$state,h),a=!0,u=!0,b}
/*! #__NO_SIDE_EFFECTS__ */function Bc(e,t,n){let r,o;const s="function"==typeof t;function i(e,n){const i=!(!Yo()&&!Fr);(e=e||(i?Dr(kc,null):null))&&Cc(e),(e=Sc)._s.has(r)||(s?Uc(r,t,o,e):function(e,t,n){const{state:r,actions:o,getters:s}=t,i=n.state.value[e];let l;l=Uc(e,function(){i||(n.state.value[e]=r?r():{});const t=Pt(n.state.value[e]);return Vc(t,o,Object.keys(s||{}).reduce((t,r)=>(t[r]=bt(ps(()=>{Cc(n);const t=n._s.get(e);return s[r].call(t,t)})),t),{}))},t,n,0,!0)}(r,o,e));return e._s.get(r)}return r=e,o=s?n:t,i.$id=r,i}function Wc(e){{const t=yt(e),n={};for(const r in t){const o=t[r];o.effect?n[r]=ps({get:()=>e[r],set(t){e[r]=t}}):(wt(o)||ht(o))&&(n[r]=$t(e,r))}return n}}export{pi as $,Vo as A,$t as B,Yn as C,yr as D,mr as E,qo as F,So as G,W as H,Mo as I,cn as J,lr as K,No as L,Ri as M,r as N,J as O,D as P,Qn as Q,ut as R,f as S,Co as T,yt as U,an as V,xi as W,Bo as X,Pt as Y,ds as Z,Zn as _,g as a,sr as a0,Mi as a1,Ws as a2,Os as a3,bt as a4,Lo as a5,Di as a6,_n as a7,ur as a8,Ai as a9,_i as aa,_c as ab,$l as ac,Vi as ad,Tc as ae,Wo as af,xc as ag,Bc as ah,Wc as ai,Ci as aj,wc as ak,co as b,ps as c,pt as d,ne as e,Jn as f,Yo as g,wt as h,Dr as i,y as j,u as k,vs as l,v as m,Zt as n,re as o,Ir as p,O as q,St as r,Ct as s,In as t,Ot as u,$o as v,ao as w,To as x,fr as y,zo as z};
/*! #__NO_SIDE_EFFECTS__ */function Bc(e,t,n){let r,o;const s="function"==typeof t;function i(e,n){const i=!(!Yo()&&!Fr);(e=e||(i?Dr(kc,null):null))&&Cc(e),(e=Sc)._s.has(r)||(s?Uc(r,t,o,e):function(e,t,n){const{state:r,actions:o,getters:s}=t,i=n.state.value[e];let l;l=Uc(e,function(){i||(n.state.value[e]=r?r():{});const t=Pt(n.state.value[e]);return Vc(t,o,Object.keys(s||{}).reduce((t,r)=>(t[r]=bt(ps(()=>{Cc(n);const t=n._s.get(e);return s[r].call(t,t)})),t),{}))},t,n,0,!0)}(r,o,e));return e._s.get(r)}return r=e,o=s?n:t,i.$id=r,i}function Wc(e){{const t=yt(e),n={};for(const r in t){const o=t[r];o.effect?n[r]=ps({get:()=>e[r],set(t){e[r]=t}}):(wt(o)||ht(o))&&(n[r]=$t(e,r))}return n}}export{pi as $,Vo as A,$t as B,Yn as C,yr as D,mr as E,qo as F,So as G,W as H,Mo as I,cn as J,lr as K,No as L,Ri as M,r as N,J as O,D as P,Qn as Q,ut as R,f as S,Co as T,yt as U,an as V,xi as W,Bo as X,Pt as Y,ds as Z,Zn as _,g as a,sr as a0,Mi as a1,Ws as a2,Os as a3,bt as a4,Lo as a5,Di as a6,_n as a7,ur as a8,Ai as a9,_i as aa,_c as ab,$l as ac,Vi as ad,Tc as ae,Wo as af,wc as ag,xc as ah,Bc as ai,Wc as aj,Ci as ak,co as b,ps as c,pt as d,ne as e,Jn as f,Yo as g,wt as h,Dr as i,y as j,u as k,vs as l,v as m,Zt as n,re as o,Ir as p,O as q,St as r,Ct as s,In as t,Ot as u,$o as v,ao as w,To as x,fr as y,zo as z};
+3 -3
View File
@@ -8,9 +8,9 @@
<link rel="icon" href="/favicon.ico">
<!-- FontAwesome 图标库 - 本地版本 -->
<link rel="stylesheet" href="/css/all.min.css">
<script type="module" crossorigin src="/assets/main-o8DXkHjp.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-6oHsDC_h.js">
<link rel="stylesheet" crossorigin href="/assets/main-CLKiqhPx.css">
<script type="module" crossorigin src="/assets/main-BChklWkf.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-CVFEPmGb.js">
<link rel="stylesheet" crossorigin href="/assets/main-DNso_XKa.css">
</head>
<body>
<div id="app"></div>