Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
custom: ["https://afdian.com/a/filecodebox"]
|
||||
|
||||
+17
-14
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
||||
Vendored
-46
@@ -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']
|
||||
}
|
||||
}
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -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>
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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: '本站已关闭游客上传',
|
||||
}
|
||||
};
|
||||
@@ -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: '本站已关闭游客上传',
|
||||
}
|
||||
};
|
||||
@@ -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: '本站已關閉遊客上傳',
|
||||
}
|
||||
};
|
||||
@@ -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')
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
});
|
||||
@@ -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 };
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import {ref} from "vue";
|
||||
|
||||
export const useFileBoxStore = defineStore('fileBox', () => {
|
||||
const showFileBox = ref(false);
|
||||
return { showFileBox };
|
||||
});
|
||||
@@ -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 };
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
<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
@@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
<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
@@ -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 +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};
|
||||
@@ -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 +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
Reference in New Issue
Block a user