test/custom-admin-ui #3

Merged
orion merged 673 commits from test/custom-admin-ui into master 2026-06-05 17:20:58 +08:00
20 changed files with 281 additions and 82 deletions
Showing only changes of commit 1cbe8d5f2a - Show all commits
+3 -2
View File
@@ -1,8 +1,9 @@
import datetime
import uuid
import os
import uuid
from fastapi import UploadFile, HTTPException
from typing import Tuple, Optional
from typing import Optional, Tuple
from apps.base.dependencies import IPRateLimit
from apps.base.models import FileCodes
+5 -1
View File
@@ -135,4 +135,8 @@ async def download_file(key: str, code: str, ip: str = Depends(ip_limit['error']
if not has:
return APIResponse(code=404, detail='文件不存在')
return APIResponse(detail=file_code.text) if file_code.text else await file_storage.get_file_response(file_code)
return (
APIResponse(detail=file_code.text)
if file_code.text
else await file_storage.get_file_response(file_code)
)
+3
View File
@@ -40,6 +40,9 @@ DEFAULT_CONFIG = {
'uploadSize': 1024 * 1024 * 10,
'expireStyle': ['day', 'hour', 'minute', 'forever', 'count'],
'uploadMinute': 1,
'webdav_url': '',
'webdav_password': '',
'webdav_username': '',
'opacity': 0.9,
'background': '',
'uploadCount': 10,
+254 -60
View File
@@ -3,7 +3,7 @@
# @File : storage.py
# @Software: PyCharm
from typing import Optional
from urllib.parse import quote, unquote
import aiohttp
import asyncio
from pathlib import Path
@@ -22,11 +22,13 @@ from fastapi.responses import FileResponse
class FileStorageInterface:
_instance: Optional['FileStorageInterface'] = None
_instance: Optional["FileStorageInterface"] = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(FileStorageInterface, cls).__new__(cls, *args, **kwargs)
cls._instance = super(FileStorageInterface, cls).__new__(
cls, *args, **kwargs
)
return cls._instance
async def save_file(self, file: UploadFile, save_path: str):
@@ -66,7 +68,7 @@ class SystemFileStorage(FileStorageInterface):
self.root_path = data_root
def _save(self, file, save_path):
with open(save_path, 'wb') as f:
with open(save_path, "wb") as f:
chunk = file.read(self.chunk_size)
while chunk:
f.write(chunk)
@@ -89,7 +91,7 @@ class SystemFileStorage(FileStorageInterface):
async def get_file_response(self, file_code: FileCodes):
file_path = self.root_path / await file_code.get_file_path()
if not file_path.exists():
return APIResponse(code=404, detail='文件已过期删除')
return APIResponse(code=404, detail="文件已过期删除")
return FileResponse(file_path, filename=file_code.prefix + file_code.suffix)
@@ -101,30 +103,62 @@ class S3FileStorage(FileStorageInterface):
self.s3_hostname = settings.s3_hostname
self.region_name = settings.s3_region_name
self.signature_version = settings.s3_signature_version
self.endpoint_url = settings.s3_endpoint_url or f'https://{self.s3_hostname}'
self.endpoint_url = settings.s3_endpoint_url or f"https://{self.s3_hostname}"
self.aws_session_token = settings.aws_session_token
self.proxy = settings.s3_proxy
self.session = aioboto3.Session(aws_access_key_id=self.access_key_id, aws_secret_access_key=self.secret_access_key)
self.session = aioboto3.Session(
aws_access_key_id=self.access_key_id,
aws_secret_access_key=self.secret_access_key,
)
if not settings.s3_endpoint_url:
self.endpoint_url = f'https://{self.s3_hostname}'
self.endpoint_url = f"https://{self.s3_hostname}"
else:
# 如果提供了 s3_endpoint_url,则优先使用它
self.endpoint_url = settings.s3_endpoint_url
async def save_file(self, file: UploadFile, save_path: str):
async with self.session.client("s3", endpoint_url=self.endpoint_url, aws_session_token=self.aws_session_token, region_name=self.region_name,
config=Config(signature_version=self.signature_version)) as s3:
await s3.put_object(Bucket=self.bucket_name, Key=save_path, Body=await file.read(), ContentType=file.content_type)
async with self.session.client(
"s3",
endpoint_url=self.endpoint_url,
aws_session_token=self.aws_session_token,
region_name=self.region_name,
config=Config(signature_version=self.signature_version),
) as s3:
await s3.put_object(
Bucket=self.bucket_name,
Key=save_path,
Body=await file.read(),
ContentType=file.content_type,
)
async def delete_file(self, file_code: FileCodes):
async with self.session.client("s3", endpoint_url=self.endpoint_url, region_name=self.region_name, config=Config(signature_version=self.signature_version)) as s3:
await s3.delete_object(Bucket=self.bucket_name, Key=await file_code.get_file_path())
async with self.session.client(
"s3",
endpoint_url=self.endpoint_url,
region_name=self.region_name,
config=Config(signature_version=self.signature_version),
) as s3:
await s3.delete_object(
Bucket=self.bucket_name, Key=await file_code.get_file_path()
)
async def get_file_response(self, file_code: FileCodes):
try:
filename = file_code.prefix + file_code.suffix
async with self.session.client("s3", endpoint_url=self.endpoint_url, region_name=self.region_name, config=Config(signature_version=self.signature_version)) as s3:
link = await s3.generate_presigned_url('get_object', Params={'Bucket': self.bucket_name, 'Key': await file_code.get_file_path()}, ExpiresIn=3600)
async with self.session.client(
"s3",
endpoint_url=self.endpoint_url,
region_name=self.region_name,
config=Config(signature_version=self.signature_version),
) as s3:
link = await s3.generate_presigned_url(
"get_object",
Params={
"Bucket": self.bucket_name,
"Key": await file_code.get_file_path(),
},
ExpiresIn=3600,
)
tmp = io.BytesIO()
async with aiohttp.ClientSession() as session:
async with session.get(link) as resp:
@@ -132,18 +166,36 @@ class S3FileStorage(FileStorageInterface):
tmp.seek(0)
content = tmp.read()
tmp.close()
return Response(content, media_type="application/octet-stream", headers={"Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"'})
return Response(
content,
media_type="application/octet-stream",
headers={
"Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"'
},
)
except Exception:
raise HTTPException(status_code=503, detail='服务代理下载异常,请稍后再试')
raise HTTPException(status_code=503, detail="服务代理下载异常,请稍后再试")
async def get_file_url(self, file_code: FileCodes):
if file_code.prefix == '文本分享':
if file_code.prefix == "文本分享":
return file_code.text
if self.proxy:
return await get_file_url(file_code.code)
else:
async with self.session.client("s3", endpoint_url=self.endpoint_url, region_name=self.region_name, config=Config(signature_version=self.signature_version)) as s3:
result = await s3.generate_presigned_url('get_object', Params={'Bucket': self.bucket_name, 'Key': await file_code.get_file_path()}, ExpiresIn=3600)
async with self.session.client(
"s3",
endpoint_url=self.endpoint_url,
region_name=self.region_name,
config=Config(signature_version=self.signature_version),
) as s3:
result = await s3.generate_presigned_url(
"get_object",
Params={
"Bucket": self.bucket_name,
"Key": await file_code.get_file_path(),
},
ExpiresIn=3600,
)
return result
@@ -152,9 +204,11 @@ class OneDriveFileStorage(FileStorageInterface):
try:
import msal
from office365.graph_client import GraphClient
from office365.runtime.client_request_exception import ClientRequestException
from office365.runtime.client_request_exception import (
ClientRequestException,
)
except ImportError:
raise ImportError('请先安装`msal`和`Office365-REST-Python-Client`')
raise ImportError("请先安装`msal`和`Office365-REST-Python-Client`")
self.msal = msal
self.domain = settings.onedrive_domain
self.client_id = settings.onedrive_client_id
@@ -165,36 +219,45 @@ class OneDriveFileStorage(FileStorageInterface):
try:
client = GraphClient(self.acquire_token_pwd)
self.root_path = client.me.drive.root.get_by_path(settings.onedrive_root_path).get().execute_query()
self.root_path = (
client.me.drive.root.get_by_path(settings.onedrive_root_path)
.get()
.execute_query()
)
except ClientRequestException as e:
if e.code == 'itemNotFound':
if e.code == "itemNotFound":
client.me.drive.root.create_folder(settings.onedrive_root_path)
self.root_path = client.me.drive.root.get_by_path(settings.onedrive_root_path).get().execute_query()
self.root_path = (
client.me.drive.root.get_by_path(settings.onedrive_root_path)
.get()
.execute_query()
)
else:
raise e
except Exception as e:
raise Exception('OneDrive验证失败,请检查配置是否正确\n' + str(e))
raise Exception("OneDrive验证失败,请检查配置是否正确\n" + str(e))
def acquire_token_pwd(self):
authority_url = f'https://login.microsoftonline.com/{self.domain}'
authority_url = f"https://login.microsoftonline.com/{self.domain}"
app = self.msal.PublicClientApplication(
authority=authority_url,
client_id=self.client_id
authority=authority_url, client_id=self.client_id
)
result = app.acquire_token_by_username_password(
username=self.username,
password=self.password,
scopes=["https://graph.microsoft.com/.default"],
)
result = app.acquire_token_by_username_password(username=self.username,
password=self.password,
scopes=['https://graph.microsoft.com/.default'])
return result
def _get_path_str(self, path):
if isinstance(path, str):
path = path.replace('\\', '/').replace('//', '/').split('/')
path = path.replace("\\", "/").replace("//", "/").split("/")
elif isinstance(path, Path):
path = str(path).replace('\\', '/').replace('//', '/').split('/')
path = str(path).replace("\\", "/").replace("//", "/").split("/")
else:
raise TypeError('path must be str or Path')
path[-1] = path[-1].split('.')[0]
return '/'.join(path)
raise TypeError("path must be str or Path")
path[-1] = path[-1].split(".")[0]
return "/".join(path)
def _save(self, file, save_path):
content = file.file.read()
@@ -210,7 +273,7 @@ class OneDriveFileStorage(FileStorageInterface):
try:
self.root_path.get_by_path(path).delete_object().execute_query()
except self._ClientRequestException as e:
if e.code == 'itemNotFound':
if e.code == "itemNotFound":
pass
else:
raise e
@@ -219,23 +282,29 @@ class OneDriveFileStorage(FileStorageInterface):
await asyncio.to_thread(self._delete, await file_code.get_file_path())
def _convert_link_to_download_link(self, link):
p1 = re.search(r'https:\/\/(.+)\.sharepoint\.com', link).group(1)
p2 = re.search(r'personal\/(.+)\/', link).group(1)
p3 = re.search(rf'{p2}\/(.+)', link).group(1)
return f'https://{p1}.sharepoint.com/personal/{p2}/_layouts/52/download.aspx?share={p3}'
p1 = re.search(r"https:\/\/(.+)\.sharepoint\.com", link).group(1)
p2 = re.search(r"personal\/(.+)\/", link).group(1)
p3 = re.search(rf"{p2}\/(.+)", link).group(1)
return f"https://{p1}.sharepoint.com/personal/{p2}/_layouts/52/download.aspx?share={p3}"
def _get_file_url(self, save_path, name):
path = self._get_path_str(save_path)
remote_file = self.root_path.get_by_path(path + '/' + name)
expiration_datetime = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(hours=1)
remote_file = self.root_path.get_by_path(path + "/" + name)
expiration_datetime = datetime.datetime.now(
tz=datetime.timezone.utc
) + datetime.timedelta(hours=1)
expiration_datetime = expiration_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
premission = remote_file.create_link("view", "anonymous", expiration_datetime=expiration_datetime).execute_query()
premission = remote_file.create_link(
"view", "anonymous", expiration_datetime=expiration_datetime
).execute_query()
return self._convert_link_to_download_link(premission.link.webUrl)
async def get_file_response(self, file_code: FileCodes):
try:
filename = file_code.prefix + file_code.suffix
link = await asyncio.to_thread(self._get_file_url, await file_code.get_file_path(), filename)
link = await asyncio.to_thread(
self._get_file_url, await file_code.get_file_path(), filename
)
tmp = io.BytesIO()
async with aiohttp.ClientSession() as session:
async with session.get(link) as resp:
@@ -243,15 +312,25 @@ class OneDriveFileStorage(FileStorageInterface):
tmp.seek(0)
content = tmp.read()
tmp.close()
return Response(content, media_type="application/octet-stream", headers={"Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"'})
return Response(
content,
media_type="application/octet-stream",
headers={
"Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"'
},
)
except Exception:
raise HTTPException(status_code=503, detail='服务代理下载异常,请稍后再试')
raise HTTPException(status_code=503, detail="服务代理下载异常,请稍后再试")
async def get_file_url(self, file_code: FileCodes):
if self.proxy:
return await get_file_url(file_code.code)
else:
return await asyncio.to_thread(self._get_file_url, await file_code.get_file_path(), f'{file_code.prefix}{file_code.suffix}')
return await asyncio.to_thread(
self._get_file_url,
await file_code.get_file_path(),
f"{file_code.prefix}{file_code.suffix}",
)
class OpenDALFileStorage(FileStorageInterface):
@@ -263,10 +342,12 @@ class OpenDALFileStorage(FileStorageInterface):
self.service = settings.opendal_scheme
service_settings = {}
for key, value in settings.items():
if key.startswith('opendal_' + self.service):
setting_name = key.split('_', 2)[2]
if key.startswith("opendal_" + self.service):
setting_name = key.split("_", 2)[2]
service_settings[setting_name] = value
self.operator = opendal.AsyncOperator(settings.opendal_scheme, **service_settings)
self.operator = opendal.AsyncOperator(
settings.opendal_scheme, **service_settings
)
async def save_file(self, file: UploadFile, save_path: str):
await self.operator.write(save_path, file.file.read())
@@ -281,18 +362,131 @@ class OpenDALFileStorage(FileStorageInterface):
try:
filename = file_code.prefix + file_code.suffix
content = await self.operator.read(await file_code.get_file_path())
headers = {
"Content-Disposition": f'attachment; filename="{filename}"'
}
return Response(content, headers=headers, media_type="application/octet-stream")
headers = {"Content-Disposition": f'attachment; filename="{filename}"'}
return Response(
content, headers=headers, media_type="application/octet-stream"
)
except Exception as e:
print(e, file=sys.stderr)
raise HTTPException(status_code=404, detail="文件已过期删除")
class WebDAVFileStorage(FileStorageInterface):
_instance: Optional["WebDAVFileStorage"] = None
def __init__(self):
if not hasattr(self, "_initialized"):
self.base_url = settings.webdav_url.rstrip("/") + "/"
self.auth = aiohttp.BasicAuth(
login=settings.webdav_username, password=settings.webdav_password
)
self._initialized = True
def _build_url(self, path: str) -> str:
encoded_path = quote(str(path).lstrip("/"))
return f"{self.base_url}{encoded_path}"
async def _mkdir_p(self, directory_path: str):
"""递归创建目录(类似mkdir -p"""
path_obj = Path(unquote(directory_path))
current_path = ""
async with aiohttp.ClientSession(auth=self.auth) as session:
# 逐级检查目录是否存在
for part in path_obj.parts:
current_path = str(Path(current_path) / part)
url = self._build_url(current_path)
# 检查目录是否存在
async with session.head(url) as resp:
if resp.status == 404:
# 创建目录
async with session.request("MKCOL", url) as mkcol_resp:
if mkcol_resp.status not in (200, 201, 409):
content = await mkcol_resp.text()
raise HTTPException(
status_code=mkcol_resp.status,
detail=f"目录创建失败: {content[:200]}",
)
async def save_file(self, file: UploadFile, save_path: str):
"""保存文件(自动创建目录)"""
# 分离文件名和目录路径
path_obj = Path(save_path)
directory_path = str(path_obj.parent)
file_name = path_obj.name
try:
# 先创建目录结构
await self._mkdir_p(directory_path)
# 上传文件
url = self._build_url(save_path)
async with aiohttp.ClientSession(auth=self.auth) as session:
content = await file.read() # 注意:大文件需要分块读取
async with session.put(
url, data=content, headers={"Content-Type": file.content_type}
) as resp:
if resp.status not in (200, 201, 204):
content = await resp.text()
raise HTTPException(
status_code=resp.status,
detail=f"文件上传失败: {content[:200]}",
)
except aiohttp.ClientError as e:
raise HTTPException(status_code=503, detail=f"WebDAV连接异常: {str(e)}")
async def delete_file(self, file_code: FileCodes):
"""删除WebDAV文件"""
file_path = await file_code.get_file_path()
url = self._build_url(file_path)
try:
async with aiohttp.ClientSession(auth=self.auth) as session:
async with session.delete(url) as resp:
if resp.status not in (200, 204):
content = await resp.text()
raise HTTPException(
status_code=resp.status,
detail=f"WebDAV删除失败: {content[:200]}",
)
except aiohttp.ClientError as e:
raise HTTPException(status_code=503, detail=f"WebDAV连接异常: {str(e)}")
async def get_file_url(self, file_code: FileCodes):
return await get_file_url(file_code.code)
async def get_file_response(self, file_code: FileCodes):
"""获取文件响应(代理模式)"""
try:
filename = file_code.prefix + file_code.suffix
url = self._build_url(await file_code.get_file_path())
async with aiohttp.ClientSession(auth=self.auth) as session:
async with session.get(url) as resp:
if resp.status != 200:
raise HTTPException(
status_code=resp.status,
detail=f"文件获取失败: {await resp.text()}",
)
# 读取内容到内存
content = await resp.read()
return Response(
content=content,
media_type=resp.headers.get(
"Content-Type", "application/octet-stream"
),
headers={
"Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode()}"'
},
)
except aiohttp.ClientError as e:
raise HTTPException(status_code=503, detail=f"WebDAV连接异常: {str(e)}")
storages = {
'local': SystemFileStorage,
's3': S3FileStorage,
'onedrive': OneDriveFileStorage,
'opendal': OpenDALFileStorage,
"local": SystemFileStorage,
"s3": S3FileStorage,
"onedrive": OneDriveFileStorage,
"opendal": OpenDALFileStorage,
"webdav": WebDAVFileStorage,
}
-1
View File
@@ -16,7 +16,6 @@ from core.utils import get_now
async def delete_expire_files():
file_storage: FileStorageInterface = storages[settings.file_storage]()
print(settings.file_storage)
while True:
try:
# 遍历 share目录下的所有文件夹,删除空的文件夹,并判断父目录是否为空,如果为空也删除
+2 -4
View File
@@ -7,9 +7,7 @@ import hashlib
import random
import string
import time
from apps.base.dependencies import IPRateLimit
from core.settings import settings
async def get_random_num():
"""
@@ -46,7 +44,7 @@ async def get_select_token(code: str):
:param code:
:return:
"""
token = "123456"
token = settings.admin_token
return hashlib.sha256(f"{code}{int(time.time() / 1000)}000{token}".encode()).hexdigest()
@@ -1,4 +1,4 @@
import{c as i,B as k,r as h,o as u,I as v,d as x,e as t,n as o,g as e,i as w,f as n,X as _,F as M,q as C,p as z,y as B,J as L,m as D,K as F,t as I,z as d}from"./index-BIfJi_O-.js";import{B as j}from"./box-C9xlYTT6.js";/**
import{c as i,B as k,r as h,o as u,I as v,d as x,e as t,n as o,g as e,i as w,f as n,X as _,F as M,q as C,p as z,y as B,J as L,m as D,K as F,t as I,z as d}from"./index-CiI2cp0V.js";import{B as j}from"./box-BA8SMkDM.js";/**
* @license lucide-vue-next v0.445.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as g,B as v,H as w,o as _,d as u,e as t,n as a,g as e,i as k,t as i,f as y,F as U,q as F,A as M,z as p,J as z,K as C}from"./index-BIfJi_O-.js";import{F as m}from"./file-AHrKtGwY.js";import{H as D,T as S}from"./trash-wtxrZ93m.js";/**
import{c as g,B as v,H as w,o as _,d as u,e as t,n as a,g as e,i as k,t as i,f as y,F as U,q as F,A as M,z as p,J as z,K as C}from"./index-CiI2cp0V.js";import{F as m}from"./file-CWusGEpI.js";import{H as D,T as S}from"./trash-dDHutUhM.js";/**
* @license lucide-vue-next v0.445.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as V,B as T,u as q,r as _,b as k,d as c,e as t,n as o,g as a,i as A,j as P,f as m,v as H,m as I,F as v,q as w,t as d,A as S,z as g}from"./index-BIfJi_O-.js";import{F as L}from"./file-AHrKtGwY.js";/**
import{c as V,B as T,u as q,r as _,b as k,d as c,e as t,n as o,g as a,i as A,j as P,f as m,v as H,m as I,F as v,q as w,t as d,A as S,z as g}from"./index-CiI2cp0V.js";import{F as L}from"./file-CWusGEpI.js";/**
* @license lucide-vue-next v0.445.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1 +1 @@
import{G as y,r as u,B as v,u as b,d as w,e,n as l,g as o,i as x,f as h,h as k,j as S,v as A,m as V,t as B,y as D,z as j,A as P,_}from"./index-BIfJi_O-.js";import{B as z}from"./box-C9xlYTT6.js";const M=y("adminData",()=>{const d=u(localStorage.getItem("adminPassword")||"");function n(t){d.value=t,localStorage.setItem("token",t)}return{adminPassword:d,updateAdminPwd:n}}),I={class:"mx-auto h-16 w-16 relative"},L={class:"rounded-md shadow-sm -space-y-px"},N=["disabled"],T=v({__name:"LoginView",setup(d){const n=b(),t=u(""),i=u(!1),s=x("isDarkMode"),c=M(),p=()=>{let a=!0;return t.value?t.value.length<6&&(n.showAlert("密码长度至少为6位","error"),a=!1):(n.showAlert("无效的密码","error"),a=!1),a},m=D(),f=async()=>{if(p()){c.updateAdminPwd(t.value),P.post("/admin/login",{password:t.value}).then(()=>{m.push("/admin")}).catch(a=>{n.showAlert(a.response.data.detail,"error")}),i.value=!0;try{await new Promise(a=>setTimeout(a,2e3))}catch{}finally{i.value=!1}}};return(a,r)=>(j(),w("div",{class:l(["min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 transition-colors duration-200 relative overflow-hidden",o(s)?"bg-gray-900":"bg-gray-50"])},[r[6]||(r[6]=e("div",{class:"absolute inset-0 z-0"},[e("div",{class:"cyber-grid"}),e("div",{class:"floating-particles"})],-1)),e("div",{class:l(["max-w-md w-full space-y-8 backdrop-blur-lg bg-opacity-20 p-8 rounded-xl border border-opacity-20",[o(s)?"bg-gray-800 border-gray-600":"bg-white/70 border-gray-200"]])},[e("div",null,[e("div",I,[r[1]||(r[1]=e("div",{class:"absolute inset-0 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full animate-spin-slow"},null,-1)),r[2]||(r[2]=e("div",{class:"absolute -inset-2 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full opacity-50 blur-md animate-pulse"},null,-1)),e("div",{class:l(["absolute inset-1 rounded-full flex items-center justify-center",o(s)?"bg-gray-800":"bg-white"])},[h(o(z),{class:l(["h-8 w-8",o(s)?"text-cyan-400":"text-cyan-600"])},null,8,["class"])],2)]),e("h2",{class:l(["mt-6 text-center text-3xl font-extrabold",o(s)?"text-white":"text-gray-900"])}," 登录 ",2)]),e("form",{class:"mt-8 space-y-6",onSubmit:k(f,["prevent"])},[r[5]||(r[5]=e("input",{type:"hidden",name:"remember",value:"true"},null,-1)),e("div",L,[e("div",null,[r[3]||(r[3]=e("label",{for:"password",class:"sr-only"},"密码",-1)),S(e("input",{id:"password",name:"password",type:"password",autocomplete:"current-password",required:"","onUpdate:modelValue":r[0]||(r[0]=g=>t.value=g),class:l(["appearance-none rounded-t-md relative block w-full px-4 py-3 border transition-all duration-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm backdrop-blur-sm",o(s)?"bg-gray-800/50 border-gray-600 text-white placeholder-gray-400 hover:border-gray-500":"bg-white/50 border-gray-300 text-gray-900 hover:border-gray-400"]),placeholder:"密码"},null,2),[[A,t.value]])])]),e("div",null,[e("button",{type:"submit",class:l(["group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-md text-white transition-all duration-300 transform hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 shadow-lg hover:shadow-cyan-500/50",o(s)?"bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-600 hover:to-purple-600":"bg-gradient-to-r from-cyan-600 to-purple-600 hover:from-cyan-700 hover:to-purple-700",i.value?"opacity-75 cursor-not-allowed":""]),disabled:i.value},[r[4]||(r[4]=e("span",{class:"absolute left-0 inset-y-0 flex items-center pl-3"},null,-1)),V(" "+B(i.value?"登录中...":"登录"),1)],10,N)])],32)],2)],2))}}),E=_(T,[["__scopeId","data-v-2e50c3fa"]]);export{E as default};
import{G as y,r as u,B as v,u as b,d as w,e,n as l,g as o,i as x,f as h,h as k,j as S,v as A,m as V,t as B,y as D,z as j,A as P,_}from"./index-CiI2cp0V.js";import{B as z}from"./box-BA8SMkDM.js";const M=y("adminData",()=>{const d=u(localStorage.getItem("adminPassword")||"");function n(t){d.value=t,localStorage.setItem("token",t)}return{adminPassword:d,updateAdminPwd:n}}),I={class:"mx-auto h-16 w-16 relative"},L={class:"rounded-md shadow-sm -space-y-px"},N=["disabled"],T=v({__name:"LoginView",setup(d){const n=b(),t=u(""),i=u(!1),s=x("isDarkMode"),c=M(),p=()=>{let a=!0;return t.value?t.value.length<6&&(n.showAlert("密码长度至少为6位","error"),a=!1):(n.showAlert("无效的密码","error"),a=!1),a},m=D(),f=async()=>{if(p()){c.updateAdminPwd(t.value),P.post("/admin/login",{password:t.value}).then(()=>{m.push("/admin")}).catch(a=>{n.showAlert(a.response.data.detail,"error")}),i.value=!0;try{await new Promise(a=>setTimeout(a,2e3))}catch{}finally{i.value=!1}}};return(a,r)=>(j(),w("div",{class:l(["min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 transition-colors duration-200 relative overflow-hidden",o(s)?"bg-gray-900":"bg-gray-50"])},[r[6]||(r[6]=e("div",{class:"absolute inset-0 z-0"},[e("div",{class:"cyber-grid"}),e("div",{class:"floating-particles"})],-1)),e("div",{class:l(["max-w-md w-full space-y-8 backdrop-blur-lg bg-opacity-20 p-8 rounded-xl border border-opacity-20",[o(s)?"bg-gray-800 border-gray-600":"bg-white/70 border-gray-200"]])},[e("div",null,[e("div",I,[r[1]||(r[1]=e("div",{class:"absolute inset-0 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full animate-spin-slow"},null,-1)),r[2]||(r[2]=e("div",{class:"absolute -inset-2 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full opacity-50 blur-md animate-pulse"},null,-1)),e("div",{class:l(["absolute inset-1 rounded-full flex items-center justify-center",o(s)?"bg-gray-800":"bg-white"])},[h(o(z),{class:l(["h-8 w-8",o(s)?"text-cyan-400":"text-cyan-600"])},null,8,["class"])],2)]),e("h2",{class:l(["mt-6 text-center text-3xl font-extrabold",o(s)?"text-white":"text-gray-900"])}," 登录 ",2)]),e("form",{class:"mt-8 space-y-6",onSubmit:k(f,["prevent"])},[r[5]||(r[5]=e("input",{type:"hidden",name:"remember",value:"true"},null,-1)),e("div",L,[e("div",null,[r[3]||(r[3]=e("label",{for:"password",class:"sr-only"},"密码",-1)),S(e("input",{id:"password",name:"password",type:"password",autocomplete:"current-password",required:"","onUpdate:modelValue":r[0]||(r[0]=g=>t.value=g),class:l(["appearance-none rounded-t-md relative block w-full px-4 py-3 border transition-all duration-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm backdrop-blur-sm",o(s)?"bg-gray-800/50 border-gray-600 text-white placeholder-gray-400 hover:border-gray-500":"bg-white/50 border-gray-300 text-gray-900 hover:border-gray-400"]),placeholder:"密码"},null,2),[[A,t.value]])])]),e("div",null,[e("button",{type:"submit",class:l(["group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-md text-white transition-all duration-300 transform hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 shadow-lg hover:shadow-cyan-500/50",o(s)?"bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-600 hover:to-purple-600":"bg-gradient-to-r from-cyan-600 to-purple-600 hover:from-cyan-700 hover:to-purple-700",i.value?"opacity-75 cursor-not-allowed":""]),disabled:i.value},[r[4]||(r[4]=e("span",{class:"absolute left-0 inset-y-0 flex items-center pl-3"},null,-1)),V(" "+B(i.value?"登录中...":"登录"),1)],10,N)])],32)],2)],2))}}),E=_(T,[["__scopeId","data-v-2e50c3fa"]]);export{E as default};
@@ -1,4 +1,4 @@
var De=Object.defineProperty;var Pe=(d,e,t)=>e in d?De(d,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):d[e]=t;var v=(d,e,t)=>Pe(d,typeof e!="symbol"?e+"":e,t);import{c as _e,_ as Fe,u as Ze,s as Oe,r as D,o as Ue,a as Qe,w as He,b as Ne,d as I,e as p,f as $,n as w,g as f,i as Ve,t as j,h as Ge,j as We,k as Y,v as Xe,l as Q,m as Z,p as Ke,X as xe,T as Je,F as Ye,q as et,x as le,y as tt,z as L,A as nt}from"./index-BIfJi_O-.js";import{c as H,u as st,S as it,C as rt,a as ot,Q as lt,E as at}from"./_commonjsHelpers-CljsJuVy.js";import{B as ct}from"./box-C9xlYTT6.js";import{F as ke}from"./file-AHrKtGwY.js";import{H as ut,T as pt}from"./trash-wtxrZ93m.js";/**
var De=Object.defineProperty;var Pe=(d,e,t)=>e in d?De(d,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):d[e]=t;var v=(d,e,t)=>Pe(d,typeof e!="symbol"?e+"":e,t);import{c as _e,_ as Fe,u as Ze,s as Oe,r as D,o as Ue,a as Qe,w as He,b as Ne,d as I,e as p,f as $,n as w,g as f,i as Ve,t as j,h as Ge,j as We,k as Y,v as Xe,l as Q,m as Z,p as Ke,X as xe,T as Je,F as Ye,q as et,x as le,y as tt,z as L,A as nt}from"./index-CiI2cp0V.js";import{c as H,u as st,S as it,C as rt,a as ot,Q as lt,E as at}from"./_commonjsHelpers-GlvjlJV-.js";import{B as ct}from"./box-BA8SMkDM.js";import{F as ke}from"./file-CWusGEpI.js";import{H as ut,T as pt}from"./trash-dDHutUhM.js";/**
* @license lucide-vue-next v0.445.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as W,B as oe,r as z,o as ne,w as he,d as F,z as D,e as a,_ as se,u as ae,y as ge,i as pe,b as ye,p as ve,f as M,n as f,g as i,t as U,h as Q,l as E,k as N,j as J,v as X,x as K,C as be,F as Y,q as Z,m as V,X as xe,T as me,A as ee}from"./index-BIfJi_O-.js";import{g as we,u as _e,S as Ce,C as Ae,E as Me,a as Se,Q as Be}from"./_commonjsHelpers-CljsJuVy.js";import{F as te}from"./file-AHrKtGwY.js";import{T as Ie,H as Te}from"./trash-wtxrZ93m.js";/**
import{c as W,B as oe,r as z,o as ne,w as he,d as F,z as D,e as a,_ as se,u as ae,y as ge,i as pe,b as ye,p as ve,f as M,n as f,g as i,t as U,h as Q,l as E,k as N,j as J,v as X,x as K,C as be,F as Y,q as Z,m as V,X as xe,T as me,A as ee}from"./index-CiI2cp0V.js";import{g as we,u as _e,S as Ce,C as Ae,E as Me,a as Se,Q as Be}from"./_commonjsHelpers-GlvjlJV-.js";import{F as te}from"./file-CWusGEpI.js";import{T as Ie,H as Te}from"./trash-dDHutUhM.js";/**
* @license lucide-vue-next v0.445.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as k,B as F,D as R,r as _,E as K,o as V,F as X,G as x,H as Y}from"./index-BIfJi_O-.js";/**
import{c as k,B as F,D as R,r as _,E as K,o as V,F as X,G as x,H as Y}from"./index-CiI2cp0V.js";/**
* @license lucide-vue-next v0.445.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-BIfJi_O-.js";/**
import{c as a}from"./index-CiI2cp0V.js";/**
* @license lucide-vue-next v0.445.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-BIfJi_O-.js";/**
import{c as a}from"./index-CiI2cp0V.js";/**
* @license lucide-vue-next v0.445.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as a}from"./index-BIfJi_O-.js";/**
import{c as a}from"./index-CiI2cp0V.js";/**
* @license lucide-vue-next v0.445.0 - ISC
*
* This source code is licensed under the ISC license.
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="keywords" content="{{keywords}}" />
<meta name="generator" content="FileCodeBox2.2" />
<title>{{title}}</title>
<script type="module" crossorigin src="/assets/index-BIfJi_O-.js"></script>
<script type="module" crossorigin src="/assets/index-CiI2cp0V.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bx419AMb.css">
</head>
<body>