Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4b05ef564 | ||
|
|
274078084e | ||
|
|
d57b02b832 | ||
|
|
861bb63f37 | ||
|
|
e406731d9e | ||
|
|
c284a9259a | ||
|
|
032f7c487d |
No files matched your search
@@ -1,4 +1,4 @@
|
||||
# LitePan v0.2.0-beta
|
||||
# LitePan v0.3.0-beta
|
||||
|
||||
LitePan 是一个多网盘聚合与管理工具,支持 Web 界面、WebDAV、STRM 生成、媒体整理等功能。
|
||||
|
||||
|
||||
+27
-3
@@ -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
@@ -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,
|
||||
}
|
||||
})
|
||||
@@ -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}")
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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}>"
|
||||
@@ -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, "缓存系统已停止"),
|
||||
),
|
||||
|
||||
@@ -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,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))
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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 {}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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": [],
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
)
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
*,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}>"
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "litepan-frontend",
|
||||
"version": "0.2.0-beta",
|
||||
"version": "0.3.0-beta",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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>
|
||||
@@ -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>`,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
@@ -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%);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+392
-12
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
|
||||
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 +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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 +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 +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
-1
@@ -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
|
||||
@@ -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
|
||||
@@ -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};
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user