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
125 changed files with 1077 additions and 7742 deletions
Showing only changes of commit fad6a280d4 - Show all commits
-2
View File
@@ -1,2 +0,0 @@
custom: ["https://afdian.com/a/filecodebox"]
+17 -14
View File
@@ -18,16 +18,15 @@ def create_token(data: dict, expires_in: int = 3600 * 24) -> str:
:param data: 数据负载
:param expires_in: 过期时间(秒)
"""
header = base64.b64encode(json.dumps({"alg": "HS256", "typ": "JWT"}).encode()).decode()
payload = base64.b64encode(json.dumps({
**data,
"exp": int(time.time()) + expires_in
}).encode()).decode()
header = base64.b64encode(
json.dumps({"alg": "HS256", "typ": "JWT"}).encode()
).decode()
payload = base64.b64encode(
json.dumps({**data, "exp": int(time.time()) + expires_in}).encode()
).decode()
signature = hmac.new(
settings.admin_token.encode(),
f"{header}.{payload}".encode(),
'sha256'
settings.admin_token.encode(), f"{header}.{payload}".encode(), "sha256"
).digest()
signature = base64.b64encode(signature).decode()
@@ -41,13 +40,13 @@ def verify_token(token: str) -> dict:
:return: 解码后的数据
"""
try:
header_b64, payload_b64, signature_b64 = token.split('.')
header_b64, payload_b64, signature_b64 = token.split(".")
# 验证签名
expected_signature = hmac.new(
settings.admin_token.encode(),
f"{header_b64}.{payload_b64}".encode(),
'sha256'
"sha256",
).digest()
expected_signature_b64 = base64.b64encode(expected_signature).decode()
@@ -66,7 +65,9 @@ def verify_token(token: str) -> dict:
raise ValueError(f"token验证失败: {str(e)}")
async def admin_required(authorization: str = Header(default=None), request: Request = None):
async def admin_required(
authorization: str = Header(default=None), request: Request = None
):
"""
验证管理员权限
"""
@@ -81,12 +82,14 @@ async def admin_required(authorization: str = Header(default=None), request: Req
except ValueError as e:
is_admin = False
if request.url.path.startswith('/share/'):
if request.url.path.startswith("/share/"):
if not settings.openUpload and not is_admin:
raise HTTPException(status_code=403, detail='本站未开启游客上传,如需上传请先登录后台')
raise HTTPException(
status_code=403, detail="本站未开启游客上传,如需上传请先登录后台"
)
else:
if not is_admin:
raise HTTPException(status_code=401, detail='未授权或授权校验失败')
raise HTTPException(status_code=401, detail="未授权或授权校验失败")
return is_admin
except ValueError as e:
raise HTTPException(status_code=401, detail=str(e))
+1 -1
View File
@@ -7,7 +7,7 @@ class IDData(BaseModel):
class ShareItem(BaseModel):
expire_value: int
expire_style: str = 'day'
expire_style: str = "day"
filename: str
+39 -22
View File
@@ -19,16 +19,18 @@ class FileService:
await self.file_storage.delete_file(file_code)
await file_code.delete()
async def list_files(self, page: int, size: int, keyword: str = ''):
async def list_files(self, page: int, size: int, keyword: str = ""):
offset = (page - 1) * size
files = await FileCodes.filter(prefix__icontains=keyword).limit(size).offset(offset)
files = (
await FileCodes.filter(prefix__icontains=keyword).limit(size).offset(offset)
)
total = await FileCodes.filter(prefix__icontains=keyword).count()
return files, total
async def download_file(self, file_id: int):
file_code = await FileCodes.filter(id=file_id).first()
if not file_code:
raise HTTPException(status_code=404, detail='文件不存在')
raise HTTPException(status_code=404, detail="文件不存在")
if file_code.text:
return APIResponse(detail=file_code.text)
else:
@@ -37,10 +39,12 @@ class FileService:
async def share_local_file(self, item):
local_file = LocalFileClass(item.filename)
if not await local_file.exists():
raise HTTPException(status_code=404, detail='文件不存在')
raise HTTPException(status_code=404, detail="文件不存在")
text = await local_file.read()
expired_at, expired_count, used_count, code = await get_expire_info(item.expire_value, item.expire_style)
expired_at, expired_count, used_count, code = await get_expire_info(
item.expire_value, item.expire_style
)
path, suffix, prefix, uuid_file_name, save_path = await get_file_path_name(item)
await self.file_storage.save_file(text, save_path)
@@ -58,8 +62,8 @@ class FileService:
)
return {
'code': code,
'name': local_file.file,
"code": code,
"name": local_file.file,
}
@@ -68,21 +72,32 @@ class ConfigService:
return settings.items()
async def update_config(self, data: dict):
admin_token = data.get('admin_token')
if admin_token is None or admin_token == '':
raise HTTPException(status_code=400, detail='管理员密码不能为空')
admin_token = data.get("admin_token")
if admin_token is None or admin_token == "":
raise HTTPException(status_code=400, detail="管理员密码不能为空")
for key, value in data.items():
if key not in settings.default_config:
continue
if key in ['errorCount', 'errorMinute', 'max_save_seconds', 'onedrive_proxy', 'openUpload', 'port', 's3_proxy', 'uploadCount', 'uploadMinute', 'uploadSize']:
if key in [
"errorCount",
"errorMinute",
"max_save_seconds",
"onedrive_proxy",
"openUpload",
"port",
"s3_proxy",
"uploadCount",
"uploadMinute",
"uploadSize",
]:
data[key] = int(value)
elif key in ['opacity']:
elif key in ["opacity"]:
data[key] = float(value)
else:
data[key] = value
await KeyValue.filter(key='settings').update(value=data)
await KeyValue.filter(key="settings").update(value=data)
for k, v in data.items():
settings.__setattr__(k, v)
@@ -90,9 +105,9 @@ class ConfigService:
class LocalFileService:
async def list_files(self):
files = []
if not os.path.exists(data_root / 'local'):
os.makedirs(data_root / 'local')
for file in os.listdir(data_root / 'local'):
if not os.path.exists(data_root / "local"):
os.makedirs(data_root / "local")
for file in os.listdir(data_root / "local"):
files.append(LocalFileClass(file))
return files
@@ -100,22 +115,24 @@ class LocalFileService:
file = LocalFileClass(filename)
if await file.exists():
await file.delete()
return '删除成功'
raise HTTPException(status_code=404, detail='文件不存在')
return "删除成功"
raise HTTPException(status_code=404, detail="文件不存在")
class LocalFileClass:
def __init__(self, file):
self.file = file
self.path = data_root / 'local' / file
self.ctime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(os.path.getctime(self.path)))
self.path = data_root / "local" / file
self.ctime = time.strftime(
"%Y-%m-%d %H:%M:%S", time.localtime(os.path.getctime(self.path))
)
self.size = os.path.getsize(self.path)
async def read(self):
return open(self.path, 'rb')
return open(self.path, "rb")
async def write(self, data):
with open(self.path, 'w') as f:
with open(self.path, "w") as f:
f.write(data)
async def delete(self):
+49 -46
View File
@@ -6,17 +6,22 @@ import datetime
from fastapi import APIRouter, Depends, HTTPException
from apps.admin.services import FileService, ConfigService, LocalFileService
from apps.admin.dependencies import admin_required, get_file_service, get_config_service, get_local_file_service
from apps.admin.dependencies import (
admin_required,
get_file_service,
get_config_service,
get_local_file_service,
)
from apps.admin.schemas import IDData, ShareItem, DeleteItem, LoginData
from core.response import APIResponse
from apps.base.models import FileCodes, KeyValue
from apps.admin.dependencies import create_token
from core.settings import settings
admin_api = APIRouter(prefix='/admin', tags=['管理'])
admin_api = APIRouter(prefix="/admin", tags=["管理"])
@admin_api.post('/login')
@admin_api.post("/login")
async def login(data: LoginData):
# 验证管理员密码
if data.password != settings.admin_token:
@@ -24,17 +29,14 @@ async def login(data: LoginData):
# 生成包含管理员身份的token
token = create_token({"is_admin": True})
return APIResponse(detail={
"token": token,
"token_type": "Bearer"
})
return APIResponse(detail={"token": token, "token_type": "Bearer"})
@admin_api.get('/dashboard')
@admin_api.get("/dashboard")
async def dashboard(admin: bool = Depends(admin_required)):
all_codes = await FileCodes.all()
all_size = str(sum([code.size for code in all_codes]))
sys_start = await KeyValue.filter(key='sys_start').first()
sys_start = await KeyValue.filter(key="sys_start").first()
# 获取当前日期时间
now = datetime.datetime.now()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
@@ -42,104 +44,105 @@ async def dashboard(admin: bool = Depends(admin_required)):
yesterday_end = today_start - datetime.timedelta(microseconds=1)
# 统计昨天一整天的记录数(从昨天0点到23:59:59)
yesterday_codes = FileCodes.filter(
created_at__gte=yesterday_start,
created_at__lte=yesterday_end
created_at__gte=yesterday_start, created_at__lte=yesterday_end
)
# 统计今天到现在的记录数(从今天0点到现在)
today_codes = FileCodes.filter(
created_at__gte=today_start
today_codes = FileCodes.filter(created_at__gte=today_start)
return APIResponse(
detail={
"totalFiles": len(all_codes),
"storageUsed": all_size,
"sysUptime": sys_start.value,
"yesterdayCount": await yesterday_codes.count(),
"yesterdaySize": str(sum([code.size for code in await yesterday_codes])),
"todayCount": await today_codes.count(),
"todaySize": str(sum([code.size for code in await today_codes])),
}
)
return APIResponse(detail={
'totalFiles': len(all_codes),
'storageUsed': all_size,
'sysUptime': sys_start.value,
'yesterdayCount': await yesterday_codes.count(),
'yesterdaySize': str(sum([code.size for code in await yesterday_codes])),
'todayCount': await today_codes.count(),
'todaySize': str(sum([code.size for code in await today_codes])),
})
@admin_api.delete('/file/delete')
@admin_api.delete("/file/delete")
async def file_delete(
data: IDData,
file_service: FileService = Depends(get_file_service),
admin: bool = Depends(admin_required)
admin: bool = Depends(admin_required),
):
await file_service.delete_file(data.id)
return APIResponse()
@admin_api.get('/file/list')
@admin_api.get("/file/list")
async def file_list(
page: int = 1,
size: int = 10,
keyword: str = '',
keyword: str = "",
file_service: FileService = Depends(get_file_service),
admin: bool = Depends(admin_required)
admin: bool = Depends(admin_required),
):
files, total = await file_service.list_files(page, size, keyword)
return APIResponse(detail={
'page': page,
'size': size,
'data': files,
'total': total,
})
return APIResponse(
detail={
"page": page,
"size": size,
"data": files,
"total": total,
}
)
@admin_api.get('/config/get')
@admin_api.get("/config/get")
async def get_config(
config_service: ConfigService = Depends(get_config_service),
admin: bool = Depends(admin_required)
admin: bool = Depends(admin_required),
):
return APIResponse(detail=config_service.get_config())
@admin_api.patch('/config/update')
@admin_api.patch("/config/update")
async def update_config(
data: dict,
config_service: ConfigService = Depends(get_config_service),
admin: bool = Depends(admin_required)
admin: bool = Depends(admin_required),
):
data.pop('themesChoices')
data.pop("themesChoices")
await config_service.update_config(data)
return APIResponse()
@admin_api.get('/file/download')
@admin_api.get("/file/download")
async def file_download(
id: int,
file_service: FileService = Depends(get_file_service),
admin: bool = Depends(admin_required)
admin: bool = Depends(admin_required),
):
file_content = await file_service.download_file(id)
return file_content
@admin_api.get('/local/lists')
@admin_api.get("/local/lists")
async def get_local_lists(
local_file_service: LocalFileService = Depends(get_local_file_service),
admin: bool = Depends(admin_required)
admin: bool = Depends(admin_required),
):
files = await local_file_service.list_files()
return APIResponse(detail=files)
@admin_api.delete('/local/delete')
@admin_api.delete("/local/delete")
async def delete_local_file(
item: DeleteItem,
local_file_service: LocalFileService = Depends(get_local_file_service),
admin: bool = Depends(admin_required)
admin: bool = Depends(admin_required),
):
result = await local_file_service.delete_file(item.filename)
return APIResponse(detail=result)
@admin_api.post('/local/share')
@admin_api.post("/local/share")
async def share_local_file(
item: ShareItem,
file_service: FileService = Depends(get_file_service),
admin: bool = Depends(admin_required)
admin: bool = Depends(admin_required),
):
share_info = await file_service.share_local_file(item)
return APIResponse(detail=share_info)
+16 -8
View File
@@ -12,26 +12,34 @@ class IPRateLimit:
def check_ip(self, ip: str) -> bool:
if ip in self.ips:
ip_info = self.ips[ip]
if ip_info['count'] >= self.count:
if ip_info['time'] + timedelta(minutes=self.minutes) > datetime.now():
if ip_info["count"] >= self.count:
if ip_info["time"] + timedelta(minutes=self.minutes) > datetime.now():
return False
self.ips.pop(ip)
return True
def add_ip(self, ip: str) -> int:
ip_info = self.ips.get(ip, {'count': 0, 'time': datetime.now()})
ip_info['count'] += 1
ip_info['time'] = datetime.now()
ip_info = self.ips.get(ip, {"count": 0, "time": datetime.now()})
ip_info["count"] += 1
ip_info["time"] = datetime.now()
self.ips[ip] = ip_info
return ip_info['count']
return ip_info["count"]
async def remove_expired_ip(self) -> None:
now = datetime.now()
expiration = timedelta(minutes=self.minutes)
self.ips = {ip: info for ip, info in self.ips.items() if info['time'] + expiration >= now}
self.ips = {
ip: info
for ip, info in self.ips.items()
if info["time"] + expiration >= now
}
def __call__(self, request: Request) -> str:
ip = request.headers.get('X-Real-IP') or request.headers.get('X-Forwarded-For') or request.client.host
ip = (
request.headers.get("X-Real-IP")
or request.headers.get("X-Forwarded-For")
or request.client.host
)
if not self.check_ip(ip):
raise HTTPException(status_code=423, detail="请求次数过多,请稍后再试")
return ip
+52
View File
@@ -0,0 +1,52 @@
from tortoise import connections
async def create_file_codes_table():
conn = connections.get("default")
await conn.execute_script(
"""
CREATE TABLE IF NOT EXISTS filecodes
(
id INTEGER not null
primary key autoincrement,
code VARCHAR(255) not null
unique,
prefix VARCHAR(255) default '' not null,
suffix VARCHAR(255) default '' not null,
uuid_file_name VARCHAR(255),
file_path VARCHAR(255),
size INT default 0 not null,
text TEXT,
expired_at TIMESTAMP,
expired_count INT default 0 not null,
used_count INT default 0 not null,
created_at TIMESTAMP default CURRENT_TIMESTAMP not null
);
CREATE INDEX IF NOT EXISTS idx_filecodes_code_1c7ee7
on filecodes (code);
"""
)
async def create_key_value_table():
conn = connections.get("default")
await conn.execute_script(
"""
CREATE TABLE IF NOT EXISTS keyvalue
(
id INTEGER not null
primary key autoincrement,
key VARCHAR(255) not null
unique,
value JSON,
created_at TIMESTAMP default CURRENT_TIMESTAMP not null
);
CREATE INDEX IF NOT EXISTS idx_keyvalue_key_eab890
on keyvalue (key);
"""
)
async def migrate():
await create_file_codes_table()
await create_key_value_table()
+96 -15
View File
@@ -14,17 +14,68 @@ from core.utils import get_now
class FileCodes(Model):
id: Optional[int] = fields.IntField(pk=True)
code: Optional[int] = fields.CharField(description='分享码', max_length=255, index=True, unique=True)
prefix: Optional[str] = fields.CharField(max_length=255, description='前缀', default='')
suffix: Optional[str] = fields.CharField(max_length=255, description='后缀', default='')
uuid_file_name: Optional[str] = fields.CharField(max_length=255, description='uuid文件名', null=True)
file_path: Optional[str] = fields.CharField(max_length=255, description='文件路径', null=True)
size: Optional[int] = fields.IntField(description='文件大小', default=0)
text: Optional[str] = fields.TextField(description='文本内容', null=True)
expired_at: Optional[datetime] = fields.DatetimeField(null=True, description='过期时间')
expired_count: Optional[int] = fields.IntField(description='可用次数', default=0)
used_count: Optional[int] = fields.IntField(description='已用次数', default=0)
created_at: Optional[datetime] = fields.DatetimeField(auto_now_add=True, description='创建时间')
code: Optional[int] = fields.CharField(
description="分享码", max_length=255, index=True, unique=True
)
prefix: Optional[str] = fields.CharField(
max_length=255, description="前缀", default=""
)
suffix: Optional[str] = fields.CharField(
max_length=255, description="后缀", default=""
)
uuid_file_name: Optional[str] = fields.CharField(
max_length=255, description="uuid文件名", null=True
)
file_path: Optional[str] = fields.CharField(
max_length=255, description="文件路径", null=True
)
size: Optional[int] = fields.IntField(description="文件大小", default=0)
text: Optional[str] = fields.TextField(description="文本内容", null=True)
expired_at: Optional[datetime] = fields.DatetimeField(
null=True, description="过期时间"
)
expired_count: Optional[int] = fields.IntField(description="可用次数", default=0)
used_count: Optional[int] = fields.IntField(description="已用次数", default=0)
created_at: Optional[datetime] = fields.DatetimeField(
auto_now_add=True, description="创建时间"
)
#
# file_hash = fields.CharField(
# max_length=128, # SHA-256需要64字符,这里预留扩展空间
# description="文件哈希值",
# unique=True,
# null=True # 允许旧数据为空
# )
# hash_algorithm = fields.CharField(
# max_length=20,
# description="哈希算法类型",
# null=True,
# default="sha256"
# )
# # 新增分片字段
# chunk_size = fields.IntField(
# description="分片大小(字节)",
# default=0
# )
# total_chunks = fields.IntField(
# description="总分片数",
# default=0
# )
# uploaded_chunks = fields.IntField(
# description="已上传分片数",
# default=0
# )
# upload_status = fields.CharField(
# max_length=20,
# description="上传状态",
# default="pending", # pending/in_progress/completed
# choices=["pending", "in_progress", "completed"]
# )
# is_chunked = fields.BooleanField(
# description="是否分片上传",
# default=False
# )
async def is_expired(self):
# 按时间
@@ -40,11 +91,41 @@ class FileCodes(Model):
return f"{self.file_path}/{self.uuid_file_name}"
#
# class FileChunks(Model):
# id = fields.IntField(pk=True)
# file_code = fields.ForeignKeyField(
# "models.FileCodes",
# related_name="chunks",
# on_delete=fields.CASCADE
# )
# chunk_number = fields.IntField(description="分片序号")
# chunk_hash = fields.CharField(
# max_length=128,
# description="分片哈希校验值"
# )
# chunk_path = fields.CharField(
# max_length=255,
# description="分片存储路径"
# )
# created_at = fields.DatetimeField(
# auto_now_add=True,
# description="上传时间"
# )
#
# class Meta:
# unique_together = [("file_code", "chunk_number")]
class KeyValue(Model):
id: Optional[int] = fields.IntField(pk=True)
key: Optional[str] = fields.CharField(max_length=255, description='', index=True, unique=True)
value: Optional[str] = fields.JSONField(description='', null=True)
created_at: Optional[datetime] = fields.DatetimeField(auto_now_add=True, description='创建时间')
key: Optional[str] = fields.CharField(
max_length=255, description="", index=True, unique=True
)
value: Optional[str] = fields.JSONField(description="", null=True)
created_at: Optional[datetime] = fields.DatetimeField(
auto_now_add=True, description="创建时间"
)
file_codes_pydantic = pydantic_model_creator(FileCodes, name='FileCodes')
file_codes_pydantic = pydantic_model_creator(FileCodes, name="FileCodes")
+27 -17
View File
@@ -14,7 +14,7 @@ from core.utils import get_random_num, get_random_string, max_save_times_desc
async def get_file_path_name(file: UploadFile) -> Tuple[str, str, str, str, str]:
"""获取文件路径和文件名"""
today = datetime.datetime.now()
storage_path = settings.storage_path.strip('/') # 移除开头和结尾的斜杠
storage_path = settings.storage_path.strip("/") # 移除开头和结尾的斜杠
file_uuid = uuid.uuid4().hex
# 使用 UUID 作为子目录名
@@ -29,32 +29,42 @@ async def get_file_path_name(file: UploadFile) -> Tuple[str, str, str, str, str]
return path, suffix, prefix, file.filename, save_path
async def get_expire_info(expire_value: int, expire_style: str) -> Tuple[Optional[datetime.datetime], int, int, str]:
async def get_expire_info(
expire_value: int, expire_style: str
) -> Tuple[Optional[datetime.datetime], int, int, str]:
"""获取过期信息"""
expired_count, used_count = -1, 0
now = datetime.datetime.now()
code = None
max_timedelta = datetime.timedelta(seconds=settings.max_save_seconds) if settings.max_save_seconds > 0 else datetime.timedelta(days=7)
detail = await max_save_times_desc(settings.max_save_seconds) if settings.max_save_seconds > 0 else '7天'
detail = f'限制最长时间为 {detail[0]},可换用其他方式'
max_timedelta = (
datetime.timedelta(seconds=settings.max_save_seconds)
if settings.max_save_seconds > 0
else datetime.timedelta(days=7)
)
detail = (
await max_save_times_desc(settings.max_save_seconds)
if settings.max_save_seconds > 0
else "7天"
)
detail = f"限制最长时间为 {detail[0]},可换用其他方式"
expire_styles = {
'day': lambda: now + datetime.timedelta(days=expire_value),
'hour': lambda: now + datetime.timedelta(hours=expire_value),
'minute': lambda: now + datetime.timedelta(minutes=expire_value),
'count': lambda: (now + datetime.timedelta(days=1), expire_value),
'forever': lambda: (None, None), # 修改这里
"day": lambda: now + datetime.timedelta(days=expire_value),
"hour": lambda: now + datetime.timedelta(hours=expire_value),
"minute": lambda: now + datetime.timedelta(minutes=expire_value),
"count": lambda: (now + datetime.timedelta(days=1), expire_value),
"forever": lambda: (None, None), # 修改这里
}
if expire_style in expire_styles:
result = expire_styles[expire_style]()
if isinstance(result, tuple):
expired_at, extra = result
if expire_style == 'count':
if expire_style == "count":
expired_count = extra
elif expire_style == 'forever':
code = await get_random_code(style='string') # 移动到这里
elif expire_style == "forever":
code = await get_random_code(style="string") # 移动到这里
else:
expired_at = result
if expired_at and expired_at - now > max_timedelta:
@@ -68,15 +78,15 @@ async def get_expire_info(expire_value: int, expire_style: str) -> Tuple[Optiona
return expired_at, expired_count, used_count, code
async def get_random_code(style='num') -> str:
async def get_random_code(style="num") -> str:
"""获取随机字符串"""
while True:
code = await get_random_num() if style == 'num' else await get_random_string()
code = await get_random_num() if style == "num" else await get_random_string()
if not await FileCodes.filter(code=code).exists():
return code
ip_limit = {
'error': IPRateLimit(count=settings.uploadCount, minutes=settings.errorMinute),
'upload': IPRateLimit(count=settings.errorCount, minutes=settings.errorMinute)
"error": IPRateLimit(count=settings.uploadCount, minutes=settings.errorMinute),
"upload": IPRateLimit(count=settings.errorCount, minutes=settings.errorMinute),
}
+48 -36
View File
@@ -8,32 +8,36 @@ from core.settings import settings
from core.storage import storages, FileStorageInterface
from core.utils import get_select_token
share_api = APIRouter(prefix='/share', tags=['分享'])
share_api = APIRouter(prefix="/share", tags=["分享"])
async def validate_file_size(file: UploadFile, max_size: int):
if file.size > max_size:
max_size_mb = max_size / (1024 * 1024)
raise HTTPException(status_code=403, detail=f'大小超过限制,最大为{max_size_mb:.2f} MB')
raise HTTPException(
status_code=403, detail=f"大小超过限制,最大为{max_size_mb:.2f} MB"
)
async def create_file_code(code, **kwargs):
return await FileCodes.create(code=code, **kwargs)
@share_api.post('/text/', dependencies=[Depends(admin_required)])
@share_api.post("/text/", dependencies=[Depends(admin_required)])
async def share_text(
text: str = Form(...),
expire_value: int = Form(default=1, gt=0),
expire_style: str = Form(default='day'),
ip: str = Depends(ip_limit['upload'])
expire_style: str = Form(default="day"),
ip: str = Depends(ip_limit["upload"]),
):
text_size = len(text.encode('utf-8'))
text_size = len(text.encode("utf-8"))
max_txt_size = 222 * 1024
if text_size > max_txt_size:
raise HTTPException(status_code=403, detail='内容过多,建议采用文件形式')
raise HTTPException(status_code=403, detail="内容过多,建议采用文件形式")
expired_at, expired_count, used_count, code = await get_expire_info(expire_value, expire_style)
expired_at, expired_count, used_count, code = await get_expire_info(
expire_value, expire_style
)
await create_file_code(
code=code,
text=text,
@@ -41,25 +45,27 @@ async def share_text(
expired_count=expired_count,
used_count=used_count,
size=len(text),
prefix='Text'
prefix="Text",
)
ip_limit['upload'].add_ip(ip)
return APIResponse(detail={'code': code})
ip_limit["upload"].add_ip(ip)
return APIResponse(detail={"code": code})
@share_api.post('/file/', dependencies=[Depends(admin_required)])
@share_api.post("/file/", dependencies=[Depends(admin_required)])
async def share_file(
expire_value: int = Form(default=1, gt=0),
expire_style: str = Form(default='day'),
expire_style: str = Form(default="day"),
file: UploadFile = File(...),
ip: str = Depends(ip_limit['upload'])
ip: str = Depends(ip_limit["upload"]),
):
await validate_file_size(file, settings.uploadSize)
if expire_style not in settings.expireStyle:
raise HTTPException(status_code=400, detail='过期时间类型错误')
raise HTTPException(status_code=400, detail="过期时间类型错误")
expired_at, expired_count, used_count, code = await get_expire_info(expire_value, expire_style)
expired_at, expired_count, used_count, code = await get_expire_info(
expire_value, expire_style
)
path, suffix, prefix, uuid_file_name, save_path = await get_file_path_name(file)
file_storage: FileStorageInterface = storages[settings.file_storage]()
@@ -76,16 +82,16 @@ async def share_file(
expired_count=expired_count,
used_count=used_count,
)
ip_limit['upload'].add_ip(ip)
return APIResponse(detail={'code': code, 'name': file.filename})
ip_limit["upload"].add_ip(ip)
return APIResponse(detail={"code": code, "name": file.filename})
async def get_code_file_by_code(code, check=True):
file_code = await FileCodes.filter(code=code).first()
if not file_code:
return False, '文件不存在'
return False, "文件不存在"
if await file_code.is_expired() and check:
return False, '文件已过期'
return False, "文件已过期"
return True, file_code
@@ -96,44 +102,50 @@ async def update_file_usage(file_code):
await file_code.save()
@share_api.get('/select/')
async def get_code_file(code: str, ip: str = Depends(ip_limit['error'])):
@share_api.get("/select/")
async def get_code_file(code: str, ip: str = Depends(ip_limit["error"])):
file_storage: FileStorageInterface = storages[settings.file_storage]()
has, file_code = await get_code_file_by_code(code)
if not has:
ip_limit['error'].add_ip(ip)
ip_limit["error"].add_ip(ip)
return APIResponse(code=404, detail=file_code)
await update_file_usage(file_code)
return await file_storage.get_file_response(file_code)
@share_api.post('/select/')
async def select_file(data: SelectFileModel, ip: str = Depends(ip_limit['error'])):
@share_api.post("/select/")
async def select_file(data: SelectFileModel, ip: str = Depends(ip_limit["error"])):
file_storage: FileStorageInterface = storages[settings.file_storage]()
has, file_code = await get_code_file_by_code(data.code)
if not has:
ip_limit['error'].add_ip(ip)
ip_limit["error"].add_ip(ip)
return APIResponse(code=404, detail=file_code)
await update_file_usage(file_code)
return APIResponse(detail={
'code': file_code.code,
'name': file_code.prefix + file_code.suffix,
'size': file_code.size,
'text': file_code.text if file_code.text is not None else await file_storage.get_file_url(file_code),
})
return APIResponse(
detail={
"code": file_code.code,
"name": file_code.prefix + file_code.suffix,
"size": file_code.size,
"text": (
file_code.text
if file_code.text is not None
else await file_storage.get_file_url(file_code)
),
}
)
@share_api.get('/download')
async def download_file(key: str, code: str, ip: str = Depends(ip_limit['error'])):
@share_api.get("/download")
async def download_file(key: str, code: str, ip: str = Depends(ip_limit["error"])):
file_storage: FileStorageInterface = storages[settings.file_storage]()
if await get_select_token(code) != key:
ip_limit['error'].add_ip(ip)
ip_limit["error"].add_ip(ip)
has, file_code = await get_code_file_by_code(code, False)
if not has:
return APIResponse(code=404, detail='文件不存在')
return APIResponse(code=404, detail="文件不存在")
return (
APIResponse(detail=file_code.text)
+80
View File
@@ -0,0 +1,80 @@
import glob
import importlib
import os
from tortoise import Tortoise
from core.settings import data_root
async def init_db():
try:
# 使用正确的Tortoise初始化配置格式
db_config = {
"db_url": f"sqlite://{data_root}/filecodebox.db",
"modules": {"models": ["apps.base.models"]},
"use_tz": False,
"timezone": "Asia/Shanghai"
}
await Tortoise.init(**db_config)
# 创建migrations表
await Tortoise.get_connection("default").execute_script("""
CREATE TABLE IF NOT EXISTS migrates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
migration_file VARCHAR(255) NOT NULL UNIQUE,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# 执行迁移
await execute_migrations()
except Exception as e:
print(f"数据库初始化失败: {str(e)}")
raise
async def execute_migrations():
"""执行数据库迁移"""
try:
# 收集迁移文件
migration_files = []
for root, dirs, files in os.walk("apps"):
if "migrations" in dirs:
migration_path = os.path.join(root, "migrations")
migration_files.extend(glob.glob(os.path.join(migration_path, "migrations_*.py")))
# 按文件名排序
migration_files.sort()
for migration_file in migration_files:
file_name = os.path.basename(migration_file)
# 检查是否已执行
executed = await Tortoise.get_connection("default").execute_query(
"SELECT id FROM migrates WHERE migration_file = ?", [file_name]
)
if not executed[1]:
print(f"执行迁移: {file_name}")
# 导入并执行migration
module_path = migration_file.replace("/", ".").replace("\\", ".").replace(".py", "")
try:
migration_module = importlib.import_module(module_path)
if hasattr(migration_module, "migrate"):
await migration_module.migrate()
# 记录执行
await Tortoise.get_connection("default").execute_query(
"INSERT INTO migrates (migration_file) VALUES (?)",
[file_name]
)
print(f"迁移完成: {file_name}")
except Exception as e:
print(f"迁移 {file_name} 执行失败: {str(e)}")
raise
except Exception as e:
print(f"迁移过程发生错误: {str(e)}")
raise
+2 -2
View File
@@ -6,10 +6,10 @@ from typing import Generic, TypeVar
from pydantic.v1.generics import GenericModel
T = TypeVar('T')
T = TypeVar("T")
class APIResponse(GenericModel, Generic[T]):
code: int = 200
message: str = 'ok'
message: str = "ok"
detail: T
+59 -57
View File
@@ -5,70 +5,70 @@
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
data_root = BASE_DIR / 'data'
data_root = BASE_DIR / "data"
if not data_root.exists():
data_root.mkdir(parents=True, exist_ok=True)
DEFAULT_CONFIG = {
'file_storage': 'local',
'storage_path': '',
'name': '文件快递柜 - FileCodeBox',
'description': '开箱即用的文件快传系统',
'notify_title': '系统通知',
'notify_content': '欢迎使用 FileCodeBox,本程序开源于 <a href="https://github.com/vastsa/FileCodeBox" target="_blank">Github</a> ,欢迎Star和Fork。',
'page_explain': '请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。',
'keywords': 'FileCodeBox, 文件快递柜, 口令传送箱, 匿名口令分享文本, 文件',
's3_access_key_id': '',
's3_secret_access_key': '',
's3_bucket_name': '',
's3_endpoint_url': '',
's3_region_name': 'auto',
's3_signature_version': 's3v2',
's3_hostname': '',
's3_proxy': 0,
'max_save_seconds': 0,
'aws_session_token': '',
'onedrive_domain': '',
'onedrive_client_id': '',
'onedrive_username': '',
'onedrive_password': '',
'onedrive_root_path': 'filebox_storage',
'onedrive_proxy': 0,
'webdav_hostname': '',
'webdav_root_path': 'filebox_storage',
'webdav_proxy': 0,
'admin_token': 'FileCodeBox2023',
'openUpload': 1,
'uploadSize': 1024 * 1024 * 10,
'expireStyle': ['day', 'hour', 'minute', 'forever', 'count'],
'uploadMinute': 1,
'webdav_url': '',
'webdav_password': '',
'webdav_username': '',
'opacity': 0.9,
'background': '',
'uploadCount': 10,
'themesChoices': [
"file_storage": "local",
"storage_path": "",
"name": "文件快递柜 - FileCodeBox",
"description": "开箱即用的文件快传系统",
"notify_title": "系统通知",
"notify_content": '欢迎使用 FileCodeBox,本程序开源于 <a href="https://github.com/vastsa/FileCodeBox" target="_blank">Github</a> ,欢迎Star和Fork。',
"page_explain": "请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。",
"keywords": "FileCodeBox, 文件快递柜, 口令传送箱, 匿名口令分享文本, 文件",
"s3_access_key_id": "",
"s3_secret_access_key": "",
"s3_bucket_name": "",
"s3_endpoint_url": "",
"s3_region_name": "auto",
"s3_signature_version": "s3v2",
"s3_hostname": "",
"s3_proxy": 0,
"max_save_seconds": 0,
"aws_session_token": "",
"onedrive_domain": "",
"onedrive_client_id": "",
"onedrive_username": "",
"onedrive_password": "",
"onedrive_root_path": "filebox_storage",
"onedrive_proxy": 0,
"webdav_hostname": "",
"webdav_root_path": "filebox_storage",
"webdav_proxy": 0,
"admin_token": "FileCodeBox2023",
"openUpload": 1,
"uploadSize": 1024 * 1024 * 10,
"expireStyle": ["day", "hour", "minute", "forever", "count"],
"uploadMinute": 1,
"webdav_url": "",
"webdav_password": "",
"webdav_username": "",
"opacity": 0.9,
"background": "",
"uploadCount": 10,
"themesChoices": [
{
'name': '2023',
'key': 'themes/2023',
'author': 'Lan',
'version': '1.0',
"name": "2023",
"key": "themes/2023",
"author": "Lan",
"version": "1.0",
},
{
'name': '2024',
'key': 'themes/2024',
'author': 'Lan',
'version': '1.0',
}
"name": "2024",
"key": "themes/2024",
"author": "Lan",
"version": "1.0",
},
],
'themesSelect': 'themes/2024',
'errorMinute': 1,
'errorCount': 1,
'port': 12345,
'showAdminAddr': 0,
'robotsText': 'User-agent: *\nDisallow: /',
"themesSelect": "themes/2024",
"errorMinute": 1,
"errorCount": 1,
"port": 12345,
"showAdminAddr": 0,
"robotsText": "User-agent: *\nDisallow: /",
}
@@ -82,10 +82,12 @@ class Settings:
return self.user_config[attr]
if attr in self.default_config:
return self.default_config[attr]
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attr}'")
raise AttributeError(
f"'{self.__class__.__name__}' object has no attribute '{attr}'"
)
def __setattr__(self, key, value):
if key in ['default_config', 'user_config']:
if key in ["default_config", "user_config"]:
super().__setattr__(key, value)
else:
self.user_config[key] = value
+8 -1
View File
@@ -92,7 +92,14 @@ class SystemFileStorage(FileStorageInterface):
file_path = self.root_path / await file_code.get_file_path()
if not file_path.exists():
return APIResponse(code=404, detail="文件已过期删除")
return FileResponse(file_path, filename=file_code.prefix + file_code.suffix)
filename = f"{file_code.prefix}{file_code.suffix}"
encoded_filename = quote(filename, safe='')
content_disposition = f"attachment; filename*=UTF-8''{encoded_filename}"
return FileResponse(
file_path,
headers={"Content-Disposition": content_disposition},
filename=filename # 保留原始文件名以备某些场景使用
)
class S3FileStorage(FileStorageInterface):
+6 -4
View File
@@ -19,13 +19,15 @@ async def delete_expire_files():
while True:
try:
# 遍历 share目录下的所有文件夹,删除空的文件夹,并判断父目录是否为空,如果为空也删除
if settings.file_storage == 'local':
if settings.file_storage == "local":
for root, dirs, files in os.walk(f"{data_root}/share/data"):
if not dirs and not files:
os.rmdir(root)
await ip_limit['error'].remove_expired_ip()
await ip_limit['upload'].remove_expired_ip()
expire_data = await FileCodes.filter(Q(expired_at__lt=await get_now()) | Q(expired_count=0)).all()
await ip_limit["error"].remove_expired_ip()
await ip_limit["upload"].remove_expired_ip()
expire_data = await FileCodes.filter(
Q(expired_at__lt=await get_now()) | Q(expired_count=0)
).all()
for exp in expire_data:
await file_storage.delete_file(exp)
await exp.delete()
+22 -21
View File
@@ -9,6 +9,7 @@ import string
import time
from core.settings import settings
async def get_random_num():
"""
获取随机数
@@ -25,7 +26,7 @@ async def get_random_string():
获取随机字符串
:return:
"""
return ''.join(random.choice(r_s) for _ in range(5))
return "".join(random.choice(r_s) for _ in range(5))
async def get_now():
@@ -33,9 +34,7 @@ async def get_now():
获取当前时间
:return:
"""
return datetime.datetime.now(
datetime.timezone(datetime.timedelta(hours=8))
)
return datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8)))
async def get_select_token(code: str):
@@ -45,7 +44,9 @@ async def get_select_token(code: str):
:return:
"""
token = settings.admin_token
return hashlib.sha256(f"{code}{int(time.time() / 1000)}000{token}".encode()).hexdigest()
return hashlib.sha256(
f"{code}{int(time.time() / 1000)}000{token}".encode()
).hexdigest()
async def get_file_url(code: str):
@@ -54,7 +55,7 @@ async def get_file_url(code: str):
:param code:
:return:
"""
return f'/share/download?key={await get_select_token(code)}&code={code}'
return f"/share/download?key={await get_select_token(code)}&code={code}"
async def max_save_times_desc(max_save_seconds: int):
@@ -66,28 +67,28 @@ async def max_save_times_desc(max_save_seconds: int):
def gen_desc_zh(value: int, desc: str):
if value > 0:
return f'{value}{desc}'
return f"{value}{desc}"
else:
return ''
return ""
def gen_desc_en(value: int, desc: str):
if value > 0:
ret = f'{value} {desc}'
ret = f"{value} {desc}"
if value > 1:
ret += 's'
ret += ' '
ret += "s"
ret += " "
return ret
else:
return ''
return ""
max_timedelta = datetime.timedelta(seconds=max_save_seconds)
desc_zh, desc_en = '最长保存时间:', 'Max save time: '
desc_zh += gen_desc_zh(max_timedelta.days, '')
desc_en += gen_desc_en(max_timedelta.days, 'day')
desc_zh += gen_desc_zh(max_timedelta.seconds // 3600, '小时')
desc_en += gen_desc_en(max_timedelta.seconds // 3600, 'hour')
desc_zh += gen_desc_zh(max_timedelta.seconds % 3600 // 60, '分钟')
desc_en += gen_desc_en(max_timedelta.seconds % 3600 // 60, 'minute')
desc_zh += gen_desc_zh(max_timedelta.seconds % 60, '')
desc_en += gen_desc_en(max_timedelta.seconds % 60, 'second')
desc_zh, desc_en = "最长保存时间:", "Max save time: "
desc_zh += gen_desc_zh(max_timedelta.days, "")
desc_en += gen_desc_en(max_timedelta.days, "day")
desc_zh += gen_desc_zh(max_timedelta.seconds // 3600, "小时")
desc_en += gen_desc_en(max_timedelta.seconds // 3600, "hour")
desc_zh += gen_desc_zh(max_timedelta.seconds % 3600 // 60, "分钟")
desc_en += gen_desc_en(max_timedelta.seconds % 3600 // 60, "minute")
desc_zh += gen_desc_zh(max_timedelta.seconds % 60, "")
desc_en += gen_desc_en(max_timedelta.seconds % 60, "second")
return desc_zh, desc_en
-15
View File
@@ -1,15 +0,0 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
}
}
-27
View File
@@ -1,27 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.vite/
node_modules
.DS_Store
dist-ssr
coverage
*.local
.git
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-8
View File
@@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}
-9
View File
@@ -1,9 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
}
-46
View File
@@ -1,46 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
CardTools: typeof import('./src/components/CardTools.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
ElUpload: typeof import('element-plus/es')['ElUpload']
FileBox: typeof import('./src/components/FileBox.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
UploadFile: typeof import('./src/components/UploadFile.vue')['default']
UploadText: typeof import('./src/components/UploadText.vue')['default']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}
-1
View File
@@ -1 +0,0 @@
/// <reference types="vite/client" />
-16
View File
@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/assets/logo_small.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
<meta name="description" content="{{description}}">
<meta name="keywords" content="{{keywords}}" />
<meta name="generator" content="FileCodeBox2.1" />
<title>{{title}}</title>
</head>
<body style="background: url('{{background}}') no-repeat center center fixed;background-size: cover;">
<div id="app" style="opacity: {{opacity}}"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
-49
View File
@@ -1,49 +0,0 @@
{
"name": "fcb-fronted",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"@traptitech/markdown-it-katex": "^3.6.0",
"@types/markdown-it": "^14.1.2",
"@types/markdown-it-link-attributes": "^3.0.5",
"axios": "^1.7.2",
"element-plus": "^2.7.8",
"github-markdown-css": "^5.6.1",
"highlight.js": "^11.10.0",
"markdown-it": "^14.1.0",
"markdown-it-link-attributes": "^4.0.1",
"pinia": "^2.2.0",
"qrcode.vue": "^3.4.1",
"sass": "^1.77.8",
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.3",
"vue": "^3.4.34",
"vue-i18n": "^9.13.1",
"vue-router": "^4.4.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.10.4",
"@tsconfig/node18": "^18.2.4",
"@types/node": "^22.0.0",
"@vitejs/plugin-vue": "^5.1.1",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^9.11.1",
"eslint-plugin-vue": "^9.28.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"typescript": "~5.5.4",
"vite": "^5.3.5",
"vue-tsc": "^2.0.29"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

-33
View File
@@ -1,33 +0,0 @@
<script setup lang="ts">
import { onMounted } from "vue";
import { request } from "@/utils/request";
import { ElNotification } from 'element-plus'
onMounted(() => {
request({
url:'/',
method:'post'
}).then((res:any)=>{
if (res.code === 200) {
localStorage.setItem('config', JSON.stringify(res.detail));
if (res.detail.notify_title && res.detail.notify_content && localStorage.getItem('notify') !== res.detail.notify_title + res.detail.notify_content) {
localStorage.setItem('notify', res.detail.notify_title + res.detail.notify_content);
ElNotification({
title: res.detail.notify_title,
dangerouslyUseHTMLString: true,
message: res.detail.notify_content,
type: 'success'
});
}
}
})
});
</script>
<template>
<div>
<RouterView />
</div>
</template>
<style scoped></style>
-86
View File
@@ -1,86 +0,0 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f5f5f5;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #212121;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
}
html{
background: var(--vt-c-white-soft);
}
html.dark{
background: var(--vt-c-black-soft);
}
body {
color: var(--color-text);
display: flex;
justify-content: center;
align-items: center;
line-height: 1.6;
font-family: Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
File diff suppressed because it is too large Load Diff
-206
View File
@@ -1,206 +0,0 @@
html.dark {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px
}
.hljs {
color: #abb2bf;
background: #282c34
}
.hljs-keyword,
.hljs-operator,
.hljs-pattern-match {
color: #f92672
}
.hljs-function,
.hljs-pattern-match .hljs-constructor {
color: #61aeee
}
.hljs-function .hljs-params {
color: #a6e22e
}
.hljs-function .hljs-params .hljs-typing {
color: #fd971f
}
.hljs-module-access .hljs-module {
color: #7e57c2
}
.hljs-constructor {
color: #e2b93d
}
.hljs-constructor .hljs-string {
color: #9ccc65
}
.hljs-comment,
.hljs-quote {
color: #b18eb1;
font-style: italic
}
.hljs-doctag,
.hljs-formula {
color: #c678dd
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e06c75
}
.hljs-literal {
color: #56b6c2
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #98c379
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #e6c07b
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #d19a66
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #61aeee
}
.hljs-emphasis {
font-style: italic
}
.hljs-strong {
font-weight: 700
}
.hljs-link {
text-decoration: underline
}
}
html {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px;
&::-webkit-scrollbar {
height: 4px;
}
}
.hljs {
color: #383a42;
background: #fafafa
}
.hljs-comment,
.hljs-quote {
color: #a0a1a7;
font-style: italic
}
.hljs-doctag,
.hljs-formula,
.hljs-keyword {
color: #a626a4
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e45649
}
.hljs-literal {
color: #0184bb
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #50a14f
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #986801
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #4078f2
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #c18401
}
.hljs-emphasis {
font-style: italic
}
.hljs-strong {
font-weight: 700
}
.hljs-link {
text-decoration: underline
}
}
-100
View File
@@ -1,100 +0,0 @@
@import 'katex/dist/katex.min.css';
@import '@/assets/code/highlight.scss';
@import '@/assets/code/github-markdown.scss';
@import '@/assets/code/markdown-add.scss';
.markdown-body {
box-sizing: border-box;
background:none ;
*{
font-size: 0.9rem;
}
}
@media (max-width: 767px) {
.markdown-body {
padding: 15px;
}
}
.chat-container {
display: flex;
flex-direction: column;
margin: auto;
max-width: 1080px;
padding: 0 5px;
height: 100%;
}
.chat-messages {
flex-grow: 1;
overflow-y: auto;
border-radius: 10px 10px 0 0;
padding: 8px;
}
.chat-message {
display: flex;
flex-direction: column;
margin-bottom: 30px;
}
.chat-message-bubble {
padding: 8px;
align-self: flex-start;
color: #2f4f4f;
border-radius: 8px 8px 8px 0;
background: linear-gradient(to right, #c9d6ff, #e2e2e2); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
box-shadow: 0 16px 20px 0 rgba(174, 167, 223, .06);
max-width: 80%;
}
.chat-toolbar-icon{
margin-left: 5px;
cursor: pointer;
}
.chat-message-meta {
display: flex;
position: relative;
margin-top: 4px;
}
.n-input-wrapper {
padding-right: 0;
}
.chat-message-nickname-and-time {
color: #999;
font-size: 12px;
}
.is-me {
justify-content: flex-end;
}
.n-input .n-input__suffix, .n-input .n-input__prefix {
display: block;
}
.n-input .n-input-wrapper {
padding-right: 0;
}
.n-input .n-input__suffix .n-button {
border-radius: 8px !important;
width: 80px;
height: 30px;
position: absolute; right: 10px; bottom: 10px;
box-shadow: inset 0 1px 3px 0 rgba(0, 0, 0, 0.3);
background-image: linear-gradient(-56deg, #0773ff 5%, #797eff);
--n-border: none !important;
}
.chat-message.is-me .chat-message-bubble {
background: linear-gradient(to right, #56ccf2, #2f80ed); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
border-radius: 8px 8px 0 !important;
box-shadow: 0 16px 20px 0 rgba(174, 167, 223, .06);
align-self: flex-end;
}
@@ -1,75 +0,0 @@
.markdown-body {
background-color: transparent;
font-size: 14px;
p {
white-space: pre-wrap;
}
ol {
list-style-type: decimal;
}
ul {
list-style-type: disc;
}
pre code,
pre tt {
line-height: 1.65;
}
// .highlight pre,
// pre {
// background-color: #fff;
// }
code.hljs {
padding: 0;
}
.code-block {
&-wrapper {
position: relative;
padding-top: 24px;
}
&-header {
position: absolute;
top: 5px;
right: 0;
width: 100%;
padding: 0 1rem;
display: flex;
justify-content: flex-end;
align-items: center;
color: #b3b3b3;
&__copy {
cursor: pointer;
margin-left: 0.5rem;
user-select: none;
&:hover {
color: #65a665;
}
}
}
}
}
html.dark {
.message-reply {
.whitespace-pre-wrap {
white-space: pre-wrap;
color: var(--n-text-color);
}
}
.highlight pre,
pre {
background-color: #282c34;
}
}
-54
View File
@@ -1,54 +0,0 @@
@import './base.css';
#app {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 100vh;
}
.card {
max-width: 400px;
min-width: 398px;
height: 100%;
background-color: #F8FBFE;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
border-radius: 20px !important;
position: relative;
}
.tools {
display: flex;
z-index: 999;
align-items: center;
}
.box {
display: inline-block;
align-items: center;
width: 1.5rem;
height: 1.5rem;
user-select: none;
cursor: pointer;
padding: 1px;
border-radius: 50%;
}
.circle {
padding: 0 4px;
}
.red {
padding: 3px;
background-color: #ff605c;
}
.yellow {
padding: 3px;
background-color: #ffbd44;
}
.green {
background-color: #00ca4e;
padding: 3px;
}
-39
View File
@@ -1,39 +0,0 @@
<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'
import { useRoute, useRouter } from "vue-router";
const router = useRouter()
const route = useRoute()
const isDark = useDark()
const toggleDark = useToggle(isDark)
import { Moon, Sunny, Upload, Back, TakeawayBox } from "@element-plus/icons-vue";
import FileBox from "@/components/FileBox.vue";
import { useFileBoxStore } from "@/stores/fileBox";
const fileBoxStore = useFileBoxStore();
</script>
<template>
<div class="tools">
<div class="circle">
<el-icon size="17" color="#212121" class="red box" @click="router.push({'name':route.name=='home'?'send':'home'})">
<Back v-if="route.name=='send'"></Back>
<Upload v-else></Upload>
</el-icon>
</div>
<div class="circle">
<el-icon size="17" color="#212121" class="yellow box" @click="toggleDark(!isDark)">
<Moon v-if="isDark"></Moon>
<Sunny v-else></Sunny>
</el-icon>
</div>
<div class="circle">
<el-icon size="17" color="#212121" class="green box" @click="fileBoxStore.showFileBox=true">
<TakeawayBox></TakeawayBox>
</el-icon>
</div>
<file-box></file-box>
</div>
</template>
<style scoped lang="scss">
</style>
-151
View File
@@ -1,151 +0,0 @@
<script setup lang="ts">
import { useFileDataStore } from "@/stores/fileData";
import { useFileBoxStore } from "@/stores/fileBox";
import hljs from 'highlight.js';
import mila from 'markdown-it-link-attributes';
import markdownIt from 'markdown-it';
import markdownItKatex from '@traptitech/markdown-it-katex';
import '@/assets/code/main.scss'
const fileStore = useFileDataStore();
const fileBoxStore = useFileBoxStore();
import QrcodeVue from "qrcode.vue";
import { useRoute } from "vue-router";
import { ref } from "vue";
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
import { ElMessage } from "element-plus";
const openUrl = (url: string) => {
if (url.startsWith('/')) {
url = window.location.origin + url;
}
window.open(url);
};
const route = useRoute();
const copyText = (text: any, style = 0) => {
ElMessage.success(t('fileBox.copySuccess'));
if (style === 1) {
text = window.location.origin + '/#/?code=' + text;
}
const temp: any = document.createElement('textarea');
temp.value = text;
document.body.appendChild(temp);
temp.select();
if (document.execCommand('copy')) {
document.execCommand('copy');
}
document.body.removeChild(temp);
};
const nowText = ref('');
const showTextDetail = (text: any) => {
showTextDetailVisible.value = true;
nowText.value = text;
};
const showTextDetailVisible = ref(false);
const md = new markdownIt({
html: false,
linkify: true,
highlight(code: any, language: any) {
const validLang = Boolean(language && hljs.getLanguage(language));
return `<pre class="code-block-wrapper"><code class="hljs code-block-body ${validLang}">${hljs.highlight(code, { language: language ?? '' }).value}</code></pre>`;
}
});
md.use(mila, { attrs: { target: '_blank', rel: 'noopener' } });
md.use(markdownItKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' });
function renderMarkdown(message: string) {
return md.render(message);
}
function getQrCodeUrl(code: string) {
// 否是接收的文件,如果是则返回下载直链,否则拼接分享链接
if (code) {
return code.toString().startsWith('/share/download') ? window.location.origin + code : window.location.origin + '/#/?code=' + code;
}
else {
return '';
}
}
</script>
<template>
<el-drawer :append-to-body="true" v-model="fileBoxStore.showFileBox" direction="btt" style="max-width: 1080px;margin: auto;"
size="400">
<template #header>
<h4 v-if="route.name=='home'">{{t('fileBox.receiveFileBox')}}</h4>
<h4 v-else>{{t('fileBox.sendFileBox')}}</h4>
</template>
<template #default>
<div v-if="route.name=='home'" style="display: flex;flex-wrap: wrap;justify-content: center">
<el-dialog
append-to-body
align-center
:title="t('fileBox.textDetail')"
width="70%"
style="height: 70%;overflow-y: scroll"
v-model="showTextDetailVisible"
>
<div style="max-width: 100%;overflow-y: scroll" v-html="renderMarkdown(nowText)"></div>
<template #footer>
<el-button type="success" @click="copyText(nowText);showTextDetailVisible = false">{{t('fileBox.copy')}}</el-button>
<el-button type="primary" @click="showTextDetailVisible = false">{{ t('fileBox.close') }}</el-button>
</template>
</el-dialog>
<el-card v-for="(value,index) in fileStore.receiveData" :key="index" style="margin: 0.5rem">
<template #header>
<div style="display: flex;justify-content: space-between">
<h4 style="width: 6rem;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">{{ value.name }}</h4>
<el-button size="small" type="danger" @click="fileStore.deleteReceiveData(index)">{{ t('fileBox.delete') }}</el-button>
</div>
</template>
<div style="width: 200px;">
<div style="display: flex;justify-content: space-between">
<!-- 下载二维码白色边框 -->
<div v-if="value.name!=='Text'"
style="padding: 5px; background-color: white; border: 2px solid white; box-sizing: border-box; height: 114px;">
<qrcode-vue :value="getQrCodeUrl(value.text)" :size="100"></qrcode-vue>
</div>
<div v-else style="width: 100px;height: 100px;flex-wrap: wrap;overflow-y:scroll ">{{value.text}}</div>
<div style="display: flex;flex-direction: column;justify-content: space-around">
<el-tag size="large" style="cursor: pointer" @click="copyText(value.code)">{{ value.code }}</el-tag>
<el-tag v-if="value.name!=='Text'" size="large" type="success" style="cursor: pointer" @click="openUrl(value.text);">
{{ t('fileBox.download') }}
</el-tag>
<el-tag v-else size="large" type="success" style="cursor: pointer" @click="showTextDetail(value.text);">{{ t('fileBox.detail') }}</el-tag>
</div>
</div>
</div>
</el-card>
</div>
<div v-else style="display: flex;flex-wrap: wrap;justify-content: center">
<el-card v-for="(value,index) in fileStore.shareData" :key="index" style="margin: 0.5rem">
<template #header>
<div style="display: flex;justify-content: space-between">
<h4 style="width: 6rem;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">{{ value.name }}</h4>
<el-button size="small" type="danger" @click="fileStore.deleteShareData(index)">{{ t('fileBox.delete') }}</el-button>
</div>
</template>
<div style="width: 200px;">
<el-progress v-if="value.status!='success' && value.status!='fail'" striped :percentage="value.percentage" :text-inside="true"
:stroke-width="20"></el-progress>
<div v-else style="display: flex;justify-content: space-between">
<!-- 分享二维码白色边框 -->
<div style="padding: 5px; background-color: white; border: 2px solid white; box-sizing: border-box; height: 114px;">
<qrcode-vue :value="getQrCodeUrl(value.code)" :size="100"></qrcode-vue>
</div>
<div style="display: flex;flex-direction: column;justify-content: space-around">
<el-tag size="large" style="cursor: pointer" @click="copyText(value.code)">{{ value.code }}</el-tag>
<el-tag size="large" type="success" style="cursor: pointer" @click="copyText(value.code,1);">{{ t('fileBox.copyLink') }}
</el-tag>
</div>
</div>
</div>
</el-card>
</div>
</template>
</el-drawer>
</template>
-183
View File
@@ -1,183 +0,0 @@
<script setup lang="ts">
import { UploadFilled } from '@element-plus/icons-vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { request } from "@/utils/request";
import { useFileDataStore } from "@/stores/fileData";
import { useFileBoxStore } from "@/stores/fileBox";
import { useI18n } from 'vue-i18n'
import { useConfigStore } from "@/stores/config";
import { ElMessage } from "element-plus";
const { config } = useConfigStore();
const { t } = useI18n()
const fileBoxStore = useFileBoxStore();
const fileStore = useFileDataStore();
const props = defineProps({
shareData: {
type: Object,
default: () => {
return {
expire_value: 1,
expire_style: 'day',
}
}
}
})
const fileList: any = ref([])
const uploadBox: any = ref(null)
const handleOnChangeFileList = (file: any) => {
fileStore.addShareData({
'name': file.name,
'text': '',
'status': file.status,
'percentage': 0,
'size': file.size,
'type': file.raw.type,
'uid': file.uid,
});
};
const handleHttpRequest = (options: any) => {
fileBoxStore.showFileBox = true;
const formData = new FormData();
if (config.openUpload === 0 && localStorage.getItem('adminPassword') === null) {
fileStore.shareData.forEach((file: any) => {
if (file.uid === options.file.uid) {
ElMessage.error(t('msg.uploadClose'));
file.status = 'fail';
file.code = t('msg.fileUploadFail');
fileStore.save();
}
});
return;
}
if (options.file.size > config.uploadSize) {
fileStore.shareData.forEach((file: any) => {
if (file.uid === options.file.uid) {
ElMessage.error(t('msg.fileOverSize'));
file.status = 'fail';
file.code = t('msg.fileOverSize');
fileStore.save();
}
});
return;
}
formData.append('file', options.file);
formData.append('expire_value', props.shareData.expireValue);
formData.append('expire_style', props.shareData.expireStyle);
request(
{
url: "share/file/",
method: "post",
data: formData,
onUploadProgress: (event: any) => {
const percentage = Math.round((event.loaded * 100) / event.total) || 0;
fileStore.shareData.forEach((file: any) => {
if (file.uid === options.file.uid) {
file.percentage = percentage;
fileStore.save();
}
});
}
}
).then((res: any) => {
const data = res.detail;
fileStore.shareData.forEach((file: any) => {
if (file.uid === options.file.uid) {
file.status = 'success';
file.text = data.text;
file.code = data.code;
ElMessage.success(t('msg.fileUploadSuccess'));
fileStore.save();
}
});
}).catch(() => {
fileStore.shareData.forEach((file: any) => {
if (file.uid === options.file.uid) {
file.status = 'fail';
file.code = t('msg.fileUploadFail');
ElMessage.error(t('msg.fileUploadFail'));
fileStore.save();
}
});
});
};
function pasteLister(event: any) {
const items = event.clipboardData && event.clipboardData.items;
if (items && items.length) {
for (let i = 0; i < items.length; i++) {
if (items[i].kind === 'string') {
if (items[i].type.match(/^text\/plain/)) {
items[i].getAsString(function(str:any) {
console.log(str);
});
}
} else {
const file: any = items[i].getAsFile();
if (file) {
const uid = Date.now();
file.uid = uid;
fileStore.addShareData({
'name': file.name,
'text': '',
'status': 'ready',
'percentage': 0,
'size': file.size,
'type': file.type,
'uid': uid,
});
handleHttpRequest({
file: file,
})
}
}
}
}
}
onUnmounted(()=>{
// 清除剪切板事件
document.removeEventListener('paste', pasteLister);
})
onMounted(()=>{
document.addEventListener('paste', pasteLister);
})
</script>
<template>
<div>
<el-upload
class="upload-demo"
drag
multiple
:show-file-list="false"
ref="uploadBox"
v-model:file-list="fileList"
:on-change="handleOnChangeFileList"
:http-request="handleHttpRequest"
>
<el-icon class="el-icon--upload">
<upload-filled/>
</el-icon>
<div class="el-upload__text">
{{t('send.prompt1')}}<em>{{t('send.clickUpload')}}</em>
</div>
<div class="el-upload__text" style="font-size: 10px;">{{t('send.prompt2')}}</div>
<template #tip>
<div class="el-upload__tip">
</div>
</template>
</el-upload>
</div>
</template>
<style lang="scss">
.el-upload {
border-radius: 20px;
}
.el-upload-dragger {
box-shadow: 3px 3px 0 0 rgba(0, 0, 0, 0.2);
border-radius: 20px;
}
</style>
-75
View File
@@ -1,75 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { request } from "@/utils/request";
const shareText = ref('')
import { useFileDataStore } from "@/stores/fileData";
import { useFileBoxStore } from "@/stores/fileBox";
import { ElMessage } from "element-plus";
import { useI18n } from 'vue-i18n'
import { useConfigStore } from "@/stores/config";
const { t } = useI18n()
const {config} = useConfigStore();
const fileBoxStore = useFileBoxStore();
const fileStore = useFileDataStore();
const props = defineProps({
shareData: {
type: Object,
default: () => {
return {
expire_value: 1,
expire_style: 'day',
}
}
}
})
const handleSubmitShareText = ()=>{
if (shareText.value === '') {
ElMessage.warning(t('send.prompt3'));
} else if(config.openUpload === 0 && localStorage.getItem('adminPassword') === null){
ElMessage.error(t('msg.uploadClose'));
} else {
const formData = new FormData();
formData.append('text', shareText.value);
formData.append('expire_value', props.shareData.expireValue);
formData.append('expire_style', props.shareData.expireStyle);
request({
'url': 'share/text/',
'method': 'post',
'data': formData,
}).then((res: any) => {
const data = res.detail;
fileBoxStore.showFileBox = true;
fileStore.addShareData({
'name': t('send.textShare'),
'text': data.text,
'code': data.code,
'status': 'success',
'percentage': 100,
'size': shareText.value.length,
'type': 'text',
'uid': Date.now(),
})
});
}
}
</script>
<template>
<div style="position: relative">
<el-input
:placeholder="t('send.prompt3')"
v-model="shareText"
type="textarea"
:rows="9"
:input-style="{'border-radius':'20px','border':'1px dashed var(--el-border-color)','box-shadow':'none'}"
>
</el-input>
<el-button @click="handleSubmitShareText" style="position: absolute;right: 0;bottom: 0;border-radius: 20px 0 20px 0;margin: 1px;background: rgba(255,255,255,0.2)" size="large">{{t('send.share')}}</el-button>
</div>
</template>
<style scoped lang="scss">
</style>
-140
View File
@@ -1,140 +0,0 @@
export default {
send: {
disclaimers: 'Terms and Conditions',
prompt1: 'Drag and drop text or files here, or ',
prompt2: 'Days <7 or limited number of times (deleted after 24h)',
prompt3: 'Please enter the text you want to send, supports MarkDown format',
share: 'Share',
textShare: 'Text Share',
clickUpload: 'Click to Upload',
pleaseInputExpireValue: 'Please enter the validity period',
expireStyle: 'Expiration Method',
expireData: {
day: 'Days',
hour: 'Hours',
forever: 'Permanent',
minute: 'Minutes',
count: 'Times'
},
expireValue: {
day: 'Days',
hour: 'Hours',
minute: 'Minutes',
count: 'Times'
},
fileType: {
file: 'File',
text: 'Text'
}
},
fileBox: {
copySuccess: 'Copy Successful',
inputNotEmpty: 'Please enter a five-digit retrieval code',
sendFileBox: 'Outgoing Box',
ok: 'OK',
receiveFileBox: 'Incoming Box',
textDetail: 'Text Details',
copy: 'Copy',
close: 'Close',
delete: 'Delete',
download: 'Click to Download',
detail: 'View Details',
copyLink: 'Copy Link',
},
admin: {
about: {
source1: 'This project is open-source on Github: ',
source2: 'FileCodeBox'
},
settings: {
name: 'Website Name',
description: 'Website Description',
uploadlimit: 'Upload Limit',
explain: 'Interface Instructions',
errorlimit: 'Error Limit',
keywords: 'Keywords',
themeSelect: 'Theme Selection',
notify_title: 'Notification Title',
notify_content: 'Notification Content',
background: 'Background Image',
max_save_seconds: 'Maximum Save Time',
maxSaveSecondsNote: 'Maximum save time, unit: (seconds), default is 0 which means 7 days',
admin_token: 'Admin Password',
uploadSize: 'File Size',
expireStyle: 'Expiration Method',
uploadSizeNote: 'Maximum file size, unit: (Bytes), 1mb = 1 * 1024 * 1024',
showAdmin: {
open: 'Open',
close: 'Close',
note: 'Is the backend entrance displayed at the bottom of the page?',
},
openUpload: {
title: 'Enable Upload',
open: 'Enable Guest Upload',
close: 'Disable Guest Upload',
note: 'After disabling, you need to log in to the backend to upload',
},
file_storage: {
title: 'Storage Engine',
local: 'Local Storage',
s3: 'S3 Storage',
note: 'After updating, you need to restart FileCodeBox',
},
mei: 'Every',
minute: 'Minutes',
upload: 'Upload',
files: 'Files',
allow: 'Allow',
errors: 'Errors',
save: 'Save',
saveSuccess: 'Save Successful',
},
fileView: {
code: 'Retrieval Code',
prefix: 'File Prefix',
suffix: 'File Suffix',
text: 'Text',
used_count: 'Times Used',
expired_count: 'Available Times',
size: 'File Size',
expired_at: 'Expiration Time',
file_path: 'File Path',
action: 'Action',
delete: 'Delete',
delete_success: 'Delete Successful',
forever: 'Permanent',
unlimited_count: 'Unlimited Times',
download: 'Download',
download_fail: 'File save failed, please try again later~',
},
menu: {
fileManage: 'File Management',
systemSetting: 'System Settings',
about: 'About Us',
color: 'Color Mode',
send: 'Send',
local:'Local File',
receive: 'Receive',
signout: 'Sign Out',
},
login: {
managePassword: 'Admin Password',
passwordNotEmpty: 'Password cannot be empty',
login: 'Login',
loginSuccess: 'Login Successful',
loginError: 'Login Failed',
},
local: {
Name: '文件',
Expire: '过期',
Cancel: '取消',
Confirm: '确定',
}
},
msg: {
fileOverSize: 'File Too Large',
fileUploadFail: 'Upload Failed',
fileUploadSuccess: 'Upload Successful',
uploadClose: 'Guest upload is closed on this site',
}
};
-140
View File
@@ -1,140 +0,0 @@
export default {
send: {
disclaimers: 'Términos y condiciones',
prompt1: 'Arrastre, pegue el texto, archivos aquí, o ',
prompt2: 'Días<7 o límite de veces (elimina después de 24h)',
prompt3: 'Ingrese el texto que desea enviar, admite formato MarkDown',
share: 'Compartir',
textShare: 'Compartir texto',
clickUpload: 'Hacer clic para subir',
pleaseInputExpireValue: 'Ingrese el período de validez',
expireStyle: 'Método de caducidad',
expireData: {
day: 'Días',
hour: 'Horas',
forever: 'Para siempre',
minute: 'Minutos',
count: 'Veces'
},
expireValue: {
day: 'Días',
hour: 'Horas',
minute: 'Minutos',
count: 'Veces'
},
fileType: {
file: 'Archivo',
text: 'Texto'
}
},
fileBox: {
copySuccess: 'Copiado exitosamente',
inputNotEmpty: 'Ingrese un código de recogida de cinco dígitos',
sendFileBox: 'Buzón de envíos',
ok: 'Aceptar',
receiveFileBox: 'Buzón de recepción',
textDetail: 'Detalles del texto',
copy: 'Copiar',
close: 'Cerrar',
delete: 'Eliminar',
download: 'Hacer clic para descargar',
detail: 'Ver detalles',
copyLink: 'Copiar enlace',
},
admin: {
about: {
source1: 'Proyecto de código abierto en Github:',
source2: 'FileCodeBox'
},
settings: {
name: 'Nombre del sitio web',
description: 'Descripción del sitio web',
uploadlimit: 'Límite de subida',
explain: 'Descripción de la interfaz',
errorlimit: 'Límite de errores',
keywords: 'Palabras clave',
themeSelect: 'Selección de tema',
notify_title: 'Título de notificación',
notify_content: 'Contenido de notificación',
background: 'Imagen de fondo',
max_save_seconds: 'Tiempo máximo de almacenamiento',
maxSaveSecondsNote: 'Tiempo máximo de almacenamiento, en segundos. El valor predeterminado es 0, que equivale a 7 días',
admin_token: 'Contraseña de administración',
uploadSize: 'Tamaño de archivo',
expireStyle: 'Método de caducidad',
uploadSizeNote: 'Tamaño máximo de archivo, en bytes. 1mb=1 * 1024 * 1024',
openUpload: {
title: 'Habilitar subida',
open: 'Habilitar subida para visitantes',
close: 'Deshabilitar subida para visitantes',
note: 'Después de deshabilitar, es necesario iniciar sesión en el backend para poder subir archivos',
},
showAdmin: {
open: 'abierto',
close: 'cierre',
note: 'si mostrar la entrada de fondo en la parte inferior de la página',
},
file_storage: {
title: 'Motor de almacenamiento',
local: 'Almacenamiento local',
s3: 'Almacenamiento S3',
note: 'Es necesario reiniciar FileCodeBox después de actualizar',
},
mei: 'Cada',
minute: 'Minuto',
upload: 'Subir',
files: 'Archivos',
allow: 'Permitir',
errors: 'Errores',
save: 'Guardar',
saveSuccess: 'Guardado exitosamente',
},
fileView: {
code: 'Código de recogida',
prefix: 'Prefijo de archivo',
suffix: 'Sufijo de archivo',
text: 'Texto',
used_count: 'Número de veces utilizado',
expired_count: 'Número de veces disponible',
size: 'Tamaño de archivo',
expired_at: 'Fecha de caducidad',
file_path: 'Ruta del archivo',
action: 'Acción',
delete: 'Eliminar',
delete_success: 'Eliminado exitosamente',
forever: 'Válido para siempre',
unlimited_count: 'Número de veces ilimitado',
download: 'Descargar',
download_fail: 'Error al guardar el archivo, intente nuevamente más tarde~',
},
menu: {
fileManage: 'Gestión de archivos',
systemSetting: 'Configuración del sistema',
about: 'Sobre nosotros',
color: 'Modo de color',
send: 'Enviar',
local:'Archivo Local',
receive: 'Recibir',
signout: 'Cerrar sesión',
},
login: {
managePassword: 'Contraseña de administración',
passwordNotEmpty: 'La contraseña no puede estar vacía',
login: 'Iniciar sesión',
loginSuccess: 'Inicio de sesión exitoso',
loginError: 'Error de inicio de sesión',
},
local: {
Name: '文件',
Expire: '过期',
Cancel: '取消',
Confirm: '确定',
}
},
msg: {
fileOverSize: 'Archivo demasiado grande',
fileUploadFail: 'Error al subir',
fileUploadSuccess: 'Subida exitosa',
uploadClose: 'El sitio ha deshabilitado la subida para visitantes',
}
};
-18
View File
@@ -1,18 +0,0 @@
import { createI18n } from 'vue-i18n' //引入vue-i18n组件
import messages from './index'
let language = (
navigator.language || 'zh_cn'
).toLowerCase().replace(/-/, '_');
if (['zh','zh_cn','en','es','zh_tw'].indexOf(language) === -1) {
language = 'zh_cn'
}
const lang = (localStorage.getItem('language') || language);
const i18n = createI18n({
silentTranslationWarn: true,
globalInjection: true,
legacy: false,
locale: lang,
messages,
});
export default i18n
-13
View File
@@ -1,13 +0,0 @@
import zh_cn from './zh_cn';
import zh from './zh';
import es from './es';
import en from './en';
import zh_tw from './zh_tw'
export default {
zh_cn,
en,
zh_tw,
zh,
es
}
-140
View File
@@ -1,140 +0,0 @@
export default {
send: {
disclaimers:'使用须知',
prompt1: '将文字、文件拖、粘贴到此处,或 ',
prompt2: '天数<7或限制次数(24h后删除)',
prompt3: '请输入您要寄出的文本,支持MarkDown格式',
share: '分享',
textShare: '文本分享',
clickUpload: '点击上传',
pleaseInputExpireValue: '请输入有效期',
expireStyle: '过期方式',
expireData: {
day: '天数',
hour: '小时',
forever: '永久',
minute: '分钟',
count: '次数'
},
expireValue: {
day: '天',
hour: '时',
minute: '分',
count: '次'
},
fileType: {
file: '文件',
text: '文本'
}
},
fileBox: {
copySuccess: '复制成功',
inputNotEmpty: '请输入五位取件码',
sendFileBox: '寄件箱',
ok:'确定',
receiveFileBox: '收件箱',
textDetail: '文本详情',
copy: '复 制',
close: '关 闭',
delete: '删 除',
download: '点 击 下 载',
detail: '查 看 详 情',
copyLink: '复制链接',
},
admin: {
about: {
source1: '本项目开源于Github',
source2: 'FileCodeBox'
},
settings: {
name: '网站名称',
description: '网站描述',
uploadlimit: '上传限制',
explain:'界面说明',
errorlimit: '错误限制',
keywords: '关键词',
themeSelect: '主题选择',
notify_title: '通知标题',
notify_content: '通知内容',
background: '背景图片',
max_save_seconds: '最长保存',
maxSaveSecondsNote: '最长保存时间,单位:(秒),默认为0则为7天',
admin_token: '管理密码',
uploadSize: '文件大小',
expireStyle: '过期方式',
uploadSizeNote: '最大文件大小,单位:Bytes),1mb=1 * 1024 * 1024',
openUpload: {
title: '开启上传',
open: '开启游客上传',
close: '关闭游客上传',
note: '关闭之后需要登录后台方可上传',
},
showAdmin:{
open: '开启',
close: '关闭',
note: '是否在页面底部显示后台入口',
},
file_storage: {
title: '存储引擎',
local: '本地存储',
s3: 'S3存储',
note: '更新后需要重启FileCodeBox',
},
mei: '每',
minute: '分钟',
upload: '上传',
files: '个文件',
allow: '允许',
errors: '次错误',
save: '保存',
saveSuccess: '保存成功',
},
fileView: {
code: '取件码',
prefix: '文件前缀',
suffix: '文件后缀',
text: '文本',
used_count: '已使用次数',
expired_count: '可用次数',
size: '文件大小',
expired_at: '过期时间',
file_path: '文件路径',
action: '操作',
delete: '删除',
delete_success: '删除成功',
forever: '永久有效',
unlimited_count: '不限次数',
download: '下载',
download_fail: '文件保存失败,请稍后再试~',
},
menu: {
fileManage: '文件管理',
systemSetting: '系统设置',
about: '关于我们',
color: '颜色模式',
local:'本地文件',
send: '寄件',
receive: '收件',
signout: '退出登录',
},
login: {
managePassword: '管理密码',
passwordNotEmpty: '密码不能为空',
login: '登 录',
loginSuccess: '登录成功',
loginError: '登录失败',
},
local: {
Name: '文件',
Expire: '过期',
Cancel: '取消',
Confirm: '确定',
}
},
msg:{
fileOverSize: '文件过大',
fileUploadFail: '上传失败',
fileUploadSuccess: '上传成功',
uploadClose: '本站已关闭游客上传',
}
};
-142
View File
@@ -1,142 +0,0 @@
export default {
send: {
disclaimers:'使用须知',
prompt1: '将文字、文件拖、粘贴到此处,或 ',
prompt2: '天数<7或限制次数(24h后删除)',
prompt3: '请输入您要寄出的文本,支持MarkDown格式',
share: '分享',
textShare: '文本分享',
clickUpload: '点击上传',
pleaseInputExpireValue: '请输入有效期',
expireStyle: '过期方式',
expireData: {
day: '天数',
hour: '小时',
forever: '永久',
minute: '分钟',
count: '次数'
},
expireValue: {
day: '天',
hour: '时',
minute: '分',
count: '次'
},
fileType: {
file: '文件',
text: '文本'
}
},
fileBox: {
copySuccess: '复制成功',
inputNotEmpty: '请输入五位取件码',
sendFileBox: '寄件箱',
ok:'确定',
receiveFileBox: '收件箱',
textDetail: '文本详情',
copy: '复 制',
close: '关 闭',
delete: '删 除',
download: '点 击 下 载',
detail: '查 看 详 情',
copyLink: '复制链接',
},
admin: {
about: {
source1: '本项目开源于Github',
source2: 'FileCodeBox'
},
settings: {
name: '网站名称',
description: '网站描述',
uploadlimit: '上传限制',
explain:'界面说明',
errorlimit: '错误限制',
keywords: '关键词',
themeSelect: '主题选择',
notify_title: '通知标题',
notify_content: '通知内容',
background: '背景图片',
max_save_seconds: '最长保存',
maxSaveSecondsNote: '最长保存时间,单位:(秒),默认为0则为7天',
admin_token: '管理密码',
uploadSize: '文件大小',
expireStyle: '过期方式',
showAdminAddr: '显示后台',
showAdmin:{
open: '开启',
close: '关闭',
note: '是否在页面底部显示后台入口',
},
robotsText: 'robots.txt',
uploadSizeNote: '最大文件大小,单位:Bytes),1mb=1 * 1024 * 1024',
openUpload: {
title: '开启上传',
open: '开启游客上传',
close: '关闭游客上传',
note: '关闭之后需要登录后台方可上传',
},
file_storage: {
title: '存储引擎',
local: '本地存储',
s3: 'S3存储',
note: '更新后需要重启FileCodeBox',
},
mei: '每',
minute: '分钟',
upload: '上传',
files: '个文件',
allow: '允许',
errors: '次错误',
save: '保存',
saveSuccess: '保存成功',
},
fileView: {
code: '取件码',
prefix: '文件前缀',
suffix: '文件后缀',
text: '文本',
used_count: '已使用次数',
expired_count: '可用次数',
size: '文件大小',
expired_at: '过期时间',
file_path: '文件路径',
action: '操作',
delete: '删除',
delete_success: '删除成功',
forever: '永久有效',
unlimited_count: '不限次数',
download: '下载',
download_fail: '文件保存失败,请稍后再试~',
},
menu: {
fileManage: '文件管理',
systemSetting: '系统设置',
about: '关于我们',
local:'本地文件',
color: '颜色模式',
send: '寄件',
receive: '收件',
signout: '退出登录',
},
login: {
managePassword: '管理密码',
passwordNotEmpty: '密码不能为空',
login: '登 录',
loginSuccess: '登录成功',
loginError: '登录失败',
},
local: {
Name: '文件',
Expire: '过期',
Cancel: '取消',
Confirm: '确定',
}
},
msg:{
fileOverSize: '文件过大',
fileUploadFail: '上传失败',
fileUploadSuccess: '上传成功',
uploadClose: '本站已关闭游客上传',
}
};
-139
View File
@@ -1,139 +0,0 @@
export default {
send: {
disclaimers:'使用須知',
prompt1: '將文字、文件拖、貼到此處,或 ',
prompt2: '天數<7或限制次數(24h後刪除)',
prompt3: '請輸入您要寄出的文本,支持MarkDown格式',
share: '分享',
textShare: '文本分享',
clickUpload: '點擊上傳',
pleaseInputExpireValue: '請輸入有效期',
expireStyle: '過期方式',
expireData: {
day: '天數',
hour: '小時',
forever: '永久',
minute: '分鐘',
count: '次數'
},
expireValue: {
day: '天',
hour: '時',
minute: '分',
count: '次'
},
fileType: {
file: '文件',
text: '文本'
}
},
fileBox: {
copySuccess: '複製成功',
inputNotEmpty: '請輸入五位取件碼',
sendFileBox: '寄件箱',
ok:'確定',
receiveFileBox: '收件箱',
textDetail: '文本詳情',
copy: '複 製',
close: '關 閉',
delete: '刪 除',
download: '點 擊 下 載',
detail: '查 看 詳 情',
copyLink: '複製連結',
},
admin: {
about: {
source1: '本項目開源於Github',
source2: 'FileCodeBox'
},
settings: {
name: '網站名稱',
description: '網站描述',
uploadlimit: '上傳限制',
explain: '界面說明',
errorlimit: '錯誤限制',
keywords: '關鍵詞',
themeSelect: 'Theme選擇',
notify_title: '通知標題',
notify_content: '通知內容',
background: '背景圖片',
max_save_seconds: '最長保存',
maxSaveSecondsNote: '最長保存時間,單位:(秒),默認為0則為7天',
admin_token: '管理密碼',
uploadSize: '文件大小',
expireStyle: '過期方式',
uploadSizeNote: '最大文件大小,單位:Bytes),1mb=1 * 1024 * 1024',
showAdmin: {
open: '開啟',
close: '關閉',
note: '是否在頁面底部顯示後臺入口',
},
openUpload: {
title: '開啟上傳',
open: '開啟遊客上傳',
close: '關閉遊客上傳',
note: '關閉之後需要登錄後臺方可上傳',
},
file_storage: {
title: '存儲引擎',
local: '本地存儲',
s3: 'S3存儲',
note: '更新後需要重啟FileCodeBox',
},
mei: '每',
minute: '分鐘',
upload: '上傳',
files: '個文件',
allow: '允許',
errors: '次錯誤',
save: '保存',
saveSuccess: '保存成功',
},
fileView: {
code: '取件碼',
prefix: '文件前綴',
suffix: '文件後綴',
text: '文本',
used_count: '已使用次數',
expired_count: '可用次數',
size: '文件大小',
expired_at: '過期時間',
file_path: '文件路徑',
action: '操作',
delete: '刪除',
delete_success: '刪除成功',
forever: '永久有效',
unlimited_count: '不限次數',
download: '下載',
download_fail: '文件保存失敗,請稍後再試~',
},
menu: {
fileManage: '文件管理',
systemSetting: '系統設置',
about: '關於我們',
color: '顏色模式',
send: '寄件',
local:'本地文件',
receive: '收件',
signout: '退出登錄',
},
login: {
managePassword: '管理密碼',
passwordNotEmpty: '密碼不能為空',
login: '登 錄',
loginSuccess: '登錄成功',
loginError: '登錄失敗',
},local: {
Name: '文件',
Expire: '過期',
Cancel: '取消',
Confirm: '確定',
},
},
msg:{
fileOverSize: '文件過大',
fileUploadFail: '上傳失敗',
fileUploadSuccess: '上傳成功',
uploadClose: '本站已關閉遊客上傳',
}
};
-15
View File
@@ -1,15 +0,0 @@
import './assets/main.css'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import i18n from "./locals/i18n";
const app = createApp(App)
app.use(createPinia())
app.use(i18n)
app.use(router)
app.mount('#app')
-46
View File
@@ -1,46 +0,0 @@
import { createRouter, createWebHashHistory } from 'vue-router';
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('@/views/Share/HomeView.vue'),
},
{
path: '/send',
name: 'send',
component: () => import('@/views/Share/SendView.vue'),
},
{
path: '/admin',
name: 'admin',
component: () => import('@/views/Admin/AdminView.vue'),
children:[
{
path: '',
name: 'file',
component: () => import('@/views/Admin/FileView.vue'),
},
{
path: 'setting',
name: 'setting',
component: () => import('@/views/Admin/SettingView.vue'),
},
{
path: 'local',
name: 'local',
component: () => import('@/views/Admin/LocalView.vue'),
},
{
path: 'about',
name: 'about',
component: () => import('@/views/Admin/AboutView.vue'),
},
]
}
],
});
export default router;
-13
View File
@@ -1,13 +0,0 @@
import { defineStore } from 'pinia';
import {ref} from "vue";
export const useAdminData = defineStore('adminData', () => {
const adminPassword = ref(localStorage.getItem('adminPassword') || '');
const isAdmin = ref(!!localStorage.getItem('adminPassword'));
function updateAdminPwd(pwd: string) {
adminPassword.value = pwd;
isAdmin.value = true;
localStorage.setItem('adminPassword', pwd);
}
return { adminPassword,updateAdminPwd,isAdmin };
});
-7
View File
@@ -1,7 +0,0 @@
import { defineStore } from 'pinia';
import {ref} from "vue";
export const useConfigStore = defineStore('config', () => {
const config:any = ref(JSON.parse(localStorage.getItem('config') || '{}') || {}); // 配置
return { config };
});
-7
View File
@@ -1,7 +0,0 @@
import { defineStore } from 'pinia';
import {ref} from "vue";
export const useFileBoxStore = defineStore('fileBox', () => {
const showFileBox = ref(false);
return { showFileBox };
});
-31
View File
@@ -1,31 +0,0 @@
import { defineStore } from 'pinia';
import {reactive} from "vue";
export const useFileDataStore = defineStore('fileData', () => {
const receiveData = reactive(JSON.parse(localStorage.getItem('receiveData')||'[]') || []); // 接收的数据
const shareData = reactive(JSON.parse(localStorage.getItem('shareData')||'[]') || []); // 接收的数据
function save() {
localStorage.setItem('receiveData', JSON.stringify(receiveData));
localStorage.setItem('shareData', JSON.stringify(shareData));
}
function addReceiveData(data:any) {
receiveData.unshift(data);
save();
}
function addShareData(data:any) {
shareData.unshift(data);
save();
}
function deleteReceiveData(index: number) {
receiveData.splice(index, 1);
save();
}
function deleteShareData(index: number) {
shareData.splice(index, 1);
save();
}
return { receiveData, shareData, save, addShareData, addReceiveData, deleteReceiveData, deleteShareData };
});
-32
View File
@@ -1,32 +0,0 @@
// @ts-ignore
import axios from "axios";
import { ElMessage } from "element-plus";
const instance = axios.create({
baseURL: import.meta.env.DEV ? "http://localhost:12345" : "/",
timeout: 6000000,
});
instance.interceptors.request.use(
(config: any) => {
config.headers= {
'Authorization': 'Bearer '+ localStorage.getItem('adminPassword') || '',
}
return config;
});
instance.interceptors.response.use(
(response:any) => {
if (response.status === 200 && response.config.url === '/admin/file/download') {
return response;
}
if (response.data.code === 200) {
return response.data;
} else {
ElMessage.error(response.data.detail);
return Promise.reject(response.data);
}
}, (error:any) => {
localStorage.clear()
ElMessage.error(error.response.data.detail);
return Promise.reject(error);
});
export const request = instance;
-10
View File
@@ -1,10 +0,0 @@
export function formatTimestamp(timestamp: string): string {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
-15
View File
@@ -1,15 +0,0 @@
<template>
<main style="text-align: center">
<span>{{ t('admin.about.source1') }}
<em>
<a style="color: #333" href="https://github.com/vastsa/FileCodeBox">{{ t('admin.about.source2') }}</a>
</em>
</span>
</main>
</template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
-92
View File
@@ -1,92 +0,0 @@
<template>
<el-container v-if="adminData.isAdmin" style="height: 100vh;width: 100vw;position: relative;user-select: none">
<el-header>
<el-menu mode="horizontal" router :default-active="route.path">
<el-menu-item v-for="menu in menus" :index="menu.path" :key="menu.path">{{menu.name}}</el-menu-item>
<el-menu-item style="float: right" @click="toggleDark(!isDark)">{{ t('admin.menu.color') }}</el-menu-item>
<el-menu-item style="float: right" @click="logout()">{{ t('admin.menu.signout') }}</el-menu-item>
</el-menu>
</el-header>
<el-main>
<router-view/>
</el-main>
</el-container>
<el-form size="large" v-else>
<el-form-item :label="t('admin.login.managePassword')">
<el-input
v-model="adminData.adminPassword"
:placeholder="t('admin.login.passwordNotEmpty')"
type="password"
>
<template #append>
<el-button @click="refreshLoginStatus">{{ t('admin.login.login') }}</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core';
import { ref } from "vue";
const isDark = useDark()
const toggleDark = useToggle(isDark)
import { useRoute } from 'vue-router';
import { useAdminData } from "@/stores/adminData";
import { request } from "@/utils/request";
import { ElMessage } from "element-plus";
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const adminData = useAdminData();
const route = useRoute();
const menus = ref([
{
name: t('admin.menu.fileManage'),
path: '/admin',
},
{
name: t('admin.menu.systemSetting'),
path: '/admin/setting',
},
{
name: t('admin.menu.local'),
path: '/admin/local',
},
{
name: t('admin.menu.about'),
path: '/admin/about',
},
{
name: t('admin.menu.send'),
path: '/#/send',
},
{
name: t('admin.menu.receive'),
path: '/#/',
},
]);
const refreshLoginStatus = () => {
request({
url: '/admin/login',
method: 'post',
data: {
password: adminData.adminPassword,
},
}).then((res: any) => {
if (res.code === 200) {
adminData.updateAdminPwd(res.detail.token);
ElMessage.success(t('admin.login.loginSuccess'));
} else {
localStorage.clear();
ElMessage.error(t('admin.login.loginError'));
}
});
};
const logout = () => {
localStorage.clear();
};
</script>
<style lang="scss" scoped>
</style>
-147
View File
@@ -1,147 +0,0 @@
<template>
<main>
<el-table size="large" stripe :data="tableData" style="width: 100%">
<el-table-column prop="code" :label="t('admin.fileView.code')" />
<el-table-column prop="prefix" :label="t('admin.fileView.prefix')" />
<el-table-column prop="suffix" :label="t('admin.fileView.suffix')" />
<el-table-column prop="text" :label="t('admin.fileView.text')">
<template #default="scope">
<span style="width: 6rem;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">{{ scope.row.text }}</span>
</template>
</el-table-column>
<el-table-column prop="used_count" :label="t('admin.fileView.used_count')" />
<el-table-column prop="expired_count" :label="t('admin.fileView.expired_count')">
<template #default="scope">
<span>{{ scope.row.expired_count > -1 ? scope.row.expired_count : t('admin.fileView.unlimited_count') }}</span>
</template>
</el-table-column>
<el-table-column prop="size" :label="t('admin.fileView.size')">
<template #default="scope">
<span>{{ Math.round(scope.row.size/1024/1024*100)/100 }}MB</span>
</template>
</el-table-column>
<el-table-column prop="expired_at" :label="t('admin.fileView.expired_at')">
<template #default="scope">
<span>{{ scope.row.expired_at ? formatTimestamp(scope.row.expired_at) : t('admin.fileView.forever') }}</span>
</template>
</el-table-column>
<el-table-column prop="file_path" :label="t('admin.fileView.file_path')" />
<el-table-column>
<template #header>
{{ t('admin.fileView.action')}}
</template>
<template #default="scope">
<el-button type="danger" size="small" @click="deleteFile(scope.row.id)">{{ t('admin.fileView.delete') }}</el-button>
<el-button type="success" size="small" @click="downloadFile(scope.row.id)" v-if="scope.row.file_path">{{ t('admin.fileView.download') }}</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination @size-change="updatePageSize" @current-change="updateCurrentPage" background layout="prev, pager, next" :page-size="params.size" :page-count="Math.round(params.total/params.size)" :total="params.total" />
</main>
</template>
<script lang="ts" setup>
import { request } from "@/utils/request";
import { formatTimestamp } from "@/utils/timestamp-format"
import { ref } from "vue";
import { ElMessage } from "element-plus";
const tableData = ref([]);
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const params = ref({
page: 1,
size: 10,
total: 0,
});
const updateCurrentPage = (currentPage: number) => {
params.value.page = currentPage;
refreshData();
};
const deleteFile = (id: number) => {
request({
url: '/admin/file/delete',
method: 'delete',
data: {
id,
},
}).then(() => {
ElMessage.success(t('admin.fileView.delete_success'));
refreshData();
});
};
const downloadFile = (id: number) => {
request({
url: '/admin/file/download',
method: 'get',
params: {
id,
},
responseType: 'blob'
}).then((response: any) => {
const contentDisposition = response.headers['content-disposition'];
let filename = 'file';
// 使用正则表达式来提取文件名
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (filenameMatch != null && filenameMatch[1]) {
// 去除文件名周围的引号
filename = filenameMatch[1].replace(/['"]/g, '');
}
// @ts-ignore
if (window.showSaveFilePicker)
saveFileByWebApi(response.data, filename).catch(() => {
// ElMessage.error('文件保存函数不支持您的浏览器');
saveFileByElementA(response.data, filename).catch(() => {
ElMessage.error(t('admin.fileView.download_fail'));
})
})
else
saveFileByElementA(response.data, filename).catch(() => {
ElMessage.error(t('admin.fileView.download_fail'));
})
});
};
async function saveFileByElementA(fileBlob: Blob, filename: string) {
const downloadUrl = window.URL.createObjectURL(fileBlob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = filename; // 设置下载文件名
document.body.appendChild(link);
link.click();
// 清理并释放URL对象
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(link);
}
async function saveFileByWebApi(fileBlob: Blob, filename: string) {
// 创建一个新句柄。
// @ts-ignore
const newHandle = await window.showSaveFilePicker({
suggestedName: filename,
});
// 创建一个 FileSystemWritableFileStream 用于写入。
const writableStream = await newHandle.createWritable();
// 写入我们的文件。
await writableStream.write(fileBlob);
// 关闭文件并将内容写入磁盘。
await writableStream.close();
}
const updatePageSize=(pageSize: number) => {
params.value.size = pageSize;
refreshData();
};
const refreshData=() => {
request({
url: '/admin/file/list',
method: 'get',
params: params.value,
}).then((res:any) => {
tableData.value = res.detail.data;
params.value.total = res.detail.total;
});
}
refreshData();
</script>
-155
View File
@@ -1,155 +0,0 @@
<template>
<div class="file-list">
<el-empty style="width: 90vw;" v-if="localFiles.length===0" description="请在/opt/filecodebox/local目录上传您需要分享的文件" />
<el-card v-for="file in localFiles" :key="file.name" class="file-card" shadow="hover">
<div class="file-info">
<div class="file-name">{{ file.file }}</div>
<div class="file-date">{{ file.ctime }}</div>
<div style="width: 100%;text-align: right">
<el-button type="primary" style="margin-top: 1rem" @click="shareLocalFile(file)" plain>分享</el-button>
<el-button type="danger" style="margin-top: 1rem" @click="deleteLocalFile(file)" plain>删除</el-button>
</div>
</div>
</el-card>
<el-dialog v-model="dialogFormVisible" width="500">
<el-form :model="form">
<el-form-item :label="t('admin.local.Name')" >
<el-input v-model="form.name" readonly autocomplete="off" />
</el-form-item>
<el-form-item :label="t('admin.local.Expire')" >
<el-input
v-model="form.expireValue"
style="width: 200px"
:placeholder="t('send.pleaseInputExpireValue')"
>
<template #prepend>
<el-select v-model="form.expireStyle" :placeholder="t('send.expireStyle')" style="width: 75px">
<el-option v-for="item in config.expireStyle" :key="item" :label="t(`send.expireData.${item}`)" :value="item" />
</el-select>
</template>
<template #append>
<span v-if="form.expireStyle==='day'">{{t('send.expireValue.day')}}</span>
<span v-else-if="form.expireStyle==='hour'">{{t('send.expireValue.hour')}}</span>
<span v-else-if="form.expireStyle==='minute'">{{t('send.expireValue.minute')}}</span>
<span v-else-if="form.expireStyle==='forever'">👌</span>
<span v-else-if="form.expireStyle==='count'">{{t('send.expireValue.count')}}</span>
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogFormVisible = false">{{ t('admin.local.Cancel') }}</el-button>
<el-button type="primary" @click="toShareFile()">
{{ t('admin.local.Confirm') }}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import {ref, reactive} from 'vue';
import {request} from "@/utils/request";
import {ElMessage} from "element-plus";
import {useConfigStore} from "@/stores/config";
import {useI18n} from "vue-i18n";
const { t } = useI18n()
const { config } = useConfigStore();
const localFiles:any = ref([]);
const loadLocalFiles = ()=>{
request({
url: '/admin/local/lists',
method: 'get',
}).then((data:any) => {
localFiles.value = data.detail;
});
}
const dialogFormVisible = ref(false);
const form = reactive({
name: '1',
expireStyle: 'day',
expireValue: 1,
})
loadLocalFiles()
const deleteLocalFile = (file: any) => {
request({
url: '/admin/local/delete',
method: 'delete',
data: {
filename: file.file
}
}).then((data:any) => {
ElMessage.success(data.detail);
loadLocalFiles()
});
};
const shareLocalFile = (file:any) => {
form.name = file.file;
dialogFormVisible.value = true;
};
const toShareFile=()=>{
request({
url: '/admin/local/share',
method: 'post',
data: {
filename: form.name,
expire_style: form.expireStyle,
expire_value: form.expireValue
}
}).then((data:any) => {
dialogFormVisible.value = false;
ElMessage.success(
{
showClose: true,
message: 'Code:' + data.detail.code,
duration: 0
}
);
loadLocalFiles()
});
}
</script>
<style scoped>
.file-list {
display: grid;
width: 100%;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
padding: 16px;
}
.file-card {
align-items: center;
justify-content: space-between;
border-radius: 10px;
transition: all 0.3s ease;
}
.file-card:hover {
box-shadow: 0px 6px 15px rgba(0, 0, 0, 0.25);
}
.file-info {
flex: 1;
}
.file-name {
font-size: 14px;
font-weight: bold;
white-space: nowrap;
}
.file-date {
font-size: 12px;
color: #a0a0b2;
}
</style>
-219
View File
@@ -1,219 +0,0 @@
<template>
<el-form>
<el-form-item size="large" :label="t('admin.settings.name')">
<el-input v-model="config.name" />
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.description')">
<el-input v-model="config.description" />
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.keywords')" style="letter-spacing: 0.3rem">
<el-input v-model="config.keywords" />
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.themeSelect')" >
<el-select
v-model="config.themesSelect"
style="width: 100%"
>
<el-option v-for="item in config.themesChoices" :label="item.name" :value="item.key" />
</el-select>
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.notify_title')">
<el-input v-model="config.notify_title" />
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.notify_content')">
<el-input v-model="config.notify_content" />
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.background')">
<span style="display: flex;height: 38px;width: 45%">
<el-input v-model="config.background" placeholder="url" />
</span>
<span style="display: flex;height: 38px;margin-left: 20px">
Opacity: <el-input type="number" v-model="config.opacity" />
</span>
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.showAdminAddr')">
<el-select v-model="config.showAdminAddr" style="width: 80%">
<el-option :label="t('admin.settings.showAdmin.open')" :value="1" />
<el-option :label="t('admin.settings.showAdmin.close')" :value="0" />
</el-select>
<small style="margin-left: 0.4rem">{{ t('admin.settings.showAdmin.note') }}</small>
</el-form-item>
<el-form-item size="large" label="robots.Text">
<el-input type="textarea" v-model="config.robotsText" placeholder="explain" />
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.explain')">
<el-input type="textarea" v-model="config.page_explain" placeholder="explain" />
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.admin_token')">
<el-input type="password" v-model="config.admin_token" />
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.uploadSize')">
<el-input type="number" v-model="config.uploadSize" />
<template #append></template>
<small>Bytes{{ t('admin.settings.uploadSizeNote') }}</small>
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.max_save_seconds')">
<el-input type="number" v-model="config.max_save_seconds" />
<small>Seconds:{{t('admin.settings.maxSaveSecondsNote')}}</small>
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.expireStyle')" >
<el-select
v-model="config.expireStyle"
multiple
style="width: 100%"
>
<el-option :label="t('send.expireData.day')" value="day" />
<el-option :label="t('send.expireData.hour')" value="hour" />
<el-option :label="t('send.expireData.minute')" value="minute" />
<el-option :label="t('send.expireData.forever')" value="forever" />
<el-option :label="t('send.expireData.count')" value="count" />
</el-select>
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.openUpload.title')">
<el-select v-model="config.openUpload" style="width: 80%">
<el-option :label="t('admin.settings.openUpload.open')" :value="1" />
<el-option :label="t('admin.settings.openUpload.close')" :value="0" />
</el-select>
<small style="margin-left: 0.4rem">{{ t('admin.settings.openUpload.note') }}</small>
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.file_storage.title')">
<el-select v-model="config.file_storage" style="width: 80%">
<el-option :label="t('admin.settings.file_storage.local')" value="local" />
<el-option :label="t('admin.settings.file_storage.s3')" value="s3" />
</el-select>
<small style="margin-left: 0.4rem">{{ t('admin.settings.file_storage.note') }}</small>
</el-form-item>
<div v-if="config.file_storage==='s3'">
<el-form-item size="large" label="S3 AccessKeyId">
<el-input v-model="config.s3_access_key_id" />
</el-form-item>
<el-form-item size="large" label="S3 SecretAccessKey">
<el-input v-model="config.s3_secret_access_key" />
</el-form-item>
<el-form-item size="large" label="S3 BucketName">
<el-input v-model="config.s3_bucket_name" />
</el-form-item>
<el-form-item size="large" label="S3 EndpointUrl">
<el-input v-model="config.s3_endpoint_url" />
</el-form-item>
<el-form-item size="large" label="S3 hostname">
<el-input v-model="config.s3_hostname" />
</el-form-item>
<el-form-item size="large" label="S3 region name">
<el-input v-model="config.s3_region_name" />
</el-form-item>
<el-form-item size="large" label="S3 Signature Version">
<el-input v-model="config.s3_signature_version" />
</el-form-item>
<el-form-item size="large" label="Aws Session Token">
<el-input v-model="config.aws_session_token" />
</el-form-item>
</div>
<el-form-item size="large" :label="t('admin.settings.uploadlimit')">
<span style="display: flex;height: 38px">
<span style="margin-right: 0.4rem">{{ t('admin.settings.mei') }}</span>
<el-input type="number" v-model="config.uploadMinute" />
<span style="width: 200px;margin-left: 0.4rem">{{ t('admin.settings.minute') }}</span>
</span>
<span style="display: flex;height: 38px">
<span style="width:3rem;margin-right: 0.4rem">{{ t('admin.settings.upload') }}</span>
<el-input type="number" v-model="config.uploadCount" />
<span style="width: 200px;margin-left: 0.4rem">{{ t('admin.settings.files') }}</span>
</span>
</el-form-item>
<el-form-item size="large" :label="t('admin.settings.errorlimit')">
<span style="display: flex;height: 38px">
<span style="margin-right: 0.4rem">{{ t('admin.settings.mei') }}</span>
<el-input type="number" v-model="config.errorMinute" />
<span style="width: 200px;margin-left: 0.4rem">{{ t('admin.settings.minute') }}</span>
</span>
<span style="display: flex;height: 38px">
<span style="width:3rem;margin-right: 0.4rem">{{ t('admin.settings.allow') }}</span>
<el-input type="number" v-model="config.errorCount" />
<span style="width: 200px;margin-left: 0.4rem">{{ t('admin.settings.errors') }}</span>
</span>
</el-form-item>
<el-form-item>
<el-button @click="submitSave" type="primary" style="margin: auto">{{ t('admin.settings.save') }}</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import {ref} from "vue";
import { request } from "@/utils/request";
import { ElMessage } from "element-plus";
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const config:any = ref({
name: '',
description: '',
file_storage: '',
expireStyle: [],
themesChoices: [],
themesSelect: '',
admin_token: '',
robotsText:'',
keywords: '',
notify_title: '',
notify_content: '',
openUpload: 1,
uploadSize: 1,
uploadMinute: 1,
max_save_seconds: 0,
opacity: 0.9,
s3_access_key_id: '',
background: '',
showAdminAddr: 0,
page_explain: '',
s3_secret_access_key: '',
aws_session_token: '',
s3_signature_version: '',
s3_region_name: '',
s3_bucket_name: '',
s3_endpoint_url: '',
s3_hostname: '',
uploadCount: 1,
errorMinute: 1,
errorCount: 1,
});
const refreshData = ()=>{
request({
url: '/admin/config/get',
method: 'get'
}).then((res: any) => {
config.value = res.detail;
});
}
refreshData();
const submitSave = () => {
request({
url: '/admin/config/update',
method: 'patch',
data: config.value
}).then((res: any) => {
if (res.code == 200) {
ElMessage.success(t('admin.settings.saveSuccess'));
} else {
ElMessage.error(res.message);
}
});
};
</script>
<style lang="scss">
.left-menu {
margin-bottom: 1rem;
cursor: pointer;
text-align: center;
font-weight: bold;
font-size: 1rem;
letter-spacing: 0.4rem;
}
small,.el-form-item__content{
color: #909399;
margin-left: 0.4rem;
}
</style>
-126
View File
@@ -1,126 +0,0 @@
<script setup lang="ts">
import { Upload, TakeawayBox } from '@element-plus/icons-vue';
import { onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from "vue-router";
import CardTools from "@/components/CardTools.vue";
import { useFileBoxStore } from "@/stores/fileBox";
import { useFileDataStore } from "@/stores/fileData";
import { ElMessageBox } from 'element-plus'
import { request } from "@/utils/request";
const fileBoxStore = useFileBoxStore();
const fileStore = useFileDataStore();
const router = useRouter()
const route = useRoute()
const code = ref('')
const input_status = reactive({
'readonly': false,
'loading': false,
})
import { useI18n } from 'vue-i18n'
import { useConfigStore } from "@/stores/config";
const { config } = useConfigStore()
const noDialog = ()=>{
ElMessageBox.alert(config.explain, t('send.disclaimers'), {
confirmButtonText: t('fileBox.ok'),
});
}
const { t } = useI18n()
onMounted(() => {
const query_code = route.query.code as string;
if (query_code) {
code.value = query_code
}
})
watch(code, (newVal) => {
if (newVal.length === 5) {
input_status.readonly = true;
input_status.loading = true;
request({
'url': '/share/select/',
'method': 'POST',
'data': {
'code': newVal
}
}).then((res: any) => {
fileBoxStore.showFileBox = true;
let flag = true;
fileStore.receiveData.forEach((file: any) => {
if (file.code === res.detail.code) {
flag = false;
return;
}
});
if (flag) {
fileStore.addReceiveData(res.detail);
}
}).finally(() => {
input_status.readonly = false;
input_status.loading = false;
code.value = '';
});
}
});
const listenInput = (num: number) => {
if (code.value.length < 5) {
code.value += num
}
};
const name = atob('RmlsZUNvZGVCb3ggVjIuMQ==');
const url = atob('aHR0cHM6Ly9naXRodWIuY29tL3Zhc3RzYS9GaWxlQ29kZUJveA==');
</script>
<template>
<main>
<el-card class="card" style="padding-bottom: 1rem">
<CardTools/>
<el-row style="text-align: center">
<el-col :span="24">
<el-input :readonly="input_status.readonly" v-loading="input_status.loading" v-model="code" class="code-input" round autofocus clearable maxlength="5" :placeholder="t('fileBox.inputNotEmpty')"/>
</el-col>
<el-col :span=8 v-for="i in 9" :key="i">
<el-button class="key-button" round @click="listenInput(i)">{{ i }}</el-button>
</el-col>
<el-col :span=8>
<el-button @click="router.push({'name':'send'})" class="key-button" :icon="Upload" round>
</el-button>
</el-col>
<el-col :span=8>
<el-button class="key-button" round @click="listenInput(0)">0</el-button>
</el-col>
<el-col :span=8>
<el-button class="key-button" round :icon="TakeawayBox" @click="fileBoxStore.showFileBox=true">
</el-button>
</el-col>
</el-row>
</el-card>
<div style="text-align: center; margin-top: 1rem;color: #606266">
<a style="text-decoration: none;color: #606266" target="_blank" :href="url">
{{ name }}
</a>
<a @click="noDialog" style="text-decoration: none;color: #606266;margin-left: 1rem" href="javascript:void(0)">{{t('send.disclaimers')}}</a>
<a v-if="config.show_admin_address" style="color: #606266;margin-left: 1rem;text-decoration: none" href="#/admin">Admin</a>
</div>
</main>
</template>
<style lang='scss'>
.key-button{
width: 6rem;
height: 6rem;
margin: 0.2rem;
font-size: 2rem;
font-weight: bold;
text-align: center;
}
.code-input {
height: 100px;
font-size: 30px;
font-weight: bold;
margin: 1rem 0;
.el-input__wrapper{
border-radius: 20px !important;
}
}
</style>
-59
View File
@@ -1,59 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import CardTools from "@/components/CardTools.vue";
import UploadFile from "@/components/UploadFile.vue";
import UploadText from "@/components/UploadText.vue";
import { useI18n } from 'vue-i18n'
import { useConfigStore } from "@/stores/config";
const { config } = useConfigStore();
const { t } = useI18n()
const shareData = ref({
expireValue: 1,
expireStyle: config.expireStyle.length > 0 ? config.expireStyle[0] : 'day',
targetType: 'file',
})
</script>
<template>
<main>
<el-card class="card" style="position: relative" :body-style="{ padding: '0' }">
<card-tools style="padding: 1rem"/>
<div style="display: flex;margin: 1rem">
<div>
<el-input
v-model="shareData.expireValue"
style="width: 200px"
:placeholder="t('send.pleaseInputExpireValue')"
>
<template #prepend>
<el-select v-model="shareData.expireStyle" :placeholder="t('send.expireStyle')" style="width: 75px">
<el-option v-for="item in config.expireStyle" :key="item" :label="t(`send.expireData.${item}`)" :value="item" />
</el-select>
</template>
<template #append>
<span v-if="shareData.expireStyle=='day'">{{t('send.expireValue.day')}}</span>
<span v-else-if="shareData.expireStyle=='hour'">{{t('send.expireValue.hour')}}</span>
<span v-else-if="shareData.expireStyle=='minute'">{{t('send.expireValue.minute')}}</span>
<span v-else-if="shareData.expireStyle=='forever'">👌</span>
<span v-else-if="shareData.expireStyle=='count'">{{t('send.expireValue.count')}}</span>
</template>
</el-input>
</div>
<el-radio-group v-model="shareData.targetType" style="margin-left: 1rem;">
<el-radio label="file">
{{t('send.fileType.file')}}
</el-radio>
<el-radio label="text">
{{t('send.fileType.text')}}
</el-radio>
</el-radio-group>
</div>
<div style="margin: 1rem">
<upload-file :shareData="shareData" v-if="shareData.targetType=='file'"/>
<upload-text :shareData="shareData" v-else-if="shareData.targetType=='text'"/>
</div>
</el-card>
</main>
</template>
-12
View File
@@ -1,12 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
-11
View File
@@ -1,11 +0,0 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}
-16
View File
@@ -1,16 +0,0 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}
-24
View File
@@ -1,24 +0,0 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
File diff suppressed because it is too large Load Diff
+57 -47
View File
@@ -16,6 +16,7 @@ from apps.base.models import KeyValue
from apps.base.utils import ip_limit
from apps.base.views import share_api
from apps.admin.views import admin_api
from core.database import init_db
from core.response import APIResponse
from core.settings import data_root, settings, BASE_DIR, DEFAULT_CONFIG
from core.tasks import delete_expire_files
@@ -24,16 +25,6 @@ from contextlib import asynccontextmanager
from tortoise import Tortoise
async def init_db():
await Tortoise.init(
db_url=f'sqlite://{data_root}/filecodebox.db',
modules={'models': ['apps.base.models']},
use_tz=False,
timezone="Asia/Shanghai"
)
await Tortoise.generate_schemas()
@asynccontextmanager
async def lifespan(app: FastAPI):
# 初始化数据库
@@ -41,7 +32,11 @@ async def lifespan(app: FastAPI):
# 加载配置
await load_config()
app.mount('/assets', StaticFiles(directory=f'./{settings.themesSelect}/assets'), name="assets")
app.mount(
"/assets",
StaticFiles(directory=f"./{settings.themesSelect}/assets"),
name="assets",
)
# 启动后台任务
task = asyncio.create_task(delete_expire_files())
@@ -56,14 +51,18 @@ async def lifespan(app: FastAPI):
async def load_config():
user_config, _ = await KeyValue.get_or_create(key='settings', defaults={'value': DEFAULT_CONFIG})
await KeyValue.update_or_create(key='sys_start', defaults={'value': int(time.time() * 1000)})
user_config, _ = await KeyValue.get_or_create(
key="settings", defaults={"value": DEFAULT_CONFIG}
)
await KeyValue.update_or_create(
key="sys_start", defaults={"value": int(time.time() * 1000)}
)
settings.user_config = user_config.value
# 更新 ip_limit 配置
ip_limit['error'].minutes = settings.errorMinute
ip_limit['error'].count = settings.errorCount
ip_limit['upload'].minutes = settings.uploadMinute
ip_limit['upload'].count = settings.uploadCount
ip_limit["error"].minutes = settings.errorMinute
ip_limit["error"].count = settings.errorCount
ip_limit["upload"].minutes = settings.uploadMinute
ip_limit["upload"].count = settings.uploadCount
app = FastAPI(lifespan=lifespan)
@@ -80,11 +79,11 @@ app.add_middleware(
register_tortoise(
app,
config={
'connections': {'default': f'sqlite://{data_root}/filecodebox.db'},
'apps': {
'models': {
'models': ['apps.base.models'],
'default_connection': 'default',
"connections": {"default": f"sqlite://{data_root}/filecodebox.db"},
"apps": {
"models": {
"models": ["apps.base.models"],
"default_connection": "default",
},
},
},
@@ -96,39 +95,50 @@ app.include_router(share_api)
app.include_router(admin_api)
@app.get('/')
async def index():
@app.exception_handler(404)
@app.get("/")
async def index(request=None, exc=None):
return HTMLResponse(
content=open(BASE_DIR / f'{settings.themesSelect}/index.html', 'r', encoding='utf-8').read()
.replace('{{title}}', str(settings.name))
.replace('{{description}}', str(settings.description))
.replace('{{keywords}}', str(settings.keywords))
.replace('{{opacity}}', str(settings.opacity))
.replace('{{background}}', str(settings.background))
, media_type='text/html', headers={'Cache-Control': 'no-cache'})
content=open(
BASE_DIR / f"{settings.themesSelect}/index.html", "r", encoding="utf-8"
)
.read()
.replace("{{title}}", str(settings.name))
.replace("{{description}}", str(settings.description))
.replace("{{keywords}}", str(settings.keywords))
.replace("{{opacity}}", str(settings.opacity))
.replace('"/assets/', '"assets/')
.replace("{{background}}", str(settings.background)),
media_type="text/html",
headers={"Cache-Control": "no-cache"},
)
@app.get('/robots.txt')
@app.get("/robots.txt")
async def robots():
return HTMLResponse(content=settings.robotsText, media_type='text/plain')
return HTMLResponse(content=settings.robotsText, media_type="text/plain")
@app.post('/')
@app.post("/")
async def get_config():
return APIResponse(detail={
'name': settings.name,
'description': settings.description,
'explain': settings.page_explain,
'uploadSize': settings.uploadSize,
'expireStyle': settings.expireStyle,
'openUpload': settings.openUpload,
'notify_title': settings.notify_title,
'notify_content': settings.notify_content,
'show_admin_address': settings.showAdminAddr,
})
return APIResponse(
detail={
"name": settings.name,
"description": settings.description,
"explain": settings.page_explain,
"uploadSize": settings.uploadSize,
"expireStyle": settings.expireStyle,
"openUpload": settings.openUpload,
"notify_title": settings.notify_title,
"notify_content": settings.notify_content,
"show_admin_address": settings.showAdminAddr,
}
)
if __name__ == '__main__':
if __name__ == "__main__":
import uvicorn
uvicorn.run(app='main:app', host="0.0.0.0", port=settings.port, reload=False, workers=1)
uvicorn.run(
app="main:app", host="0.0.0.0", port=settings.port, reload=False, workers=1
)
+54
View File
@@ -0,0 +1,54 @@
import { request } from '@/utils/request'; // 假设这是你的请求函数
import { ElMessage } from 'element-plus'; // Element Plus 的消息组件
import { t } from '@/locales'; // 国际化函数
import { saveFileByWebApi, saveFileByElementA } from '@/utils/file'; // 文件保存工具函数
const downloadFile = (id: number) => {
request({
url: '/admin/file/download',
method: 'get',
params: {
id,
},
responseType: 'blob'
}).then((response: any) => {
const contentDisposition = response.headers?.['content-disposition'] || '';
let filename = 'file';
try {
// 先尝试获取 filename* 参数
const filenameStarMatch = contentDisposition.match(/filename\*=UTF-8''([^;]*)/);
if (filenameStarMatch) {
filename = decodeURIComponent(filenameStarMatch[1]);
} else {
// 如果没有 filename*,则尝试获取普通 filename
const filenameMatch = contentDisposition.match(/filename="([^"]*)"/) || contentDisposition.match(/filename=([^;]*)/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
} catch (error) {
console.warn('解析文件名失败:', error);
// 如果解析失败,使用默认文件名
filename = `download_${id}`;
}
// @ts-ignore
if (window.showSaveFilePicker) {
saveFileByWebApi(response.data, filename).catch(() => {
saveFileByElementA(response.data, filename).catch(() => {
ElMessage.error(t('admin.fileView.download_fail'));
});
});
} else {
saveFileByElementA(response.data, filename).catch(() => {
ElMessage.error(t('admin.fileView.download_fail'));
});
}
}).catch(error => {
console.error('下载文件失败:', error);
ElMessage.error(t('admin.fileView.download_fail'));
});
};
export default downloadFile;
+6 -2
View File
@@ -22,6 +22,7 @@
## 🚀 更新计划
- [ ] 切片上传,同文件秒传,断点续传
- [x] 适配子目录
- [x] 用户登录重构
- [x] webdav存储
- [x] 存储支持自定义路径
@@ -36,9 +37,12 @@ FileCodeBox 是一个基于 FastAPI + Vue3 开发的轻量级文件分享工具
<div align="center">
<h3>
<a href="https://github.com/vastsa/FileCodeBoxFronted" target="_blank">
<img src="https://img.shields.io/badge/Frontend-FileCodeBoxFronted-blue?style=for-the-badge&logo=github" alt="前端仓库">
<img src="https://img.shields.io/badge/Frontend-FileCodeBoxFronted2024-blue?style=for-the-badge&logo=github" alt="前端仓库2024">
</a>
&nbsp;&nbsp;&nbsp;
<a href="https://github.com/vastsa/FileCodeBoxFronted2023" target="_blank">
<img src="https://img.shields.io/badge/Frontend-FileCodeBoxFronted2023-blue?style=for-the-badge&logo=github" alt="前端仓库2023">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://share.lanol.cn" target="_blank">
<img src="https://img.shields.io/badge/Demo-share.lanol.cn-green?style=for-the-badge&logo=internet-explorer" alt="演示站点">
</a>
+6 -3
View File
@@ -28,11 +28,14 @@ FileCodeBox is a lightweight file sharing tool developed with FastAPI + Vue3. It
<div align="center">
<h3>
<a href="https://github.com/vastsa/FileCodeBoxFronted" target="_blank">
<img src="https://img.shields.io/badge/Frontend-FileCodeBoxFronted-blue?style=for-the-badge&logo=github" alt="Frontend Repository">
<img src="https://img.shields.io/badge/Frontend-FileCodeBoxFronted2024-blue?style=for-the-badge&logo=github" alt="前端仓库2024">
</a>
&nbsp;&nbsp;&nbsp;
<a href="https://github.com/vastsa/FileCodeBoxFronted2023" target="_blank">
<img src="https://img.shields.io/badge/Frontend-FileCodeBoxFronted2023-blue?style=for-the-badge&logo=github" alt="前端仓库2023">
</a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://share.lanol.cn" target="_blank">
<img src="https://img.shields.io/badge/Demo-share.lanol.cn-green?style=for-the-badge&logo=internet-explorer" alt="Demo Site">
<img src="https://img.shields.io/badge/Demo-share.lanol.cn-green?style=for-the-badge&logo=internet-explorer" alt="演示站点">
</a>
</h3>
</div>
+4 -45
View File
@@ -1,49 +1,8 @@
aioboto3==11.2.0
aiobotocore==2.5.0
aiohttp==3.9.2
aioitertools==0.11.0
aiosignal==1.3.1
aiosqlite==0.17.0
annotated-types==0.5.0
anyio==3.7.1
async-timeout==4.0.3
atlastk==0.13.2
attrs==23.1.0
boto3==1.26.76
aiohttp==3.10.11
botocore==1.29.76
certifi==2024.8.30
cffi==1.17.1
charset-normalizer==3.2.0
click==8.1.6
cryptography==43.0.1
exceptiongroup==1.1.2
fastapi==0.115.0
frozenlist==1.4.0
greenlet==2.0.2
h11==0.14.0
idna==3.7
iso8601==1.1.0
jmespath==1.0.1
msal==1.31.0
multidict==6.0.4
Office365-REST-Python-Client==2.5.2
pycparser==2.22
pydantic==2.1.1
pydantic_core==2.4.0
PyJWT==2.9.0
pypika-tortoise==0.1.6
python-dateutil==2.8.2
python-multipart==0.0.7
pytz==2023.3
requests==2.32.3
s3transfer==0.6.1
six==1.16.0
sniffio==1.3.0
SQLAlchemy==2.0.19
starlette==0.38.6
tortoise-orm==0.20.0
typing_extensions==4.8.0
urllib3==1.26.20
pydantic==2.4.0
uvicorn==0.23.2
wrapt==1.15.0
yarl==1.9.2
tortoise-orm==0.20.0
python-multipart==0.0.20
-1
View File
@@ -1 +0,0 @@
import{d as s,a8 as n,Y as c,W as e,a1 as r,$ as a,g as o,o as i}from"./index-CVeK0dPI.js";const l={style:{"text-align":"center"}},u={style:{color:"#333"},href:"https://github.com/vastsa/FileCodeBox"},f=s({__name:"AboutView",setup(_){const{t}=n();return(m,d)=>(i(),c("main",l,[e("span",null,[r(a(o(t)("admin.about.source1"))+" ",1),e("em",null,[e("a",u,a(o(t)("admin.about.source2")),1)])])]))}});export{f as default};
+1
View File
@@ -0,0 +1 @@
import{a as s,a8 as n,P as c,N as e,U as r,R as a,j as o,o as i}from"./index-5orz1M6_.js";const l={style:{"text-align":"center"}},u={style:{color:"#333"},href:"https://github.com/vastsa/FileCodeBox"},f=s({__name:"AboutView",setup(_){const{t}=n();return(m,p)=>(i(),c("main",l,[e("span",null,[r(a(o(t)("admin.about.source1"))+" ",1),e("em",null,[e("a",u,a(o(t)("admin.about.source2")),1)])])]))}});export{f as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{c as y,k as F,a as l,m as C,s as _,g as v,n as N,o as E,h as K,p as j,q as u,r as B,e as R,b as q,f as W,j as Y,S as H,t as J}from"./el-popper-CwdMC8HJ.js";import{bs as S,b2 as x,a$ as Q,b5 as V,bt as X}from"./index-CVeK0dPI.js";import{v as Z}from"./el-button-CIFg-IwZ.js";function z(e,r){for(var n=-1,s=e==null?0:e.length;++n<s&&r(e[n],n,e)!==!1;);return e}function k(e,r){return e&&y(r,F(r),e)}function ee(e,r){return e&&y(r,l(r),e)}function re(e,r){return y(e,C(e),r)}var te=Object.getOwnPropertySymbols,L=te?function(e){for(var r=[];e;)Z(r,C(e)),e=v(e);return r}:_;function ne(e,r){return y(e,L(e),r)}function ae(e){return N(e,l,L)}var oe=Object.prototype,se=oe.hasOwnProperty;function ce(e){var r=e.length,n=new e.constructor(r);return r&&typeof e[0]=="string"&&se.call(e,"index")&&(n.index=e.index,n.input=e.input),n}function ie(e,r){var n=r?E(e.buffer):e.buffer;return new e.constructor(n,e.byteOffset,e.byteLength)}var be=/\w*$/;function fe(e){var r=new e.constructor(e.source,be.exec(e));return r.lastIndex=e.lastIndex,r}var O=S?S.prototype:void 0,w=O?O.valueOf:void 0;function ge(e){return w?Object(w.call(e)):{}}var ue="[object Boolean]",ye="[object Date]",Te="[object Map]",le="[object Number]",je="[object RegExp]",pe="[object Set]",Ae="[object String]",me="[object Symbol]",de="[object ArrayBuffer]",$e="[object DataView]",Se="[object Float32Array]",Oe="[object Float64Array]",we="[object Int8Array]",Ie="[object Int16Array]",he="[object Int32Array]",Fe="[object Uint8Array]",Ce="[object Uint8ClampedArray]",Ee="[object Uint16Array]",Be="[object Uint32Array]";function xe(e,r,n){var s=e.constructor;switch(r){case de:return E(e);case ue:case ye:return new s(+e);case $e:return ie(e,n);case Se:case Oe:case we:case Ie:case he:case Fe:case Ce:case Ee:case Be:return K(e,n);case Te:return new s;case le:case Ae:return new s(e);case je:return fe(e);case pe:return new s;case me:return ge(e)}}var Le="[object Map]";function Me(e){return x(e)&&j(e)==Le}var I=u&&u.isMap,Ue=I?B(I):Me,Pe="[object Set]";function De(e){return x(e)&&j(e)==Pe}var h=u&&u.isSet,Ge=h?B(h):De,_e=1,ve=2,Ne=4,M="[object Arguments]",Ke="[object Array]",Re="[object Boolean]",qe="[object Date]",We="[object Error]",U="[object Function]",Ye="[object GeneratorFunction]",He="[object Map]",Je="[object Number]",P="[object Object]",Qe="[object RegExp]",Ve="[object Set]",Xe="[object String]",Ze="[object Symbol]",ze="[object WeakMap]",ke="[object ArrayBuffer]",er="[object DataView]",rr="[object Float32Array]",tr="[object Float64Array]",nr="[object Int8Array]",ar="[object Int16Array]",or="[object Int32Array]",sr="[object Uint8Array]",cr="[object Uint8ClampedArray]",ir="[object Uint16Array]",br="[object Uint32Array]",t={};t[M]=t[Ke]=t[ke]=t[er]=t[Re]=t[qe]=t[rr]=t[tr]=t[nr]=t[ar]=t[or]=t[He]=t[Je]=t[P]=t[Qe]=t[Ve]=t[Xe]=t[Ze]=t[sr]=t[cr]=t[ir]=t[br]=!0;t[We]=t[U]=t[ze]=!1;function T(e,r,n,s,p,c){var a,f=r&_e,g=r&ve,D=r&Ne;if(a!==void 0)return a;if(!Q(e))return e;var A=V(e);if(A){if(a=ce(e),!f)return R(e,a)}else{var b=j(e),m=b==U||b==Ye;if(q(e))return W(e,f);if(b==P||b==M||m&&!p){if(a=g||m?{}:Y(e),!f)return g?ne(e,ee(a,e)):re(e,k(a,e))}else{if(!t[b])return p?e:{};a=xe(e,b,f)}}c||(c=new H);var d=c.get(e);if(d)return d;c.set(e,a),Ge(e)?e.forEach(function(o){a.add(T(o,r,n,o,e,c))}):Ue(e)&&e.forEach(function(o,i){a.set(i,T(o,r,n,i,e,c))});var G=D?g?ae:J:g?l:F,$=A?void 0:G(e);return z($||e,function(o,i){$&&(i=o,o=e[i]),X(a,i,T(o,r,n,i,e,c))}),a}export{T as b};
@@ -0,0 +1 @@
import{c as y,k as F,a as l,m as C,s as _,g as v,n as N,o as E,h as K,p as j,q as x,r as u,e as R,b as q,f as W,j as Y,S as H,t as J}from"./el-popper-DxfKqvb2.js";import{bv as $,b6 as B,b3 as Q,b9 as V,bw as X}from"./index-5orz1M6_.js";import{x as Z}from"./el-input-D_xPEeA2.js";function z(e,r){for(var n=-1,s=e==null?0:e.length;++n<s&&r(e[n],n,e)!==!1;);return e}function k(e,r){return e&&y(r,F(r),e)}function ee(e,r){return e&&y(r,l(r),e)}function re(e,r){return y(e,C(e),r)}var te=Object.getOwnPropertySymbols,L=te?function(e){for(var r=[];e;)Z(r,C(e)),e=v(e);return r}:_;function ne(e,r){return y(e,L(e),r)}function ae(e){return N(e,l,L)}var oe=Object.prototype,se=oe.hasOwnProperty;function ce(e){var r=e.length,n=new e.constructor(r);return r&&typeof e[0]=="string"&&se.call(e,"index")&&(n.index=e.index,n.input=e.input),n}function ie(e,r){var n=r?E(e.buffer):e.buffer;return new e.constructor(n,e.byteOffset,e.byteLength)}var be=/\w*$/;function fe(e){var r=new e.constructor(e.source,be.exec(e));return r.lastIndex=e.lastIndex,r}var w=$?$.prototype:void 0,O=w?w.valueOf:void 0;function ge(e){return O?Object(O.call(e)):{}}var ue="[object Boolean]",ye="[object Date]",Te="[object Map]",le="[object Number]",je="[object RegExp]",pe="[object Set]",Ae="[object String]",me="[object Symbol]",de="[object ArrayBuffer]",Se="[object DataView]",$e="[object Float32Array]",we="[object Float64Array]",Oe="[object Int8Array]",Ie="[object Int16Array]",he="[object Int32Array]",Fe="[object Uint8Array]",Ce="[object Uint8ClampedArray]",Ee="[object Uint16Array]",xe="[object Uint32Array]";function Be(e,r,n){var s=e.constructor;switch(r){case de:return E(e);case ue:case ye:return new s(+e);case Se:return ie(e,n);case $e:case we:case Oe:case Ie:case he:case Fe:case Ce:case Ee:case xe:return K(e,n);case Te:return new s;case le:case Ae:return new s(e);case je:return fe(e);case pe:return new s;case me:return ge(e)}}var Le="[object Map]";function Me(e){return B(e)&&j(e)==Le}var I=u&&u.isMap,Ue=I?x(I):Me,Pe="[object Set]";function De(e){return B(e)&&j(e)==Pe}var h=u&&u.isSet,Ge=h?x(h):De,_e=1,ve=2,Ne=4,M="[object Arguments]",Ke="[object Array]",Re="[object Boolean]",qe="[object Date]",We="[object Error]",U="[object Function]",Ye="[object GeneratorFunction]",He="[object Map]",Je="[object Number]",P="[object Object]",Qe="[object RegExp]",Ve="[object Set]",Xe="[object String]",Ze="[object Symbol]",ze="[object WeakMap]",ke="[object ArrayBuffer]",er="[object DataView]",rr="[object Float32Array]",tr="[object Float64Array]",nr="[object Int8Array]",ar="[object Int16Array]",or="[object Int32Array]",sr="[object Uint8Array]",cr="[object Uint8ClampedArray]",ir="[object Uint16Array]",br="[object Uint32Array]",t={};t[M]=t[Ke]=t[ke]=t[er]=t[Re]=t[qe]=t[rr]=t[tr]=t[nr]=t[ar]=t[or]=t[He]=t[Je]=t[P]=t[Qe]=t[Ve]=t[Xe]=t[Ze]=t[sr]=t[cr]=t[ir]=t[br]=!0;t[We]=t[U]=t[ze]=!1;function T(e,r,n,s,p,c){var a,f=r&_e,g=r&ve,D=r&Ne;if(a!==void 0)return a;if(!Q(e))return e;var A=V(e);if(A){if(a=ce(e),!f)return R(e,a)}else{var b=j(e),m=b==U||b==Ye;if(q(e))return W(e,f);if(b==P||b==M||m&&!p){if(a=g||m?{}:Y(e),!f)return g?ne(e,ee(a,e)):re(e,k(a,e))}else{if(!t[b])return p?e:{};a=Be(e,b,f)}}c||(c=new H);var d=c.get(e);if(d)return d;c.set(e,a),Ge(e)?e.forEach(function(o){a.add(T(o,r,n,o,e,c))}):Ue(e)&&e.forEach(function(o,i){a.set(i,T(o,r,n,i,e,c))});var G=D?g?ae:J:g?l:F,S=A?void 0:G(e);return z(S||e,function(o,i){S&&(i=o,o=e[i]),X(a,i,T(o,r,n,i,e,c))}),a}export{T as b};
-1
View File
@@ -1 +0,0 @@
const i='a[href],button:not([disabled]),button:not([hidden]),:not([tabindex="-1"]),input:not([disabled]),input:not([type="hidden"]),select:not([disabled]),textarea:not([disabled])',o=t=>getComputedStyle(t).position==="fixed"?!1:t.offsetParent!==null,u=t=>Array.from(t.querySelectorAll(i)).filter(e=>d(e)&&o(e)),d=t=>{if(t.tabIndex>0||t.tabIndex===0&&t.getAttribute("tabIndex")!==null)return!0;if(t.disabled)return!1;switch(t.nodeName){case"A":return!!t.href&&t.rel!=="ignore";case"INPUT":return!(t.type==="hidden"||t.type==="file");case"BUTTON":case"SELECT":case"TEXTAREA":return!0;default:return!1}},a=function(t,e,...r){let n;e.includes("mouse")||e.includes("click")?n="MouseEvents":e.includes("key")?n="KeyboardEvent":n="HTMLEvents";const s=document.createEvent(n);return s.initEvent(e,...r),t.dispatchEvent(s),t};export{u as o,a as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
.el-card{--el-card-border-color:var(--el-border-color-light);--el-card-border-radius:4px;--el-card-padding:20px;--el-card-bg-color:var(--el-fill-color-blank);background-color:var(--el-card-bg-color);border:1px solid var(--el-card-border-color);border-radius:var(--el-card-border-radius);color:var(--el-text-color-primary);overflow:hidden;transition:var(--el-transition-duration)}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{box-shadow:var(--el-box-shadow-light)}.el-card__header{border-bottom:1px solid var(--el-card-border-color);box-sizing:border-box;padding:calc(var(--el-card-padding) - 2px) var(--el-card-padding)}.el-card__body{padding:var(--el-card-padding)}.el-card__footer{border-top:1px solid var(--el-card-border-color);box-sizing:border-box;padding:calc(var(--el-card-padding) - 2px) var(--el-card-padding)}.el-overlay{background-color:var(--el-overlay-color-lighter);bottom:0;height:100%;left:0;overflow:auto;position:fixed;right:0;top:0;z-index:2000}.el-overlay .el-overlay-root{height:0}:root{--el-popup-modal-bg-color:var(--el-color-black);--el-popup-modal-opacity:.5}.v-modal-enter{animation:v-modal-in var(--el-transition-duration-fast) ease}.v-modal-leave{animation:v-modal-out var(--el-transition-duration-fast) ease forwards}@keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-out{to{opacity:0}}.v-modal{background:var(--el-popup-modal-bg-color);height:100%;left:0;opacity:var(--el-popup-modal-opacity);position:fixed;top:0;width:100%}.el-popup-parent--hidden{overflow:hidden}.el-dialog{--el-dialog-width:50%;--el-dialog-margin-top:15vh;--el-dialog-bg-color:var(--el-bg-color);--el-dialog-box-shadow:var(--el-box-shadow);--el-dialog-title-font-size:var(--el-font-size-large);--el-dialog-content-font-size:14px;--el-dialog-font-line-height:var(--el-font-line-height-primary);--el-dialog-padding-primary:16px;--el-dialog-border-radius:var(--el-border-radius-base);background:var(--el-dialog-bg-color);border-radius:var(--el-dialog-border-radius);box-shadow:var(--el-dialog-box-shadow);box-sizing:border-box;margin:var(--el-dialog-margin-top,15vh) auto 50px;overflow-wrap:break-word;padding:var(--el-dialog-padding-primary);position:relative;width:var(--el-dialog-width,50%)}.el-dialog:focus{outline:none!important}.el-dialog.is-align-center{margin:auto}.el-dialog.is-fullscreen{--el-dialog-width:100%;--el-dialog-margin-top:0;height:100%;margin-bottom:0;overflow:auto}.el-dialog__wrapper{bottom:0;left:0;margin:0;overflow:auto;position:fixed;right:0;top:0}.el-dialog.is-draggable .el-dialog__header{cursor:move;-webkit-user-select:none;-moz-user-select:none;user-select:none}.el-dialog__header{padding-bottom:var(--el-dialog-padding-primary)}.el-dialog__header.show-close{padding-right:calc(var(--el-dialog-padding-primary) + var(--el-message-close-size, 16px))}.el-dialog__headerbtn{background:transparent;border:none;cursor:pointer;font-size:var(--el-message-close-size,16px);height:48px;outline:none;padding:0;position:absolute;right:0;top:0;width:48px}.el-dialog__headerbtn .el-dialog__close{color:var(--el-color-info);font-size:inherit}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:var(--el-color-primary)}.el-dialog__title{color:var(--el-text-color-primary);font-size:var(--el-dialog-title-font-size);line-height:var(--el-dialog-font-line-height)}.el-dialog__body{color:var(--el-text-color-regular);font-size:var(--el-dialog-content-font-size)}.el-dialog__footer{box-sizing:border-box;padding-top:var(--el-dialog-padding-primary);text-align:right}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial}.el-dialog--center .el-dialog__footer{text-align:inherit}.el-overlay-dialog{bottom:0;left:0;overflow:auto;position:fixed;right:0;top:0}.dialog-fade-enter-active{animation:modal-fade-in var(--el-transition-duration)}.dialog-fade-enter-active .el-overlay-dialog{animation:dialog-fade-in var(--el-transition-duration)}.dialog-fade-leave-active{animation:modal-fade-out var(--el-transition-duration)}.dialog-fade-leave-active .el-overlay-dialog{animation:dialog-fade-out var(--el-transition-duration)}@keyframes dialog-fade-in{0%{opacity:0;transform:translate3d(0,-20px,0)}to{opacity:1;transform:translateZ(0)}}@keyframes dialog-fade-out{0%{opacity:1;transform:translateZ(0)}to{opacity:0;transform:translate3d(0,-20px,0)}}@keyframes modal-fade-in{0%{opacity:0}to{opacity:1}}@keyframes modal-fade-out{0%{opacity:1}to{opacity:0}}
.el-card{--el-card-border-color:var(--el-border-color-light);--el-card-border-radius:4px;--el-card-padding:20px;--el-card-bg-color:var(--el-fill-color-blank);background-color:var(--el-card-bg-color);border:1px solid var(--el-card-border-color);border-radius:var(--el-card-border-radius);color:var(--el-text-color-primary);overflow:hidden;transition:var(--el-transition-duration)}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{box-shadow:var(--el-box-shadow-light)}.el-card__header{border-bottom:1px solid var(--el-card-border-color);box-sizing:border-box;padding:calc(var(--el-card-padding) - 2px) var(--el-card-padding)}.el-card__body{padding:var(--el-card-padding)}.el-card__footer{border-top:1px solid var(--el-card-border-color);box-sizing:border-box;padding:calc(var(--el-card-padding) - 2px) var(--el-card-padding)}.el-overlay{background-color:var(--el-overlay-color-lighter);bottom:0;height:100%;left:0;overflow:auto;position:fixed;right:0;top:0;z-index:2000}.el-overlay .el-overlay-root{height:0}:root{--el-popup-modal-bg-color:var(--el-color-black);--el-popup-modal-opacity:.5}.v-modal-enter{animation:v-modal-in var(--el-transition-duration-fast) ease}.v-modal-leave{animation:v-modal-out var(--el-transition-duration-fast) ease forwards}@keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-out{to{opacity:0}}.v-modal{background:var(--el-popup-modal-bg-color);height:100%;left:0;opacity:var(--el-popup-modal-opacity);position:fixed;top:0;width:100%}.el-popup-parent--hidden{overflow:hidden}.el-dialog{--el-dialog-width:50%;--el-dialog-margin-top:15vh;--el-dialog-bg-color:var(--el-bg-color);--el-dialog-box-shadow:var(--el-box-shadow);--el-dialog-title-font-size:var(--el-font-size-large);--el-dialog-content-font-size:14px;--el-dialog-font-line-height:var(--el-font-line-height-primary);--el-dialog-padding-primary:16px;--el-dialog-border-radius:var(--el-border-radius-base);background:var(--el-dialog-bg-color);border-radius:var(--el-dialog-border-radius);box-shadow:var(--el-dialog-box-shadow);box-sizing:border-box;margin:var(--el-dialog-margin-top,15vh) auto 50px;overflow-wrap:break-word;padding:var(--el-dialog-padding-primary);position:relative;width:var(--el-dialog-width,50%)}.el-dialog:focus{outline:none!important}.el-dialog.is-align-center{margin:auto}.el-dialog.is-fullscreen{--el-dialog-width:100%;--el-dialog-margin-top:0;border-radius:0;height:100%;margin-bottom:0;overflow:auto}.el-dialog__wrapper{bottom:0;left:0;margin:0;overflow:auto;position:fixed;right:0;top:0}.el-dialog.is-draggable .el-dialog__header{cursor:move;-webkit-user-select:none;-moz-user-select:none;user-select:none}.el-dialog__header{padding-bottom:var(--el-dialog-padding-primary)}.el-dialog__header.show-close{padding-right:calc(var(--el-dialog-padding-primary) + var(--el-message-close-size, 16px))}.el-dialog__headerbtn{background:transparent;border:none;cursor:pointer;font-size:var(--el-message-close-size,16px);height:48px;outline:none;padding:0;position:absolute;right:0;top:0;width:48px}.el-dialog__headerbtn .el-dialog__close{color:var(--el-color-info);font-size:inherit}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:var(--el-color-primary)}.el-dialog__title{color:var(--el-text-color-primary);font-size:var(--el-dialog-title-font-size);line-height:var(--el-dialog-font-line-height)}.el-dialog__body{color:var(--el-text-color-regular);font-size:var(--el-dialog-content-font-size)}.el-dialog__footer{box-sizing:border-box;padding-top:var(--el-dialog-padding-primary);text-align:right}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial}.el-dialog--center .el-dialog__footer{text-align:inherit}.el-overlay-dialog{bottom:0;left:0;overflow:auto;position:fixed;right:0;top:0}.dialog-fade-enter-active{animation:modal-fade-in var(--el-transition-duration)}.dialog-fade-enter-active .el-overlay-dialog{animation:dialog-fade-in var(--el-transition-duration)}.dialog-fade-leave-active{animation:modal-fade-out var(--el-transition-duration)}.dialog-fade-leave-active .el-overlay-dialog{animation:dialog-fade-out var(--el-transition-duration)}@keyframes dialog-fade-in{0%{opacity:0;transform:translate3d(0,-20px,0)}to{opacity:1;transform:translateZ(0)}}@keyframes dialog-fade-out{0%{opacity:1;transform:translateZ(0)}to{opacity:0;transform:translate3d(0,-20px,0)}}@keyframes modal-fade-in{0%{opacity:0}to{opacity:1}}@keyframes modal-fade-out{0%{opacity:1}to{opacity:0}}
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More