Merge branch 'main' of https://github.com/shumengya/InfoGenie
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -205,3 +205,5 @@ cython_debug/
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
frontend/react-app/node_modules/
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"git.ignoreLimitWarning": true
|
||||
}
|
||||
543
QQEmailSendAPI.py
Normal file
543
QQEmailSendAPI.py
Normal file
@@ -0,0 +1,543 @@
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.header import Header
|
||||
import random
|
||||
import string
|
||||
import json
|
||||
import os
|
||||
|
||||
# 邮件发送配置
|
||||
SENDER_EMAIL = '3205788256@qq.com' # 发件人邮箱
|
||||
SENDER_AUTH_CODE = 'szcaxvbftusqddhi' # 授权码
|
||||
SMTP_SERVER = 'smtp.qq.com' # QQ邮箱SMTP服务器
|
||||
SMTP_PORT = 465 # QQ邮箱SSL端口
|
||||
|
||||
# 验证码缓存文件
|
||||
VERIFICATION_CACHE_FILE = os.path.join("config", "verification_codes.json")
|
||||
|
||||
class QQMailAPI:
|
||||
"""QQ邮箱发送邮件API类"""
|
||||
|
||||
def __init__(self, sender_email, authorization_code):
|
||||
"""
|
||||
初始化邮箱配置
|
||||
:param sender_email: 发送方QQ邮箱地址
|
||||
:param authorization_code: QQ邮箱授权码
|
||||
"""
|
||||
self.sender_email = sender_email
|
||||
self.authorization_code = authorization_code
|
||||
self.smtp_server = 'smtp.qq.com'
|
||||
self.smtp_port = 465 # SSL端口
|
||||
|
||||
# 发送纯文本邮件
|
||||
def send_text_email(self, receiver_email, subject, content, cc_emails=None):
|
||||
"""
|
||||
发送纯文本邮件
|
||||
:param receiver_email: 接收方邮箱地址(单个)
|
||||
:param subject: 邮件主题
|
||||
:param content: 邮件正文内容
|
||||
:param cc_emails: 抄送邮箱列表
|
||||
:return: 发送成功返回True,失败返回False
|
||||
"""
|
||||
try:
|
||||
# 创建邮件对象
|
||||
message = MIMEText(content, 'plain', 'utf-8')
|
||||
message['From'] = Header(self.sender_email, 'utf-8')
|
||||
message['To'] = Header(receiver_email, 'utf-8')
|
||||
message['Subject'] = Header(subject, 'utf-8')
|
||||
|
||||
# 添加抄送
|
||||
if cc_emails:
|
||||
message['Cc'] = Header(",".join(cc_emails), 'utf-8')
|
||||
all_receivers = [receiver_email] + cc_emails
|
||||
else:
|
||||
all_receivers = [receiver_email]
|
||||
|
||||
# 连接SMTP服务器并发送邮件
|
||||
with smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) as server:
|
||||
server.login(self.sender_email, self.authorization_code)
|
||||
server.sendmail(self.sender_email, all_receivers, message.as_string())
|
||||
|
||||
print(f"邮件发送成功:主题='{subject}', 收件人='{receiver_email}'")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"邮件发送失败:{str(e)}")
|
||||
return False
|
||||
|
||||
# 发送HTML格式邮件,可带附件
|
||||
def send_html_email(self, receiver_email, subject, html_content, cc_emails=None, attachments=None):
|
||||
"""
|
||||
发送HTML格式邮件,可带附件
|
||||
:param receiver_email: 接收方邮箱地址(单个)
|
||||
:param subject: 邮件主题
|
||||
:param html_content: HTML格式的邮件正文
|
||||
:param cc_emails: 抄送邮箱列表
|
||||
:param attachments: 附件文件路径列表
|
||||
:return: 发送成功返回True,失败返回False
|
||||
"""
|
||||
try:
|
||||
# 创建带附件的邮件对象
|
||||
message = MIMEMultipart()
|
||||
message['From'] = Header(self.sender_email, 'utf-8')
|
||||
message['To'] = Header(receiver_email, 'utf-8')
|
||||
message['Subject'] = Header(subject, 'utf-8')
|
||||
|
||||
# 添加抄送
|
||||
if cc_emails:
|
||||
message['Cc'] = Header(",".join(cc_emails), 'utf-8')
|
||||
all_receivers = [receiver_email] + cc_emails
|
||||
else:
|
||||
all_receivers = [receiver_email]
|
||||
|
||||
# 添加HTML正文
|
||||
message.attach(MIMEText(html_content, 'html', 'utf-8'))
|
||||
|
||||
# 添加附件
|
||||
if attachments:
|
||||
for file_path in attachments:
|
||||
try:
|
||||
with open(file_path, 'rb') as file:
|
||||
attachment = MIMEApplication(file.read(), _subtype="octet-stream")
|
||||
attachment.add_header('Content-Disposition', 'attachment', filename=file_path.split("/")[-1])
|
||||
message.attach(attachment)
|
||||
except Exception as e:
|
||||
print(f"添加附件失败 {file_path}: {str(e)}")
|
||||
|
||||
# 连接SMTP服务器并发送邮件
|
||||
with smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) as server:
|
||||
server.login(self.sender_email, self.authorization_code)
|
||||
server.sendmail(self.sender_email, all_receivers, message.as_string())
|
||||
|
||||
print(f"HTML邮件发送成功:主题='{subject}', 收件人='{receiver_email}'")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"HTML邮件发送失败:{str(e)}")
|
||||
return False
|
||||
|
||||
class EmailVerification:
|
||||
|
||||
#生成指定长度的随机验证码
|
||||
@staticmethod
|
||||
def generate_verification_code(length=6):
|
||||
"""
|
||||
生成指定长度的随机验证码
|
||||
|
||||
参数:
|
||||
length (int): 验证码长度,默认6位
|
||||
|
||||
返回:
|
||||
str: 生成的验证码
|
||||
"""
|
||||
# 生成包含大写字母和数字的验证码
|
||||
chars = string.ascii_uppercase + string.digits
|
||||
return ''.join(random.choice(chars) for _ in range(length))
|
||||
|
||||
#发送验证码邮件到QQ邮箱
|
||||
@staticmethod
|
||||
def send_verification_email(qq_number, verification_code, email_type="register"):
|
||||
"""
|
||||
发送验证码邮件到QQ邮箱
|
||||
|
||||
参数:
|
||||
qq_number (str): 接收者QQ号
|
||||
verification_code (str): 验证码
|
||||
email_type (str): 邮件类型,"register" 或 "reset_password"
|
||||
|
||||
返回:
|
||||
bool: 发送成功返回True,否则返回False
|
||||
str: 成功或错误信息
|
||||
"""
|
||||
receiver_email = f"{qq_number}@qq.com"
|
||||
|
||||
# 根据邮件类型设置不同的内容
|
||||
if email_type == "reset_password":
|
||||
email_title = "【萌芽农场】密码重置验证码"
|
||||
email_purpose = "重置萌芽农场游戏账号密码"
|
||||
email_color = "#FF6B35" # 橙红色,表示警告性操作
|
||||
else:
|
||||
email_title = "【萌芽农场】注册验证码"
|
||||
email_purpose = "注册萌芽农场游戏账号"
|
||||
email_color = "#4CAF50" # 绿色,表示正常操作
|
||||
|
||||
# 创建邮件内容
|
||||
message = MIMEText(f'''
|
||||
<html>
|
||||
<body>
|
||||
<div style="font-family: Arial, sans-serif; color: #333;">
|
||||
<h2 style="color: {email_color};">萌芽农场 - 邮箱验证码</h2>
|
||||
<p>亲爱的玩家,您好!</p>
|
||||
<p>您正在{email_purpose},您的验证码是:</p>
|
||||
<div style="background-color: #f2f2f2; padding: 10px; font-size: 24px; font-weight: bold; color: {email_color}; text-align: center; margin: 20px 0;">
|
||||
{verification_code}
|
||||
</div>
|
||||
<p>该验证码有效期为5分钟,请勿泄露给他人。</p>
|
||||
<p>如果这不是您本人的操作,请忽略此邮件。</p>
|
||||
<p style="margin-top: 30px; font-size: 12px; color: #999;">
|
||||
本邮件由系统自动发送,请勿直接回复。
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
''', 'html', 'utf-8')
|
||||
|
||||
# 修正From头格式,符合QQ邮箱的要求
|
||||
message['From'] = SENDER_EMAIL
|
||||
message['To'] = receiver_email
|
||||
message['Subject'] = Header(email_title, 'utf-8')
|
||||
|
||||
try:
|
||||
# 使用SSL/TLS连接而不是STARTTLS
|
||||
smtp_obj = smtplib.SMTP_SSL(SMTP_SERVER, 465)
|
||||
smtp_obj.login(SENDER_EMAIL, SENDER_AUTH_CODE)
|
||||
smtp_obj.sendmail(SENDER_EMAIL, [receiver_email], message.as_string())
|
||||
smtp_obj.quit()
|
||||
return True, "验证码发送成功"
|
||||
except Exception as e:
|
||||
return False, f"发送验证码失败: {str(e)}"
|
||||
|
||||
#保存验证码到MongoDB(优先)或缓存文件(备用)
|
||||
@staticmethod
|
||||
def save_verification_code(qq_number, verification_code, expiry_time=300, code_type="register"):
|
||||
"""
|
||||
保存验证码到MongoDB(优先)或缓存文件(备用)
|
||||
|
||||
参数:
|
||||
qq_number (str): QQ号
|
||||
verification_code (str): 验证码
|
||||
expiry_time (int): 过期时间(秒),默认5分钟
|
||||
code_type (str): 验证码类型,"register" 或 "reset_password"
|
||||
|
||||
返回:
|
||||
bool: 保存成功返回True,否则返回False
|
||||
"""
|
||||
import time
|
||||
|
||||
# 优先尝试使用MongoDB
|
||||
try:
|
||||
from SMYMongoDBAPI import SMYMongoDBAPI
|
||||
import os
|
||||
# 根据环境动态选择MongoDB配置
|
||||
if os.path.exists('/.dockerenv') or os.environ.get('PRODUCTION', '').lower() == 'true':
|
||||
environment = "production"
|
||||
else:
|
||||
environment = "test"
|
||||
mongo_api = SMYMongoDBAPI(environment)
|
||||
if mongo_api.is_connected():
|
||||
success = mongo_api.save_verification_code(qq_number, verification_code, expiry_time, code_type)
|
||||
if success:
|
||||
print(f"[验证码系统-MongoDB] 为QQ {qq_number} 保存{code_type}验证码: {verification_code}")
|
||||
return True
|
||||
else:
|
||||
print(f"[验证码系统-MongoDB] 保存失败,尝试使用JSON文件")
|
||||
except Exception as e:
|
||||
print(f"[验证码系统-MongoDB] MongoDB保存失败: {str(e)},尝试使用JSON文件")
|
||||
|
||||
# MongoDB失败,使用JSON文件备用
|
||||
# 创建目录(如果不存在)
|
||||
os.makedirs(os.path.dirname(VERIFICATION_CACHE_FILE), exist_ok=True)
|
||||
|
||||
# 读取现有的验证码数据
|
||||
verification_data = {}
|
||||
if os.path.exists(VERIFICATION_CACHE_FILE):
|
||||
try:
|
||||
with open(VERIFICATION_CACHE_FILE, 'r', encoding='utf-8') as file:
|
||||
verification_data = json.load(file)
|
||||
except Exception as e:
|
||||
print(f"读取验证码文件失败: {str(e)}")
|
||||
verification_data = {}
|
||||
|
||||
# 添加新的验证码
|
||||
expire_at = time.time() + expiry_time
|
||||
current_time = time.time()
|
||||
|
||||
# 创建验证码记录,包含更多信息用于调试
|
||||
verification_data[qq_number] = {
|
||||
"code": verification_code,
|
||||
"expire_at": expire_at,
|
||||
"code_type": code_type,
|
||||
"created_at": current_time,
|
||||
"used": False # 新增:标记验证码是否已使用
|
||||
}
|
||||
|
||||
# 保存到文件
|
||||
try:
|
||||
with open(VERIFICATION_CACHE_FILE, 'w', encoding='utf-8') as file:
|
||||
json.dump(verification_data, file, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"[验证码系统-JSON] 为QQ {qq_number} 保存{code_type}验证码: {verification_code}, 过期时间: {expire_at}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"保存验证码失败: {str(e)}")
|
||||
return False
|
||||
|
||||
#验证用户输入的验证码(优先使用MongoDB)
|
||||
@staticmethod
|
||||
def verify_code(qq_number, input_code, code_type="register"):
|
||||
"""
|
||||
验证用户输入的验证码(优先使用MongoDB)
|
||||
|
||||
参数:
|
||||
qq_number (str): QQ号
|
||||
input_code (str): 用户输入的验证码
|
||||
code_type (str): 验证码类型,"register" 或 "reset_password"
|
||||
|
||||
返回:
|
||||
bool: 验证成功返回True,否则返回False
|
||||
str: 成功或错误信息
|
||||
"""
|
||||
import time
|
||||
|
||||
# 优先尝试使用MongoDB
|
||||
try:
|
||||
from SMYMongoDBAPI import SMYMongoDBAPI
|
||||
import os
|
||||
# 根据环境动态选择MongoDB配置
|
||||
if os.path.exists('/.dockerenv') or os.environ.get('PRODUCTION', '').lower() == 'true':
|
||||
environment = "production"
|
||||
else:
|
||||
environment = "test"
|
||||
mongo_api = SMYMongoDBAPI(environment)
|
||||
if mongo_api.is_connected():
|
||||
success, message = mongo_api.verify_verification_code(qq_number, input_code, code_type)
|
||||
print(f"[验证码系统-MongoDB] QQ {qq_number} 验证结果: {success}, 消息: {message}")
|
||||
return success, message
|
||||
except Exception as e:
|
||||
print(f"[验证码系统-MongoDB] MongoDB验证失败: {str(e)},尝试使用JSON文件")
|
||||
|
||||
# MongoDB失败,使用JSON文件备用
|
||||
# 检查缓存文件是否存在
|
||||
if not os.path.exists(VERIFICATION_CACHE_FILE):
|
||||
print(f"[验证码系统-JSON] QQ {qq_number} 验证失败: 缓存文件不存在")
|
||||
return False, "验证码不存在或已过期"
|
||||
|
||||
# 读取验证码数据
|
||||
try:
|
||||
with open(VERIFICATION_CACHE_FILE, 'r', encoding='utf-8') as file:
|
||||
verification_data = json.load(file)
|
||||
except Exception as e:
|
||||
print(f"[验证码系统-JSON] 读取验证码文件失败: {str(e)}")
|
||||
return False, "验证码数据损坏"
|
||||
|
||||
# 检查该QQ号是否有验证码
|
||||
if qq_number not in verification_data:
|
||||
print(f"[验证码系统-JSON] QQ {qq_number} 验证失败: 没有找到验证码记录")
|
||||
return False, "验证码不存在,请重新获取"
|
||||
|
||||
# 获取存储的验证码信息
|
||||
code_info = verification_data[qq_number]
|
||||
stored_code = code_info.get("code", "")
|
||||
expire_at = code_info.get("expire_at", 0)
|
||||
stored_code_type = code_info.get("code_type", "register")
|
||||
is_used = code_info.get("used", False)
|
||||
created_at = code_info.get("created_at", 0)
|
||||
|
||||
print(f"[验证码系统-JSON] QQ {qq_number} 验证码详情: 存储码={stored_code}, 输入码={input_code}, 类型={stored_code_type}, 已使用={is_used}, 创建时间={created_at}")
|
||||
|
||||
# 检查验证码类型是否匹配
|
||||
if stored_code_type != code_type:
|
||||
print(f"[验证码系统-JSON] QQ {qq_number} 验证失败: 验证码类型不匹配,存储类型={stored_code_type}, 请求类型={code_type}")
|
||||
return False, f"验证码类型不匹配,请重新获取{code_type}验证码"
|
||||
|
||||
# 检查验证码是否已被使用
|
||||
if is_used:
|
||||
print(f"[验证码系统-JSON] QQ {qq_number} 验证失败: 验证码已被使用")
|
||||
return False, "验证码已被使用,请重新获取"
|
||||
|
||||
# 检查验证码是否过期
|
||||
current_time = time.time()
|
||||
if current_time > expire_at:
|
||||
# 移除过期的验证码
|
||||
del verification_data[qq_number]
|
||||
with open(VERIFICATION_CACHE_FILE, 'w', encoding='utf-8') as file:
|
||||
json.dump(verification_data, file, indent=2, ensure_ascii=False)
|
||||
print(f"[验证码系统-JSON] QQ {qq_number} 验证失败: 验证码已过期")
|
||||
return False, "验证码已过期,请重新获取"
|
||||
|
||||
# 验证码比较(不区分大小写)
|
||||
if input_code.upper() == stored_code.upper():
|
||||
# 验证成功,标记为已使用而不是删除
|
||||
verification_data[qq_number]["used"] = True
|
||||
verification_data[qq_number]["used_at"] = current_time
|
||||
|
||||
try:
|
||||
with open(VERIFICATION_CACHE_FILE, 'w', encoding='utf-8') as file:
|
||||
json.dump(verification_data, file, indent=2, ensure_ascii=False)
|
||||
print(f"[验证码系统-JSON] QQ {qq_number} 验证成功: 验证码已标记为已使用")
|
||||
return True, "验证码正确"
|
||||
except Exception as e:
|
||||
print(f"[验证码系统-JSON] 标记验证码已使用时失败: {str(e)}")
|
||||
return True, "验证码正确" # 即使标记失败,验证还是成功的
|
||||
else:
|
||||
print(f"[验证码系统-JSON] QQ {qq_number} 验证失败: 验证码不匹配")
|
||||
return False, "验证码错误"
|
||||
|
||||
#清理过期的验证码和已使用的验证码(优先使用MongoDB)
|
||||
@staticmethod
|
||||
def clean_expired_codes():
|
||||
"""
|
||||
清理过期的验证码和已使用的验证码(优先使用MongoDB)
|
||||
"""
|
||||
import time
|
||||
|
||||
# 优先尝试使用MongoDB
|
||||
try:
|
||||
from SMYMongoDBAPI import SMYMongoDBAPI
|
||||
import os
|
||||
# 根据环境动态选择MongoDB配置
|
||||
if os.path.exists('/.dockerenv') or os.environ.get('PRODUCTION', '').lower() == 'true':
|
||||
environment = "production"
|
||||
else:
|
||||
environment = "test"
|
||||
mongo_api = SMYMongoDBAPI(environment)
|
||||
if mongo_api.is_connected():
|
||||
expired_count = mongo_api.clean_expired_verification_codes()
|
||||
print(f"[验证码系统-MongoDB] 清理完成,删除了 {expired_count} 个过期验证码")
|
||||
return expired_count
|
||||
except Exception as e:
|
||||
print(f"[验证码系统-MongoDB] MongoDB清理失败: {str(e)},尝试使用JSON文件")
|
||||
|
||||
# MongoDB失败,使用JSON文件备用
|
||||
if not os.path.exists(VERIFICATION_CACHE_FILE):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(VERIFICATION_CACHE_FILE, 'r', encoding='utf-8') as file:
|
||||
verification_data = json.load(file)
|
||||
|
||||
current_time = time.time()
|
||||
removed_keys = []
|
||||
|
||||
# 找出过期的验证码和已使用的验证码(超过1小时)
|
||||
for qq_number, code_info in verification_data.items():
|
||||
expire_at = code_info.get("expire_at", 0)
|
||||
is_used = code_info.get("used", False)
|
||||
used_at = code_info.get("used_at", 0)
|
||||
|
||||
should_remove = False
|
||||
|
||||
# 过期的验证码
|
||||
if current_time > expire_at:
|
||||
should_remove = True
|
||||
print(f"[验证码清理-JSON] 移除过期验证码: QQ {qq_number}")
|
||||
|
||||
# 已使用超过1小时的验证码
|
||||
elif is_used and used_at > 0 and (current_time - used_at) > 3600:
|
||||
should_remove = True
|
||||
print(f"[验证码清理-JSON] 移除已使用的验证码: QQ {qq_number}")
|
||||
|
||||
if should_remove:
|
||||
removed_keys.append(qq_number)
|
||||
|
||||
# 移除标记的验证码
|
||||
for key in removed_keys:
|
||||
del verification_data[key]
|
||||
|
||||
# 保存更新后的数据
|
||||
if removed_keys:
|
||||
with open(VERIFICATION_CACHE_FILE, 'w', encoding='utf-8') as file:
|
||||
json.dump(verification_data, file, indent=2, ensure_ascii=False)
|
||||
print(f"[验证码清理-JSON] 共清理了 {len(removed_keys)} 个验证码")
|
||||
|
||||
except Exception as e:
|
||||
print(f"清理验证码失败: {str(e)}")
|
||||
|
||||
#获取验证码状态(优先使用MongoDB)
|
||||
@staticmethod
|
||||
def get_verification_status(qq_number):
|
||||
"""
|
||||
获取验证码状态(优先使用MongoDB)
|
||||
|
||||
参数:
|
||||
qq_number (str): QQ号
|
||||
|
||||
返回:
|
||||
dict: 验证码状态信息
|
||||
"""
|
||||
import time
|
||||
|
||||
# 优先尝试使用MongoDB
|
||||
try:
|
||||
from SMYMongoDBAPI import SMYMongoDBAPI
|
||||
import os
|
||||
# 根据环境动态选择MongoDB配置
|
||||
if os.path.exists('/.dockerenv') or os.environ.get('PRODUCTION', '').lower() == 'true':
|
||||
environment = "production"
|
||||
else:
|
||||
environment = "test"
|
||||
mongo_api = SMYMongoDBAPI(environment)
|
||||
if mongo_api.is_connected():
|
||||
verification_codes = mongo_api.get_verification_codes()
|
||||
if verification_codes and qq_number in verification_codes:
|
||||
code_info = verification_codes[qq_number]
|
||||
current_time = time.time()
|
||||
|
||||
return {
|
||||
"status": "found",
|
||||
"code": code_info.get("code", ""),
|
||||
"code_type": code_info.get("code_type", "unknown"),
|
||||
"used": code_info.get("used", False),
|
||||
"expired": current_time > code_info.get("expire_at", 0),
|
||||
"created_at": code_info.get("created_at", 0),
|
||||
"expire_at": code_info.get("expire_at", 0),
|
||||
"used_at": code_info.get("used_at", 0),
|
||||
"source": "mongodb"
|
||||
}
|
||||
else:
|
||||
return {"status": "no_code", "source": "mongodb"}
|
||||
except Exception as e:
|
||||
print(f"[验证码系统-MongoDB] MongoDB状态查询失败: {str(e)},尝试使用JSON文件")
|
||||
|
||||
# MongoDB失败,使用JSON文件备用
|
||||
if not os.path.exists(VERIFICATION_CACHE_FILE):
|
||||
return {"status": "no_cache_file"}
|
||||
|
||||
try:
|
||||
with open(VERIFICATION_CACHE_FILE, 'r', encoding='utf-8') as file:
|
||||
verification_data = json.load(file)
|
||||
|
||||
if qq_number not in verification_data:
|
||||
return {"status": "no_code"}
|
||||
|
||||
code_info = verification_data[qq_number]
|
||||
current_time = time.time()
|
||||
|
||||
return {
|
||||
"status": "found",
|
||||
"code": code_info.get("code", ""),
|
||||
"code_type": code_info.get("code_type", "unknown"),
|
||||
"used": code_info.get("used", False),
|
||||
"expired": current_time > code_info.get("expire_at", 0),
|
||||
"created_at": code_info.get("created_at", 0),
|
||||
"expire_at": code_info.get("expire_at", 0),
|
||||
"used_at": code_info.get("used_at", 0),
|
||||
"source": "json"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
|
||||
# 测试邮件发送
|
||||
if __name__ == "__main__":
|
||||
# 清理过期验证码
|
||||
EmailVerification.clean_expired_codes()
|
||||
|
||||
# 生成验证码
|
||||
test_qq = input("请输入测试QQ号: ")
|
||||
verification_code = EmailVerification.generate_verification_code()
|
||||
print(f"生成的验证码: {verification_code}")
|
||||
|
||||
# 发送测试邮件
|
||||
success, message = EmailVerification.send_verification_email(test_qq, verification_code)
|
||||
print(f"发送结果: {success}, 消息: {message}")
|
||||
|
||||
if success:
|
||||
# 保存验证码
|
||||
EmailVerification.save_verification_code(test_qq, verification_code)
|
||||
|
||||
# 测试验证
|
||||
test_input = input("请输入收到的验证码: ")
|
||||
verify_success, verify_message = EmailVerification.verify_code(test_qq, test_input)
|
||||
print(f"验证结果: {verify_success}, 消息: {verify_message}")
|
||||
100
README.md
100
README.md
@@ -1,4 +1,102 @@
|
||||
# InfoGenie
|
||||
# ✨ InfoGenie 神奇万事通
|
||||
|
||||
> 🎨 一个多功能的聚合软件应用 💬
|
||||
|
||||
## 📋 项目概述
|
||||
|
||||
InfoGenie 是一个前后端分离的多功能聚合应用,提供实时数据接口、休闲游戏、AI工具等丰富功能。
|
||||
|
||||
### 🏗️ 技术架构
|
||||
|
||||
- **前端**: React + Styled Components + React Router
|
||||
- **后端**: Python Flask + MongoDB + PyMongo
|
||||
- **架构**: 前后端分离,RESTful API
|
||||
- **部署**: 支持Docker容器化部署
|
||||
|
||||
### 🌟 主要功能
|
||||
|
||||
#### 📡 60s API 模块
|
||||
- **热搜榜单**: 抖音、微博、猫眼票房、HackerNews等
|
||||
- **日更资讯**: 60秒读懂世界、必应壁纸、历史今天、汇率信息
|
||||
- **实用功能**: 天气查询、百科搜索、农历信息、二维码生成
|
||||
- **娱乐消遣**: 随机一言、音频、趣味题、文案生成
|
||||
|
||||
#### 🎮 小游戏模块
|
||||
- 经典游戏合集(开发中)
|
||||
- 移动端优化
|
||||
- 即点即玩
|
||||
|
||||
#### 🤖 AI模型模块
|
||||
- AI对话助手(开发中)
|
||||
- 智能文本生成(开发中)
|
||||
- 图像识别分析(规划中)
|
||||
- 需要登录验证
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 📋 环境要求
|
||||
|
||||
- **Python**: 3.8+
|
||||
- **Node.js**: 14+
|
||||
- **MongoDB**: 4.0+
|
||||
|
||||
### 📦 安装依赖
|
||||
|
||||
#### 后端依赖
|
||||
```bash
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### 前端依赖
|
||||
```bash
|
||||
cd frontend/react-app
|
||||
npm install
|
||||
```
|
||||
|
||||
### 🎯 启动服务
|
||||
|
||||
#### 方式一:使用启动器(推荐)
|
||||
```bash
|
||||
# 双击运行 启动器.bat
|
||||
# 选择相应的启动选项
|
||||
```
|
||||
|
||||
#### 方式二:手动启动
|
||||
|
||||
**启动后端服务**
|
||||
```bash
|
||||
cd backend
|
||||
python run.py
|
||||
# 后端服务: http://localhost:5000
|
||||
```
|
||||
|
||||
**启动前端服务**
|
||||
```bash
|
||||
cd frontend/react-app
|
||||
npm start
|
||||
# 前端服务: http://localhost:3000
|
||||
```
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
- **开发者**: 神奇万事通
|
||||
- **项目地址**: https://github.com/shumengya/InfoGenie
|
||||
- **反馈邮箱**: 请通过GitHub Issues反馈
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**✨ 感谢使用 InfoGenie 神奇万事通 ✨**
|
||||
|
||||
🎨 *一个多功能的聚合软件应用* 💬
|
||||
|
||||
</div>
|
||||
神奇万事通,一个支持Windows,Android和web的app,聚合了许多神奇有趣的功能,帮助用户一键化解决问题
|
||||
前端使用React框架,后端使用Python的Flask框架
|
||||
|
||||
|
||||
127
backend/app.py
Normal file
127
backend/app.py
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
InfoGenie 后端主应用入口
|
||||
Created by: 神奇万事通
|
||||
Date: 2025-09-02
|
||||
"""
|
||||
|
||||
from flask import Flask, jsonify, request, session, send_from_directory
|
||||
from flask_cors import CORS
|
||||
from flask_pymongo import PyMongo
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
# 导入模块
|
||||
from modules.auth import auth_bp
|
||||
from modules.api_60s import api_60s_bp
|
||||
from modules.user_management import user_bp
|
||||
from modules.email_service import init_mail
|
||||
|
||||
from config import Config
|
||||
|
||||
def create_app():
|
||||
"""创建Flask应用实例"""
|
||||
app = Flask(__name__)
|
||||
|
||||
# 加载配置
|
||||
app.config.from_object(Config)
|
||||
|
||||
# 启用CORS跨域支持
|
||||
CORS(app, supports_credentials=True)
|
||||
|
||||
# 初始化MongoDB
|
||||
mongo = PyMongo(app)
|
||||
app.mongo = mongo
|
||||
|
||||
# 初始化邮件服务
|
||||
init_mail(app)
|
||||
|
||||
# 注册蓝图
|
||||
app.register_blueprint(auth_bp, url_prefix='/api/auth')
|
||||
app.register_blueprint(api_60s_bp, url_prefix='/api/60s')
|
||||
app.register_blueprint(user_bp, url_prefix='/api/user')
|
||||
|
||||
# 基础路由
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""API根路径"""
|
||||
return jsonify({
|
||||
'message': '✨ 神奇万事通 API 服务运行中 ✨',
|
||||
'version': '1.0.0',
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'endpoints': {
|
||||
'auth': '/api/auth',
|
||||
'60s_api': '/api/60s',
|
||||
'user': '/api/user'
|
||||
}
|
||||
})
|
||||
|
||||
@app.route('/api/health')
|
||||
def health_check():
|
||||
"""健康检查接口"""
|
||||
try:
|
||||
# 检查数据库连接
|
||||
mongo.db.command('ping')
|
||||
db_status = 'connected'
|
||||
except Exception as e:
|
||||
db_status = f'error: {str(e)}'
|
||||
|
||||
return jsonify({
|
||||
'status': 'running',
|
||||
'database': db_status,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
# 60sapi静态文件服务
|
||||
@app.route('/60sapi/<path:filename>')
|
||||
def serve_60sapi_files(filename):
|
||||
"""提供60sapi目录下的静态文件服务"""
|
||||
try:
|
||||
# 获取项目根目录
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
api_directory = os.path.join(project_root, 'frontend', '60sapi')
|
||||
|
||||
# 安全检查:确保文件路径在允许的目录内
|
||||
full_path = os.path.join(api_directory, filename)
|
||||
if not os.path.commonpath([api_directory, full_path]) == api_directory:
|
||||
return jsonify({'error': '非法文件路径'}), 403
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(full_path):
|
||||
return jsonify({'error': '文件不存在'}), 404
|
||||
|
||||
# 获取文件目录和文件名
|
||||
directory = os.path.dirname(full_path)
|
||||
file_name = os.path.basename(full_path)
|
||||
|
||||
return send_from_directory(directory, file_name)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'文件服务错误: {str(e)}'}), 500
|
||||
|
||||
# 错误处理
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
return jsonify({
|
||||
'error': 'API接口不存在',
|
||||
'message': '请检查请求路径是否正确'
|
||||
}), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(error):
|
||||
return jsonify({
|
||||
'error': '服务器内部错误',
|
||||
'message': '请稍后重试或联系管理员'
|
||||
}), 500
|
||||
|
||||
return app
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
print("🚀 启动 InfoGenie 后端服务...")
|
||||
print("📡 API地址: http://localhost:5000")
|
||||
print("📚 文档地址: http://localhost:5000/api/health")
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
87
backend/config.py
Normal file
87
backend/config.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
InfoGenie 配置文件
|
||||
Created by: 神奇万事通
|
||||
Date: 2025-09-02
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
class Config:
|
||||
"""应用配置类"""
|
||||
|
||||
# 基础配置
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'infogenie-secret-key-2025'
|
||||
|
||||
# MongoDB 配置
|
||||
MONGO_URI = os.environ.get('MONGO_URI') or 'mongodb://localhost:27017/InfoGenie'
|
||||
|
||||
# Session 配置
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(days=7) # 会话持续7天
|
||||
SESSION_COOKIE_SECURE = False # 开发环境设为False,生产环境设为True
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
|
||||
# 邮件配置
|
||||
MAIL_SERVER = 'smtp.qq.com'
|
||||
MAIL_PORT = 465
|
||||
MAIL_USE_SSL = True
|
||||
MAIL_USE_TLS = False
|
||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'your-email@qq.com'
|
||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'your-app-password'
|
||||
MAIL_DEFAULT_SENDER = ('InfoGenie 神奇万事通', os.environ.get('MAIL_USERNAME') or 'your-email@qq.com')
|
||||
|
||||
# API 配置
|
||||
API_RATE_LIMIT = '100 per hour' # API调用频率限制
|
||||
|
||||
# 外部API配置
|
||||
EXTERNAL_APIS = {
|
||||
'60s': [
|
||||
'https://60s.api.shumengya.top',
|
||||
'https://60s-cf.viki.moe',
|
||||
'https://60s.viki.moe',
|
||||
'https://60s.b23.run',
|
||||
'https://60s.114128.xyz',
|
||||
'https://60s-cf.114128.xyz'
|
||||
]
|
||||
}
|
||||
|
||||
# 应用信息
|
||||
APP_INFO = {
|
||||
'name': '✨ 神奇万事通 ✨',
|
||||
'description': '🎨 一个多功能的聚合软件应用 💬',
|
||||
'author': '👨💻 by-神奇万事通',
|
||||
'version': '1.0.0',
|
||||
'icp': '📄 蜀ICP备2025151694号'
|
||||
}
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
"""开发环境配置"""
|
||||
DEBUG = True
|
||||
TESTING = False
|
||||
|
||||
class ProductionConfig(Config):
|
||||
"""生产环境配置"""
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
||||
class TestingConfig(Config):
|
||||
"""测试环境配置"""
|
||||
DEBUG = True
|
||||
TESTING = True
|
||||
MONGO_URI = 'mongodb://localhost:27017/InfoGenie_Test'
|
||||
|
||||
# 配置字典
|
||||
config = {
|
||||
'development': DevelopmentConfig,
|
||||
'production': ProductionConfig,
|
||||
'testing': TestingConfig,
|
||||
'default': DevelopmentConfig
|
||||
}
|
||||
92
backend/md/邮件服务修复说明.md
Normal file
92
backend/md/邮件服务修复说明.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# InfoGenie 邮件服务修复说明
|
||||
|
||||
## 修复内容
|
||||
|
||||
### 问题描述
|
||||
原始的 `email_service.py` 中的邮件发送功能存在问题,无法正常发送验证码邮件。
|
||||
|
||||
### 修复方案
|
||||
参考成功的 `QQEmailSendAPI.py` 实现,对 `email_service.py` 进行了以下修复:
|
||||
|
||||
1. **SMTP连接方式优化**
|
||||
- 将 `with smtplib.SMTP_SSL()` 改为直接使用 `smtplib.SMTP_SSL()`
|
||||
- 显式调用 `smtp_obj.quit()` 关闭连接
|
||||
|
||||
2. **邮件头设置优化**
|
||||
- 确保 `From` 字段直接使用邮箱地址,不使用 `Header` 包装
|
||||
- 保持与成功实现的一致性
|
||||
|
||||
3. **错误处理增强**
|
||||
- 添加了针对 `SMTPAuthenticationError` 的专门处理
|
||||
- 添加了针对 `SMTPConnectError` 的专门处理
|
||||
- 提供更详细的错误信息
|
||||
|
||||
4. **调试信息优化**
|
||||
- 添加了适量的日志输出用于问题诊断
|
||||
- 移除了生产环境不安全的验证码返回
|
||||
|
||||
## 配置要求
|
||||
|
||||
### 环境变量
|
||||
确保设置以下环境变量:
|
||||
```bash
|
||||
MAIL_USERNAME=your-qq-email@qq.com
|
||||
MAIL_PASSWORD=your-qq-auth-code
|
||||
```
|
||||
|
||||
### QQ邮箱授权码
|
||||
1. 登录QQ邮箱
|
||||
2. 进入设置 -> 账户
|
||||
3. 开启SMTP服务
|
||||
4. 获取授权码(不是QQ密码)
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 发送验证码
|
||||
```python
|
||||
from modules.email_service import send_verification_email
|
||||
|
||||
# 发送注册验证码
|
||||
result = send_verification_email('user@qq.com', 'register')
|
||||
|
||||
# 发送登录验证码
|
||||
result = send_verification_email('user@qq.com', 'login')
|
||||
```
|
||||
|
||||
### 验证验证码
|
||||
```python
|
||||
from modules.email_service import verify_code
|
||||
|
||||
# 验证用户输入的验证码
|
||||
result = verify_code('user@qq.com', '123456')
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
运行测试脚本验证功能:
|
||||
```bash
|
||||
cd backend
|
||||
python test/test_email_fix.py
|
||||
```
|
||||
|
||||
## 支持的邮箱
|
||||
|
||||
目前仅支持QQ邮箱系列:
|
||||
- @qq.com
|
||||
- @vip.qq.com
|
||||
- @foxmail.com
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **安全性**:验证码不会在API响应中返回,仅通过邮件发送
|
||||
2. **有效期**:验证码有效期为5分钟
|
||||
3. **尝试次数**:每个验证码最多可尝试验证3次
|
||||
4. **频率限制**:建议添加发送频率限制防止滥用
|
||||
|
||||
## 修复文件
|
||||
|
||||
- `backend/modules/email_service.py` - 主要修复文件
|
||||
- `backend/test/test_email_fix.py` - 测试脚本
|
||||
- `backend/邮件服务修复说明.md` - 本说明文档
|
||||
|
||||
修复完成后,邮件发送功能已正常工作,可以成功发送注册和登录验证码邮件。
|
||||
419
backend/modules/api_60s.py
Normal file
419
backend/modules/api_60s.py
Normal file
@@ -0,0 +1,419 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
60s API模块 - 提供各种实时数据接口
|
||||
Created by: 神奇万事通
|
||||
Date: 2025-09-02
|
||||
"""
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
import time
|
||||
|
||||
api_60s_bp = Blueprint('api_60s', __name__)
|
||||
|
||||
# API配置
|
||||
API_ENDPOINTS = {
|
||||
'抖音热搜': {
|
||||
'urls': [
|
||||
'https://api.vvhan.com/api/hotlist?type=douyin',
|
||||
'https://tenapi.cn/v2/douyinhot',
|
||||
'https://api.oioweb.cn/api/common/tebie/dyhot'
|
||||
],
|
||||
'cache_time': 600 # 10分钟缓存
|
||||
},
|
||||
'微博热搜': {
|
||||
'urls': [
|
||||
'https://api.vvhan.com/api/hotlist?type=weibo',
|
||||
'https://tenapi.cn/v2/wbhot',
|
||||
'https://api.oioweb.cn/api/common/tebie/wbhot'
|
||||
],
|
||||
'cache_time': 300 # 5分钟缓存
|
||||
},
|
||||
'猫眼票房': {
|
||||
'urls': [
|
||||
'https://api.vvhan.com/api/hotlist?type=maoyan',
|
||||
'https://tenapi.cn/v2/maoyan'
|
||||
],
|
||||
'cache_time': 3600 # 1小时缓存
|
||||
},
|
||||
'网易云音乐': {
|
||||
'urls': [
|
||||
'https://api.vvhan.com/api/hotlist?type=netease',
|
||||
'https://tenapi.cn/v2/music'
|
||||
],
|
||||
'cache_time': 1800 # 30分钟缓存
|
||||
},
|
||||
'HackerNews': {
|
||||
'urls': [
|
||||
'https://api.vvhan.com/api/hotlist?type=hackernews',
|
||||
'https://hacker-news.firebaseio.com/v0/topstories.json'
|
||||
],
|
||||
'cache_time': 1800 # 30分钟缓存
|
||||
}
|
||||
}
|
||||
|
||||
# 内存缓存
|
||||
cache = {}
|
||||
|
||||
def fetch_data_with_fallback(urls, timeout=10):
|
||||
"""使用备用URL获取数据"""
|
||||
for url in urls:
|
||||
try:
|
||||
response = requests.get(url, timeout=timeout, headers={
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
})
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
print(f"URL {url} 失败: {str(e)}")
|
||||
continue
|
||||
return None
|
||||
|
||||
def get_cached_data(key, cache_time):
|
||||
"""获取缓存数据"""
|
||||
if key in cache:
|
||||
cached_time, data = cache[key]
|
||||
if datetime.now() - cached_time < timedelta(seconds=cache_time):
|
||||
return data
|
||||
return None
|
||||
|
||||
def set_cache_data(key, data):
|
||||
"""设置缓存数据"""
|
||||
cache[key] = (datetime.now(), data)
|
||||
|
||||
@api_60s_bp.route('/douyin', methods=['GET'])
|
||||
def get_douyin_hot():
|
||||
"""获取抖音热搜榜"""
|
||||
try:
|
||||
# 检查缓存
|
||||
cached = get_cached_data('douyin', API_ENDPOINTS['抖音热搜']['cache_time'])
|
||||
if cached:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': cached,
|
||||
'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'from_cache': True
|
||||
})
|
||||
|
||||
# 获取新数据
|
||||
data = fetch_data_with_fallback(API_ENDPOINTS['抖音热搜']['urls'])
|
||||
|
||||
if data:
|
||||
# 标准化数据格式
|
||||
if 'data' in data:
|
||||
hot_list = data['data']
|
||||
elif isinstance(data, list):
|
||||
hot_list = data
|
||||
else:
|
||||
hot_list = []
|
||||
|
||||
result = {
|
||||
'title': '抖音热搜榜',
|
||||
'subtitle': '实时热门话题 · 紧跟潮流趋势',
|
||||
'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'total': len(hot_list),
|
||||
'list': hot_list[:50] # 最多返回50条
|
||||
}
|
||||
|
||||
# 设置缓存
|
||||
set_cache_data('douyin', result)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': result,
|
||||
'from_cache': False
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '获取数据失败,所有数据源暂时不可用'
|
||||
}), 503
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@api_60s_bp.route('/weibo', methods=['GET'])
|
||||
def get_weibo_hot():
|
||||
"""获取微博热搜榜"""
|
||||
try:
|
||||
# 检查缓存
|
||||
cached = get_cached_data('weibo', API_ENDPOINTS['微博热搜']['cache_time'])
|
||||
if cached:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': cached,
|
||||
'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'from_cache': True
|
||||
})
|
||||
|
||||
# 获取新数据
|
||||
data = fetch_data_with_fallback(API_ENDPOINTS['微博热搜']['urls'])
|
||||
|
||||
if data:
|
||||
if 'data' in data:
|
||||
hot_list = data['data']
|
||||
elif isinstance(data, list):
|
||||
hot_list = data
|
||||
else:
|
||||
hot_list = []
|
||||
|
||||
result = {
|
||||
'title': '微博热搜榜',
|
||||
'subtitle': '热门话题 · 实时更新',
|
||||
'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'total': len(hot_list),
|
||||
'list': hot_list[:50]
|
||||
}
|
||||
|
||||
set_cache_data('weibo', result)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': result,
|
||||
'from_cache': False
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '获取数据失败,所有数据源暂时不可用'
|
||||
}), 503
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@api_60s_bp.route('/maoyan', methods=['GET'])
|
||||
def get_maoyan_box_office():
|
||||
"""获取猫眼票房排行榜"""
|
||||
try:
|
||||
cached = get_cached_data('maoyan', API_ENDPOINTS['猫眼票房']['cache_time'])
|
||||
if cached:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': cached,
|
||||
'from_cache': True
|
||||
})
|
||||
|
||||
data = fetch_data_with_fallback(API_ENDPOINTS['猫眼票房']['urls'])
|
||||
|
||||
if data:
|
||||
if 'data' in data:
|
||||
box_office_list = data['data']
|
||||
elif isinstance(data, list):
|
||||
box_office_list = data
|
||||
else:
|
||||
box_office_list = []
|
||||
|
||||
result = {
|
||||
'title': '猫眼票房排行榜',
|
||||
'subtitle': '实时票房数据',
|
||||
'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'total': len(box_office_list),
|
||||
'list': box_office_list[:20]
|
||||
}
|
||||
|
||||
set_cache_data('maoyan', result)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': result,
|
||||
'from_cache': False
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '获取数据失败'
|
||||
}), 503
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@api_60s_bp.route('/60s', methods=['GET'])
|
||||
def get_60s_news():
|
||||
"""获取每天60秒读懂世界"""
|
||||
try:
|
||||
urls = [
|
||||
'https://60s-cf.viki.moe',
|
||||
'https://60s.viki.moe',
|
||||
'https://60s.b23.run'
|
||||
]
|
||||
|
||||
data = fetch_data_with_fallback(urls)
|
||||
|
||||
if data:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'title': '每天60秒读懂世界',
|
||||
'content': data,
|
||||
'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '获取数据失败'
|
||||
}), 503
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@api_60s_bp.route('/bing-wallpaper', methods=['GET'])
|
||||
def get_bing_wallpaper():
|
||||
"""获取必应每日壁纸"""
|
||||
try:
|
||||
url = 'https://api.vvhan.com/api/bing'
|
||||
response = requests.get(url, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'title': '必应每日壁纸',
|
||||
'image_url': response.url,
|
||||
'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '获取壁纸失败'
|
||||
}), 503
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@api_60s_bp.route('/weather', methods=['GET'])
|
||||
def get_weather():
|
||||
"""获取天气信息"""
|
||||
try:
|
||||
city = request.args.get('city', '北京')
|
||||
url = f'https://api.vvhan.com/api/weather?city={city}'
|
||||
|
||||
response = requests.get(url, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': data
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '获取天气信息失败'
|
||||
}), 503
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@api_60s_bp.route('/scan-directories', methods=['GET'])
|
||||
def scan_directories():
|
||||
"""扫描60sapi目录结构"""
|
||||
try:
|
||||
import os
|
||||
|
||||
# 获取项目根目录
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
api_directory = os.path.join(project_root, 'frontend', '60sapi')
|
||||
|
||||
if not os.path.exists(api_directory):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '60sapi目录不存在'
|
||||
}), 404
|
||||
|
||||
categories = []
|
||||
|
||||
# 定义分类配置
|
||||
category_config = {
|
||||
'热搜榜单': {'color': '#66bb6a'},
|
||||
'日更资讯': {'color': '#4caf50'},
|
||||
'实用功能': {'color': '#388e3c'},
|
||||
'娱乐消遣': {'color': '#66bb6a'}
|
||||
}
|
||||
|
||||
# 颜色渐变配置
|
||||
gradient_colors = [
|
||||
'linear-gradient(135deg, #81c784 0%, #66bb6a 100%)',
|
||||
'linear-gradient(135deg, #a5d6a7 0%, #81c784 100%)',
|
||||
'linear-gradient(135deg, #c8e6c9 0%, #a5d6a7 100%)',
|
||||
'linear-gradient(135deg, #66bb6a 0%, #4caf50 100%)',
|
||||
'linear-gradient(135deg, #4caf50 0%, #388e3c 100%)'
|
||||
]
|
||||
|
||||
# 扫描目录
|
||||
for category_name in os.listdir(api_directory):
|
||||
category_path = os.path.join(api_directory, category_name)
|
||||
|
||||
if os.path.isdir(category_path) and category_name in category_config:
|
||||
apis = []
|
||||
|
||||
# 扫描分类下的模块
|
||||
for i, module_name in enumerate(os.listdir(category_path)):
|
||||
module_path = os.path.join(category_path, module_name)
|
||||
index_path = os.path.join(module_path, 'index.html')
|
||||
|
||||
if os.path.isdir(module_path) and os.path.exists(index_path):
|
||||
# 读取HTML文件获取标题
|
||||
try:
|
||||
with open(index_path, 'r', encoding='utf-8') as f:
|
||||
html_content = f.read()
|
||||
title_match = html_content.find('<title>')
|
||||
if title_match != -1:
|
||||
title_end = html_content.find('</title>', title_match)
|
||||
if title_end != -1:
|
||||
title = html_content[title_match + 7:title_end].strip()
|
||||
else:
|
||||
title = module_name
|
||||
else:
|
||||
title = module_name
|
||||
except:
|
||||
title = module_name
|
||||
|
||||
apis.append({
|
||||
'title': title,
|
||||
'description': f'{module_name}相关功能',
|
||||
'link': f'/60sapi/{category_name}/{module_name}/index.html',
|
||||
'status': 'active',
|
||||
'color': gradient_colors[i % len(gradient_colors)]
|
||||
})
|
||||
|
||||
if apis:
|
||||
categories.append({
|
||||
'title': category_name,
|
||||
'color': category_config[category_name]['color'],
|
||||
'apis': apis
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'categories': categories
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'扫描目录时出错: {str(e)}'
|
||||
}), 500
|
||||
416
backend/modules/auth.py
Normal file
416
backend/modules/auth.py
Normal file
@@ -0,0 +1,416 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
用户认证模块
|
||||
Created by: 神奇万事通
|
||||
Date: 2025-09-02
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify, session, current_app
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import hashlib
|
||||
import re
|
||||
from datetime import datetime
|
||||
from .email_service import send_verification_email, verify_code, is_qq_email, get_qq_avatar_url
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
def validate_qq_email(email):
|
||||
"""验证QQ邮箱格式"""
|
||||
return is_qq_email(email)
|
||||
|
||||
def validate_password(password):
|
||||
"""验证密码格式(6-20位)"""
|
||||
return 6 <= len(password) <= 20
|
||||
|
||||
@auth_bp.route('/send-verification', methods=['POST'])
|
||||
def send_verification():
|
||||
"""发送验证码邮件"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
email = data.get('email', '').strip()
|
||||
verification_type = data.get('type', 'register') # register, login
|
||||
|
||||
# 参数验证
|
||||
if not email:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '邮箱地址不能为空'
|
||||
}), 400
|
||||
|
||||
if not validate_qq_email(email):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '仅支持QQ邮箱(qq.com、vip.qq.com、foxmail.com)'
|
||||
}), 400
|
||||
|
||||
# 获取数据库集合
|
||||
db = current_app.mongo.db
|
||||
users_collection = db.userdata
|
||||
|
||||
# 检查邮箱是否已注册
|
||||
existing_user = users_collection.find_one({'邮箱': email})
|
||||
|
||||
if verification_type == 'register' and existing_user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '该邮箱已被注册'
|
||||
}), 409
|
||||
|
||||
if verification_type == 'login' and not existing_user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '该邮箱尚未注册'
|
||||
}), 404
|
||||
|
||||
# 发送验证码
|
||||
result = send_verification_email(email, verification_type)
|
||||
|
||||
if result['success']:
|
||||
return jsonify(result), 200
|
||||
else:
|
||||
return jsonify(result), 500
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"发送验证码失败: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '发送失败,请稍后重试'
|
||||
}), 500
|
||||
|
||||
@auth_bp.route('/verify-code', methods=['POST'])
|
||||
def verify_verification_code():
|
||||
"""验证验证码"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
email = data.get('email', '').strip()
|
||||
code = data.get('code', '').strip()
|
||||
|
||||
# 参数验证
|
||||
if not email or not code:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '邮箱和验证码不能为空'
|
||||
}), 400
|
||||
|
||||
# 验证码校验
|
||||
result = verify_code(email, code)
|
||||
|
||||
if result['success']:
|
||||
return jsonify(result), 200
|
||||
else:
|
||||
return jsonify(result), 400
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"验证码校验失败: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '验证失败,请稍后重试'
|
||||
}), 500
|
||||
|
||||
@auth_bp.route('/register', methods=['POST'])
|
||||
def register():
|
||||
"""用户注册(需要先验证邮箱)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
email = data.get('email', '').strip()
|
||||
username = data.get('username', '').strip()
|
||||
password = data.get('password', '').strip()
|
||||
code = data.get('code', '').strip()
|
||||
|
||||
# 参数验证
|
||||
if not all([email, username, password, code]):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '所有字段都不能为空'
|
||||
}), 400
|
||||
|
||||
if not validate_qq_email(email):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '仅支持QQ邮箱注册'
|
||||
}), 400
|
||||
|
||||
if not validate_password(password):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '密码长度必须在6-20位之间'
|
||||
}), 400
|
||||
|
||||
# 验证验证码
|
||||
verify_result = verify_code(email, code)
|
||||
if not verify_result['success'] or verify_result.get('type') != 'register':
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '验证码无效或已过期'
|
||||
}), 400
|
||||
|
||||
# 获取数据库集合
|
||||
db = current_app.mongo.db
|
||||
users_collection = db.userdata
|
||||
|
||||
# 检查邮箱是否已被注册
|
||||
if users_collection.find_one({'邮箱': email}):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '该邮箱已被注册'
|
||||
}), 409
|
||||
|
||||
# 检查用户名是否已被使用
|
||||
if users_collection.find_one({'用户名': username}):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '该用户名已被使用'
|
||||
}), 409
|
||||
|
||||
# 获取QQ头像
|
||||
avatar_url = get_qq_avatar_url(email)
|
||||
|
||||
# 创建新用户
|
||||
password_hash = generate_password_hash(password)
|
||||
user_data = {
|
||||
'邮箱': email,
|
||||
'用户名': username,
|
||||
'密码': password_hash,
|
||||
'头像': avatar_url,
|
||||
'注册时间': datetime.now().isoformat(),
|
||||
'最后登录': None,
|
||||
'登录次数': 0,
|
||||
'用户状态': 'active'
|
||||
}
|
||||
|
||||
result = users_collection.insert_one(user_data)
|
||||
|
||||
if result.inserted_id:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '注册成功!',
|
||||
'user': {
|
||||
'email': email,
|
||||
'username': username,
|
||||
'avatar': avatar_url
|
||||
}
|
||||
}), 201
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '注册失败,请稍后重试'
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"注册失败: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '注册失败,请稍后重试'
|
||||
}), 500
|
||||
|
||||
if existing_user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '该账号已被注册'
|
||||
}), 409
|
||||
|
||||
# 创建新用户
|
||||
password_hash = generate_password_hash(password)
|
||||
user_data = {
|
||||
'账号': account,
|
||||
'密码': password_hash,
|
||||
'注册时间': datetime.now().isoformat(),
|
||||
'最后登录': None,
|
||||
'登录次数': 0,
|
||||
'用户状态': 'active'
|
||||
}
|
||||
|
||||
result = users_collection.insert_one(user_data)
|
||||
|
||||
if result.inserted_id:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '注册成功!'
|
||||
}), 201
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '注册失败,请稍后重试'
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@auth_bp.route('/login', methods=['POST'])
|
||||
def login():
|
||||
"""用户登录(支持邮箱+验证码或邮箱+密码)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
email = data.get('email', '').strip()
|
||||
password = data.get('password', '').strip()
|
||||
code = data.get('code', '').strip()
|
||||
|
||||
# 参数验证
|
||||
if not email:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '邮箱地址不能为空'
|
||||
}), 400
|
||||
|
||||
if not validate_qq_email(email):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '仅支持QQ邮箱登录'
|
||||
}), 400
|
||||
|
||||
# 获取数据库集合
|
||||
db = current_app.mongo.db
|
||||
users_collection = db.userdata
|
||||
|
||||
# 查找用户
|
||||
user = users_collection.find_one({'邮箱': email})
|
||||
|
||||
if not user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '该邮箱尚未注册'
|
||||
}), 404
|
||||
|
||||
# 检查用户状态
|
||||
if user.get('用户状态') != 'active':
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '账号已被禁用,请联系管理员'
|
||||
}), 403
|
||||
|
||||
# 验证方式:验证码登录或密码登录
|
||||
if code:
|
||||
# 验证码登录
|
||||
verify_result = verify_code(email, code)
|
||||
if not verify_result['success'] or verify_result.get('type') != 'login':
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '验证码无效或已过期'
|
||||
}), 400
|
||||
elif password:
|
||||
# 密码登录
|
||||
if not check_password_hash(user['密码'], password):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '密码错误'
|
||||
}), 401
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请输入密码或验证码'
|
||||
}), 400
|
||||
|
||||
# 登录成功,更新用户信息
|
||||
users_collection.update_one(
|
||||
{'邮箱': email},
|
||||
{
|
||||
'$set': {'最后登录': datetime.now().isoformat()},
|
||||
'$inc': {'登录次数': 1}
|
||||
}
|
||||
)
|
||||
|
||||
# 设置会话
|
||||
session['user_id'] = str(user['_id'])
|
||||
session['email'] = email
|
||||
session['username'] = user.get('用户名', '')
|
||||
session.permanent = True
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '登录成功!',
|
||||
'user': {
|
||||
'id': str(user['_id']),
|
||||
'email': email,
|
||||
'username': user.get('用户名', ''),
|
||||
'avatar': user.get('头像', ''),
|
||||
'login_count': user.get('登录次数', 0) + 1
|
||||
}
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"登录失败: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '登录失败,请稍后重试'
|
||||
}), 500
|
||||
|
||||
# 登录成功,创建会话
|
||||
session['user_id'] = str(user['_id'])
|
||||
session['account'] = user['账号']
|
||||
session['logged_in'] = True
|
||||
|
||||
# 更新登录信息
|
||||
users_collection.update_one(
|
||||
{'_id': user['_id']},
|
||||
{
|
||||
'$set': {'最后登录': datetime.now().isoformat()},
|
||||
'$inc': {'登录次数': 1}
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '登录成功!',
|
||||
'user': {
|
||||
'account': user['账号'],
|
||||
'last_login': user.get('最后登录'),
|
||||
'login_count': user.get('登录次数', 0) + 1
|
||||
}
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@auth_bp.route('/logout', methods=['POST'])
|
||||
def logout():
|
||||
"""用户登出"""
|
||||
try:
|
||||
if 'logged_in' in session:
|
||||
session.clear()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '已成功登出'
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '用户未登录'
|
||||
}), 401
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@auth_bp.route('/check', methods=['GET'])
|
||||
def check_login():
|
||||
"""检查登录状态"""
|
||||
try:
|
||||
if session.get('logged_in'):
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'logged_in': True,
|
||||
'user': {
|
||||
'account': session.get('account'),
|
||||
'user_id': session.get('user_id')
|
||||
}
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'logged_in': False
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
276
backend/modules/email_service.py
Normal file
276
backend/modules/email_service.py
Normal file
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
邮件发送模块
|
||||
负责处理用户注册、登录验证邮件
|
||||
"""
|
||||
|
||||
import random
|
||||
import string
|
||||
import smtplib
|
||||
from datetime import datetime, timedelta
|
||||
from email.mime.text import MIMEText
|
||||
from email.header import Header
|
||||
from flask import current_app
|
||||
import logging
|
||||
import os
|
||||
|
||||
# 验证码存储(生产环境建议使用Redis)
|
||||
verification_codes = {}
|
||||
|
||||
def init_mail(app):
|
||||
"""初始化邮件配置"""
|
||||
# 使用smtplib直接发送,不需要Flask-Mail
|
||||
pass
|
||||
|
||||
def generate_verification_code(length=6):
|
||||
"""生成验证码"""
|
||||
return ''.join(random.choices(string.digits, k=length))
|
||||
|
||||
def send_verification_email(email, verification_type='register'):
|
||||
"""
|
||||
发送验证邮件
|
||||
|
||||
Args:
|
||||
email: 收件人邮箱
|
||||
verification_type: 验证类型 ('register', 'login', 'reset_password')
|
||||
|
||||
Returns:
|
||||
dict: 发送结果
|
||||
"""
|
||||
try:
|
||||
# 验证QQ邮箱格式
|
||||
if not is_qq_email(email):
|
||||
return {
|
||||
'success': False,
|
||||
'message': '仅支持QQ邮箱注册登录'
|
||||
}
|
||||
|
||||
# 生成验证码
|
||||
code = generate_verification_code()
|
||||
|
||||
# 存储验证码(5分钟有效期)
|
||||
verification_codes[email] = {
|
||||
'code': code,
|
||||
'type': verification_type,
|
||||
'expires_at': datetime.now() + timedelta(minutes=5),
|
||||
'attempts': 0
|
||||
}
|
||||
|
||||
# 获取邮件配置 - 使用与QQEmailSendAPI相同的配置
|
||||
sender_email = os.environ.get('MAIL_USERNAME', '3205788256@qq.com')
|
||||
sender_password = os.environ.get('MAIL_PASSWORD', 'szcaxvbftusqddhi')
|
||||
|
||||
# 邮件模板
|
||||
if verification_type == 'register':
|
||||
subject = '【InfoGenie】注册验证码'
|
||||
html_content = f'''
|
||||
<html>
|
||||
<body>
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1 style="color: #66bb6a; margin: 0;">InfoGenie 神奇万事通</h1>
|
||||
<p style="color: #666; font-size: 14px; margin: 5px 0;">欢迎注册InfoGenie</p>
|
||||
</div>
|
||||
|
||||
<div style="background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 100%); padding: 30px; border-radius: 15px; text-align: center;">
|
||||
<h2 style="color: #2e7d32; margin-bottom: 20px;">验证码</h2>
|
||||
<div style="background: white; padding: 20px; border-radius: 10px; margin: 20px 0;">
|
||||
<span style="font-size: 32px; font-weight: bold; color: #66bb6a; letter-spacing: 5px;">{code}</span>
|
||||
</div>
|
||||
<p style="color: #4a4a4a; margin: 15px 0;">请在5分钟内输入此验证码完成注册</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 10px;">
|
||||
<p style="color: #666; font-size: 12px; margin: 0; text-align: center;">
|
||||
如果您没有申请注册,请忽略此邮件<br>
|
||||
此验证码5分钟内有效,请勿泄露给他人
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
else: # login
|
||||
subject = '【InfoGenie】登录验证码'
|
||||
html_content = f'''
|
||||
<html>
|
||||
<body>
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1 style="color: #66bb6a; margin: 0;">InfoGenie 神奇万事通</h1>
|
||||
<p style="color: #666; font-size: 14px; margin: 5px 0;">安全登录验证</p>
|
||||
</div>
|
||||
|
||||
<div style="background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%); padding: 30px; border-radius: 15px; text-align: center;">
|
||||
<h2 style="color: white; margin-bottom: 20px;">登录验证码</h2>
|
||||
<div style="background: white; padding: 20px; border-radius: 10px; margin: 20px 0;">
|
||||
<span style="font-size: 32px; font-weight: bold; color: #66bb6a; letter-spacing: 5px;">{code}</span>
|
||||
</div>
|
||||
<p style="color: white; margin: 15px 0;">请在5分钟内输入此验证码完成登录</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 10px;">
|
||||
<p style="color: #666; font-size: 12px; margin: 0; text-align: center;">
|
||||
如果不是您本人操作,请检查账户安全<br>
|
||||
此验证码5分钟内有效,请勿泄露给他人
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
# 创建邮件 - 使用与QQEmailSendAPI相同的方式
|
||||
message = MIMEText(html_content, 'html', 'utf-8')
|
||||
message['From'] = sender_email # 直接使用邮箱地址,不使用Header包装
|
||||
message['To'] = email
|
||||
message['Subject'] = Header(subject, 'utf-8')
|
||||
|
||||
# 发送邮件 - 使用SSL端口465
|
||||
try:
|
||||
# 使用与QQEmailSendAPI相同的连接方式
|
||||
smtp_obj = smtplib.SMTP_SSL('smtp.qq.com', 465)
|
||||
smtp_obj.login(sender_email, sender_password)
|
||||
smtp_obj.sendmail(sender_email, [email], message.as_string())
|
||||
smtp_obj.quit()
|
||||
|
||||
print(f"验证码邮件发送成功: {email}")
|
||||
return {
|
||||
'success': True,
|
||||
'message': '验证码已发送到您的邮箱',
|
||||
'email': email
|
||||
}
|
||||
|
||||
except smtplib.SMTPAuthenticationError as auth_error:
|
||||
print(f"SMTP认证失败: {str(auth_error)}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': 'SMTP认证失败,请检查邮箱配置'
|
||||
}
|
||||
except smtplib.SMTPConnectError as conn_error:
|
||||
print(f"SMTP连接失败: {str(conn_error)}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': 'SMTP服务器连接失败'
|
||||
}
|
||||
except Exception as smtp_error:
|
||||
print(f"SMTP发送失败: {str(smtp_error)}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'邮件发送失败: {str(smtp_error)}'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"邮件发送失败: {str(e)}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': '邮件发送失败,请稍后重试'
|
||||
}
|
||||
|
||||
def verify_code(email, code):
|
||||
"""
|
||||
验证验证码
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
code: 验证码
|
||||
|
||||
Returns:
|
||||
dict: 验证结果
|
||||
"""
|
||||
if email not in verification_codes:
|
||||
return {
|
||||
'success': False,
|
||||
'message': '验证码不存在或已过期'
|
||||
}
|
||||
|
||||
stored_info = verification_codes[email]
|
||||
|
||||
# 检查过期时间
|
||||
if datetime.now() > stored_info['expires_at']:
|
||||
del verification_codes[email]
|
||||
return {
|
||||
'success': False,
|
||||
'message': '验证码已过期,请重新获取'
|
||||
}
|
||||
|
||||
# 检查尝试次数
|
||||
if stored_info['attempts'] >= 3:
|
||||
del verification_codes[email]
|
||||
return {
|
||||
'success': False,
|
||||
'message': '验证码输入错误次数过多,请重新获取'
|
||||
}
|
||||
|
||||
# 验证码校验
|
||||
if stored_info['code'] != code:
|
||||
stored_info['attempts'] += 1
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'验证码错误,还可尝试{3 - stored_info["attempts"]}次'
|
||||
}
|
||||
|
||||
# 验证成功,删除验证码
|
||||
verification_type = stored_info['type']
|
||||
del verification_codes[email]
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': '验证码验证成功',
|
||||
'type': verification_type
|
||||
}
|
||||
|
||||
def is_qq_email(email):
|
||||
"""
|
||||
验证是否为QQ邮箱
|
||||
|
||||
Args:
|
||||
email: 邮箱地址
|
||||
|
||||
Returns:
|
||||
bool: 是否为QQ邮箱
|
||||
"""
|
||||
if not email or '@' not in email:
|
||||
return False
|
||||
|
||||
domain = email.split('@')[1].lower()
|
||||
qq_domains = ['qq.com', 'vip.qq.com', 'foxmail.com']
|
||||
|
||||
return domain in qq_domains
|
||||
|
||||
def get_qq_avatar_url(email):
|
||||
"""
|
||||
根据QQ邮箱获取QQ头像URL
|
||||
|
||||
Args:
|
||||
email: QQ邮箱地址
|
||||
|
||||
Returns:
|
||||
str: QQ头像URL
|
||||
"""
|
||||
if not is_qq_email(email):
|
||||
return None
|
||||
|
||||
# 提取QQ号码
|
||||
qq_number = email.split('@')[0]
|
||||
|
||||
# 验证是否为纯数字(QQ号)
|
||||
if not qq_number.isdigit():
|
||||
return None
|
||||
|
||||
# 返回QQ头像API URL
|
||||
return f"http://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100"
|
||||
|
||||
def cleanup_expired_codes():
|
||||
"""清理过期的验证码"""
|
||||
current_time = datetime.now()
|
||||
expired_emails = [
|
||||
email for email, info in verification_codes.items()
|
||||
if current_time > info['expires_at']
|
||||
]
|
||||
|
||||
for email in expired_emails:
|
||||
del verification_codes[email]
|
||||
|
||||
return len(expired_emails)
|
||||
211
backend/modules/user_management.py
Normal file
211
backend/modules/user_management.py
Normal file
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
用户管理模块
|
||||
Created by: 神奇万事通
|
||||
Date: 2025-09-02
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify, session, current_app
|
||||
from datetime import datetime
|
||||
from bson import ObjectId
|
||||
|
||||
user_bp = Blueprint('user', __name__)
|
||||
|
||||
def login_required(f):
|
||||
"""登录验证装饰器"""
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not session.get('logged_in'):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请先登录'
|
||||
}), 401
|
||||
return f(*args, **kwargs)
|
||||
decorated_function.__name__ = f.__name__
|
||||
return decorated_function
|
||||
|
||||
@user_bp.route('/profile', methods=['GET'])
|
||||
@login_required
|
||||
def get_profile():
|
||||
"""获取用户资料"""
|
||||
try:
|
||||
user_id = session.get('user_id')
|
||||
users_collection = current_app.mongo.db.userdata
|
||||
|
||||
user = users_collection.find_one({'_id': ObjectId(user_id)})
|
||||
|
||||
if not user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '用户不存在'
|
||||
}), 404
|
||||
|
||||
# 返回用户信息(不包含密码)
|
||||
profile = {
|
||||
'account': user['账号'],
|
||||
'register_time': user.get('注册时间'),
|
||||
'last_login': user.get('最后登录'),
|
||||
'login_count': user.get('登录次数', 0),
|
||||
'status': user.get('用户状态', 'active')
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': profile
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@user_bp.route('/change-password', methods=['POST'])
|
||||
@login_required
|
||||
def change_password():
|
||||
"""修改密码"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
old_password = data.get('old_password', '').strip()
|
||||
new_password = data.get('new_password', '').strip()
|
||||
|
||||
if not old_password or not new_password:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '旧密码和新密码不能为空'
|
||||
}), 400
|
||||
|
||||
if len(new_password) < 6 or len(new_password) > 20:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '新密码长度必须在6-20位之间'
|
||||
}), 400
|
||||
|
||||
user_id = session.get('user_id')
|
||||
users_collection = current_app.mongo.db.userdata
|
||||
|
||||
user = users_collection.find_one({'_id': ObjectId(user_id)})
|
||||
|
||||
if not user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '用户不存在'
|
||||
}), 404
|
||||
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
# 验证旧密码
|
||||
if not check_password_hash(user['密码'], old_password):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '原密码错误'
|
||||
}), 401
|
||||
|
||||
# 更新密码
|
||||
new_password_hash = generate_password_hash(new_password)
|
||||
|
||||
result = users_collection.update_one(
|
||||
{'_id': ObjectId(user_id)},
|
||||
{'$set': {'密码': new_password_hash}}
|
||||
)
|
||||
|
||||
if result.modified_count > 0:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '密码修改成功'
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '密码修改失败'
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@user_bp.route('/stats', methods=['GET'])
|
||||
@login_required
|
||||
def get_user_stats():
|
||||
"""获取用户统计信息"""
|
||||
try:
|
||||
user_id = session.get('user_id')
|
||||
|
||||
# 这里可以添加更多统计信息,比如API调用次数等
|
||||
stats = {
|
||||
'login_today': 1, # 今日登录次数
|
||||
'api_calls_today': 0, # 今日API调用次数
|
||||
'total_api_calls': 0, # 总API调用次数
|
||||
'join_days': 1, # 加入天数
|
||||
'last_activity': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': stats
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@user_bp.route('/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_account():
|
||||
"""删除账户"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
password = data.get('password', '').strip()
|
||||
|
||||
if not password:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请输入密码确认删除'
|
||||
}), 400
|
||||
|
||||
user_id = session.get('user_id')
|
||||
users_collection = current_app.mongo.db.userdata
|
||||
|
||||
user = users_collection.find_one({'_id': ObjectId(user_id)})
|
||||
|
||||
if not user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '用户不存在'
|
||||
}), 404
|
||||
|
||||
from werkzeug.security import check_password_hash
|
||||
|
||||
# 验证密码
|
||||
if not check_password_hash(user['密码'], password):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '密码错误'
|
||||
}), 401
|
||||
|
||||
# 删除用户
|
||||
result = users_collection.delete_one({'_id': ObjectId(user_id)})
|
||||
|
||||
if result.deleted_count > 0:
|
||||
# 清除会话
|
||||
session.clear()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '账户已成功删除'
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '删除失败'
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
26
backend/requirements.txt
Normal file
26
backend/requirements.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
# InfoGenie 后端依赖包
|
||||
# Web框架
|
||||
Flask==2.3.3
|
||||
Flask-CORS==4.0.0
|
||||
|
||||
# 数据库
|
||||
Flask-PyMongo==2.3.0
|
||||
pymongo==4.5.0
|
||||
|
||||
# 密码加密
|
||||
Werkzeug==2.3.7
|
||||
|
||||
# HTTP请求
|
||||
requests==2.31.0
|
||||
|
||||
# 邮件发送
|
||||
Flask-Mail==0.9.1
|
||||
|
||||
# 数据处理
|
||||
python-dateutil==2.8.2
|
||||
|
||||
# 环境变量
|
||||
python-dotenv==1.0.0
|
||||
|
||||
# 开发工具
|
||||
flask-limiter==3.5.0 # API限流
|
||||
34
backend/test/email_test.py
Normal file
34
backend/test/email_test.py
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试注册邮件发送
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
def test_send_verification_email():
|
||||
"""测试发送验证码邮件"""
|
||||
url = "http://localhost:5000/api/auth/send-verification"
|
||||
|
||||
test_data = {
|
||||
"email": "3205788256@qq.com", # 使用配置的邮箱
|
||||
"type": "register"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=test_data)
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应: {response.json()}")
|
||||
|
||||
if response.status_code == 200:
|
||||
print("\n✅ 邮件发送成功!请检查邮箱")
|
||||
else:
|
||||
print(f"\n❌ 邮件发送失败: {response.json().get('message', '未知错误')}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 请求失败: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("📧 测试注册邮件发送...")
|
||||
test_send_verification_email()
|
||||
49
backend/test/mongo_test.py
Normal file
49
backend/test/mongo_test.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
MongoDB连接测试
|
||||
"""
|
||||
|
||||
from pymongo import MongoClient
|
||||
|
||||
def test_connection():
|
||||
# 测试不同的连接配置
|
||||
configs = [
|
||||
"mongodb://shumengya:tyh%4019900420@192.168.1.233:27017/InfoGenie",
|
||||
"mongodb://shumengya:tyh%4019900420@192.168.1.233:27017/InfoGenie?authSource=admin",
|
||||
"mongodb://shumengya:tyh%4019900420@192.168.1.233:27017/InfoGenie?authSource=InfoGenie",
|
||||
"mongodb://shumengya:tyh%4019900420@192.168.1.233:27017/?authSource=admin",
|
||||
]
|
||||
|
||||
for i, uri in enumerate(configs):
|
||||
print(f"\n测试配置 {i+1}: {uri}")
|
||||
try:
|
||||
client = MongoClient(uri, serverSelectionTimeoutMS=5000)
|
||||
client.admin.command('ping')
|
||||
print("✅ 连接成功!")
|
||||
|
||||
# 测试InfoGenie数据库
|
||||
db = client.InfoGenie
|
||||
collections = db.list_collection_names()
|
||||
print(f"数据库集合: {collections}")
|
||||
|
||||
# 测试userdata集合
|
||||
if 'userdata' in collections:
|
||||
count = db.userdata.count_documents({})
|
||||
print(f"userdata集合文档数: {count}")
|
||||
|
||||
client.close()
|
||||
return uri
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 连接失败: {str(e)}")
|
||||
|
||||
return None
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔧 测试MongoDB连接...")
|
||||
success_uri = test_connection()
|
||||
if success_uri:
|
||||
print(f"\n✅ 成功的连接字符串: {success_uri}")
|
||||
else:
|
||||
print("\n❌ 所有连接尝试都失败了")
|
||||
35
backend/test/test_email.py
Normal file
35
backend/test/test_email.py
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试邮件发送功能
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
def test_send_verification():
|
||||
"""测试发送验证码"""
|
||||
url = "http://localhost:5000/api/auth/send-verification"
|
||||
|
||||
# 测试数据
|
||||
test_data = {
|
||||
"email": "3205788256@qq.com", # 使用配置中的测试邮箱
|
||||
"type": "register"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=test_data)
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应内容: {response.json()}")
|
||||
|
||||
if response.status_code == 200:
|
||||
print("✅ 邮件发送成功!")
|
||||
else:
|
||||
print("❌ 邮件发送失败")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 请求失败: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("📧 测试邮件发送功能...")
|
||||
test_send_verification()
|
||||
81
backend/test/test_email_fix.py
Normal file
81
backend/test/test_email_fix.py
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试修复后的邮件发送功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加父目录到路径
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from modules.email_service import send_verification_email, verify_code
|
||||
|
||||
def test_email_sending():
|
||||
"""
|
||||
测试邮件发送功能
|
||||
"""
|
||||
print("=== 测试邮件发送功能 ===")
|
||||
|
||||
# 测试邮箱(请替换为你的QQ邮箱)
|
||||
test_email = "3205788256@qq.com" # 替换为实际的测试邮箱
|
||||
|
||||
print(f"正在向 {test_email} 发送注册验证码...")
|
||||
|
||||
# 发送注册验证码
|
||||
result = send_verification_email(test_email, 'register')
|
||||
|
||||
print(f"发送结果: {result}")
|
||||
|
||||
if result['success']:
|
||||
print("✅ 邮件发送成功!")
|
||||
if 'code' in result:
|
||||
print(f"验证码: {result['code']}")
|
||||
|
||||
# 测试验证码验证
|
||||
print("\n=== 测试验证码验证 ===")
|
||||
verify_result = verify_code(test_email, result['code'])
|
||||
print(f"验证结果: {verify_result}")
|
||||
|
||||
if verify_result['success']:
|
||||
print("✅ 验证码验证成功!")
|
||||
else:
|
||||
print("❌ 验证码验证失败!")
|
||||
else:
|
||||
print("❌ 邮件发送失败!")
|
||||
print(f"错误信息: {result['message']}")
|
||||
|
||||
def test_login_email():
|
||||
"""
|
||||
测试登录验证码邮件
|
||||
"""
|
||||
print("\n=== 测试登录验证码邮件 ===")
|
||||
|
||||
test_email = "3205788256@qq.com" # 替换为实际的测试邮箱
|
||||
|
||||
print(f"正在向 {test_email} 发送登录验证码...")
|
||||
|
||||
result = send_verification_email(test_email, 'login')
|
||||
|
||||
print(f"发送结果: {result}")
|
||||
|
||||
if result['success']:
|
||||
print("✅ 登录验证码邮件发送成功!")
|
||||
if 'code' in result:
|
||||
print(f"验证码: {result['code']}")
|
||||
else:
|
||||
print("❌ 登录验证码邮件发送失败!")
|
||||
print(f"错误信息: {result['message']}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("InfoGenie 邮件服务测试")
|
||||
print("=" * 50)
|
||||
|
||||
# 测试注册验证码
|
||||
test_email_sending()
|
||||
|
||||
# 测试登录验证码
|
||||
test_login_email()
|
||||
|
||||
print("\n测试完成!")
|
||||
70
backend/test/test_mongo.py
Normal file
70
backend/test/test_mongo.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试MongoDB连接
|
||||
"""
|
||||
|
||||
import os
|
||||
from pymongo import MongoClient
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
def test_mongodb_connection():
|
||||
"""测试MongoDB连接"""
|
||||
try:
|
||||
# 获取连接字符串
|
||||
mongo_uri = os.environ.get('MONGO_URI')
|
||||
print(f"连接字符串: {mongo_uri}")
|
||||
|
||||
# 创建连接
|
||||
client = MongoClient(mongo_uri)
|
||||
|
||||
# 测试连接
|
||||
client.admin.command('ping')
|
||||
print("✅ MongoDB连接成功!")
|
||||
|
||||
# 获取数据库
|
||||
db = client.InfoGenie
|
||||
print(f"数据库: {db.name}")
|
||||
|
||||
# 测试集合访问
|
||||
userdata_collection = db.userdata
|
||||
print(f"用户集合: {userdata_collection.name}")
|
||||
|
||||
# 测试查询(计算文档数量)
|
||||
count = userdata_collection.count_documents({})
|
||||
print(f"用户数据集合中有 {count} 个文档")
|
||||
|
||||
# 关闭连接
|
||||
client.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ MongoDB连接失败: {str(e)}")
|
||||
|
||||
# 尝试其他认证数据库
|
||||
print("\n尝试使用不同的认证配置...")
|
||||
try:
|
||||
# 尝试不指定认证数据库
|
||||
uri_without_auth = "mongodb://shumengya:tyh%4019900420@192.168.1.233:27017/InfoGenie"
|
||||
client2 = MongoClient(uri_without_auth)
|
||||
client2.admin.command('ping')
|
||||
print("✅ 不使用authSource连接成功!")
|
||||
client2.close()
|
||||
except Exception as e2:
|
||||
print(f"❌ 无authSource也失败: {str(e2)}")
|
||||
|
||||
# 尝试使用InfoGenie作为认证数据库
|
||||
try:
|
||||
uri_with_infogenie_auth = "mongodb://shumengya:tyh%4019900420@192.168.1.233:27017/InfoGenie?authSource=InfoGenie"
|
||||
client3 = MongoClient(uri_with_infogenie_auth)
|
||||
client3.admin.command('ping')
|
||||
print("✅ 使用InfoGenie作为authSource连接成功!")
|
||||
client3.close()
|
||||
except Exception as e3:
|
||||
print(f"❌ InfoGenie authSource也失败: {str(e3)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔧 测试MongoDB连接...")
|
||||
test_mongodb_connection()
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
233
frontend/60sapi/实用功能/公网IP地址/css/background.css
Normal file
233
frontend/60sapi/实用功能/公网IP地址/css/background.css
Normal file
@@ -0,0 +1,233 @@
|
||||
/* 动态背景样式 */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
z-index: -1;
|
||||
animation: backgroundMove 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes backgroundMove {
|
||||
0%, 100% {
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
}
|
||||
25% {
|
||||
background:
|
||||
radial-gradient(circle at 60% 30%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 30% 70%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 80%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
}
|
||||
50% {
|
||||
background:
|
||||
radial-gradient(circle at 80% 60%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 60% 70%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
}
|
||||
75% {
|
||||
background:
|
||||
radial-gradient(circle at 40% 90%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 70% 10%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 20% 60%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 浮动粒子效果 */
|
||||
.particles {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 50%;
|
||||
animation: float 15s infinite linear;
|
||||
}
|
||||
|
||||
.particle:nth-child(1) {
|
||||
left: 10%;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 12s;
|
||||
}
|
||||
|
||||
.particle:nth-child(2) {
|
||||
left: 20%;
|
||||
animation-delay: 2s;
|
||||
animation-duration: 18s;
|
||||
}
|
||||
|
||||
.particle:nth-child(3) {
|
||||
left: 30%;
|
||||
animation-delay: 4s;
|
||||
animation-duration: 15s;
|
||||
}
|
||||
|
||||
.particle:nth-child(4) {
|
||||
left: 40%;
|
||||
animation-delay: 6s;
|
||||
animation-duration: 20s;
|
||||
}
|
||||
|
||||
.particle:nth-child(5) {
|
||||
left: 50%;
|
||||
animation-delay: 8s;
|
||||
animation-duration: 14s;
|
||||
}
|
||||
|
||||
.particle:nth-child(6) {
|
||||
left: 60%;
|
||||
animation-delay: 10s;
|
||||
animation-duration: 16s;
|
||||
}
|
||||
|
||||
.particle:nth-child(7) {
|
||||
left: 70%;
|
||||
animation-delay: 12s;
|
||||
animation-duration: 22s;
|
||||
}
|
||||
|
||||
.particle:nth-child(8) {
|
||||
left: 80%;
|
||||
animation-delay: 14s;
|
||||
animation-duration: 13s;
|
||||
}
|
||||
|
||||
.particle:nth-child(9) {
|
||||
left: 90%;
|
||||
animation-delay: 16s;
|
||||
animation-duration: 19s;
|
||||
}
|
||||
|
||||
.particle:nth-child(10) {
|
||||
left: 15%;
|
||||
animation-delay: 18s;
|
||||
animation-duration: 17s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% {
|
||||
transform: translateY(100vh) rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-100px) rotate(360deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 网格背景效果 */
|
||||
.grid-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
z-index: -1;
|
||||
opacity: 0.3;
|
||||
animation: gridMove 30s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes gridMove {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(50px, 50px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 光晕效果 */
|
||||
.glow-effect {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, rgba(74, 144, 226, 0.2) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: -1;
|
||||
animation: pulse 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式背景调整 */
|
||||
@media (max-width: 768px) {
|
||||
.grid-background {
|
||||
background-size: 30px 30px;
|
||||
}
|
||||
|
||||
.glow-effect {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.particle {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.grid-background {
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.glow-effect {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.particle {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
445
frontend/60sapi/实用功能/公网IP地址/css/style.css
Normal file
445
frontend/60sapi/实用功能/公网IP地址/css/style.css
Normal file
@@ -0,0 +1,445 @@
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 容器样式 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.header {
|
||||
text-align: center;
|
||||
padding: 3rem 2rem 2rem;
|
||||
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1), rgba(80, 200, 120, 0.1));
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #4a90e2, #50c878);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 i {
|
||||
margin-right: 0.5rem;
|
||||
background: linear-gradient(135deg, #4a90e2, #50c878);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 查询按钮区域 */
|
||||
.query-section {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.query-btn {
|
||||
background: linear-gradient(135deg, #4a90e2, #50c878);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 200px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.query-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(74, 144, 226, 0.4);
|
||||
background: linear-gradient(135deg, #3a7bc8, #40a868);
|
||||
}
|
||||
|
||||
.query-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.query-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #4a90e2;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading p {
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* IP信息卡片 */
|
||||
.ip-info {
|
||||
animation: fadeInUp 0.6s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.ip-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.ip-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ip-header i {
|
||||
font-size: 1.5rem;
|
||||
color: #4a90e2;
|
||||
}
|
||||
|
||||
.ip-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.ip-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1), rgba(80, 200, 120, 0.1));
|
||||
border-radius: 15px;
|
||||
border: 2px solid rgba(74, 144, 226, 0.2);
|
||||
}
|
||||
|
||||
.ip-address {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #2c3e50;
|
||||
background: linear-gradient(135deg, #4a90e2, #50c878);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: #4a90e2;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 1rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: #3a7bc8;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.ip-details {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(248, 249, 250, 0.8);
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.detail-item:hover {
|
||||
background: rgba(74, 144, 226, 0.1);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.detail-item i {
|
||||
color: #4a90e2;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail-item .label {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.detail-item .value {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* IP地址说明区域 */
|
||||
.ip-explanation {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.ip-explanation h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.3rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.ip-explanation h3 i {
|
||||
color: #4a90e2;
|
||||
}
|
||||
|
||||
.ip-explanation p {
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: rgba(248, 249, 250, 0.8);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-item:hover {
|
||||
background: rgba(74, 144, 226, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.feature-item i {
|
||||
color: #4a90e2;
|
||||
font-size: 1.5rem;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.feature-item h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.feature-item p {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 错误信息 */
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 99, 99, 0.3);
|
||||
animation: fadeInUp 0.6s ease;
|
||||
}
|
||||
|
||||
.error-message i {
|
||||
font-size: 3rem;
|
||||
color: #ff6b6b;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-message p {
|
||||
color: #666;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background: #ff5252;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
/* 页脚 */
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: rgba(248, 249, 250, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 2rem 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.ip-card, .ip-explanation {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.ip-address {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.ip-display {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.detail-item .label {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.header h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.query-btn {
|
||||
padding: 0.875rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.ip-address {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.ip-card, .ip-explanation {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
139
frontend/60sapi/实用功能/公网IP地址/index.html
Normal file
139
frontend/60sapi/实用功能/公网IP地址/index.html
Normal file
@@ -0,0 +1,139 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>公网IP地址查询</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/background.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 头部 -->
|
||||
<header class="header">
|
||||
<h1><i class="fas fa-globe"></i> 公网IP地址查询</h1>
|
||||
<p class="subtitle">快速获取您的公网IP地址信息</p>
|
||||
</header>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 查询按钮区域 -->
|
||||
<div class="query-section">
|
||||
<button id="queryBtn" class="query-btn">
|
||||
<i class="fas fa-search"></i>
|
||||
<span>查询我的IP地址</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<div id="loading" class="loading" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
<p>正在获取IP地址信息...</p>
|
||||
</div>
|
||||
|
||||
<!-- IP信息展示区域 -->
|
||||
<div id="ip-info" class="ip-info" style="display: none;">
|
||||
<div class="ip-card">
|
||||
<div class="ip-header">
|
||||
<i class="fas fa-network-wired"></i>
|
||||
<h2>您的公网IP地址</h2>
|
||||
</div>
|
||||
<div class="ip-display">
|
||||
<span id="ip-address" class="ip-address">---.---.---.---</span>
|
||||
<button id="copyBtn" class="copy-btn" title="复制IP地址">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ip-details">
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span class="label">查询时间:</span>
|
||||
<span id="query-time" class="value">--</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-server"></i>
|
||||
<span class="label">数据来源:</span>
|
||||
<span class="value">60s.viki.moe</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<span class="label">位置信息:</span>
|
||||
<span id="location" class="value">--</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-building"></i>
|
||||
<span class="label">网络服务商:</span>
|
||||
<span id="isp" class="value">--</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-flag"></i>
|
||||
<span class="label">国家:</span>
|
||||
<span id="country" class="value">--</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-map"></i>
|
||||
<span class="label">地区:</span>
|
||||
<span id="region" class="value">--</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-city"></i>
|
||||
<span class="label">城市:</span>
|
||||
<span id="city" class="value">--</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span class="label">时区:</span>
|
||||
<span id="timezone" class="value">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP地址信息说明 -->
|
||||
<div class="ip-explanation">
|
||||
<h3><i class="fas fa-info-circle"></i> 什么是公网IP地址?</h3>
|
||||
<p>公网IP地址是您的设备在互联网上的唯一标识符,由您的网络服务提供商(ISP)分配。通过这个地址,互联网上的其他设备可以找到并与您的设备通信。</p>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
<div>
|
||||
<h4>隐私保护</h4>
|
||||
<p>了解您的IP地址有助于保护网络隐私</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<div>
|
||||
<h4>地理位置</h4>
|
||||
<p>IP地址可以大致确定您的地理位置</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-cogs"></i>
|
||||
<div>
|
||||
<h4>网络配置</h4>
|
||||
<p>用于网络故障排除和配置</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div id="error-message" class="error-message" style="display: none;">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<p>获取IP地址失败,请检查网络连接或稍后重试</p>
|
||||
<button id="retryBtn" class="retry-btn">重试</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer">
|
||||
<p>© 2024 公网IP地址查询工具 | 数据来源: 60s.viki.moe</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
315
frontend/60sapi/实用功能/公网IP地址/js/script.js
Normal file
315
frontend/60sapi/实用功能/公网IP地址/js/script.js
Normal file
@@ -0,0 +1,315 @@
|
||||
// 公网IP地址查询应用
|
||||
class IPQueryApp {
|
||||
constructor() {
|
||||
this.apiEndpoint = 'https://60s.viki.moe/v2/ip';
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.createParticles();
|
||||
this.createBackgroundElements();
|
||||
console.log('IP查询应用初始化完成');
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvents() {
|
||||
const queryBtn = document.getElementById('queryBtn');
|
||||
const retryBtn = document.getElementById('retryBtn');
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
|
||||
if (queryBtn) {
|
||||
queryBtn.addEventListener('click', () => this.queryIP());
|
||||
}
|
||||
|
||||
if (retryBtn) {
|
||||
retryBtn.addEventListener('click', () => this.queryIP());
|
||||
}
|
||||
|
||||
if (copyBtn) {
|
||||
copyBtn.addEventListener('click', () => this.copyIP());
|
||||
}
|
||||
|
||||
// 页面加载完成后自动查询一次
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(() => this.queryIP(), 500);
|
||||
});
|
||||
}
|
||||
|
||||
// 创建浮动粒子
|
||||
createParticles() {
|
||||
const particlesContainer = document.createElement('div');
|
||||
particlesContainer.className = 'particles';
|
||||
document.body.appendChild(particlesContainer);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const particle = document.createElement('div');
|
||||
particle.className = 'particle';
|
||||
particlesContainer.appendChild(particle);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建背景元素
|
||||
createBackgroundElements() {
|
||||
// 创建网格背景
|
||||
const gridBackground = document.createElement('div');
|
||||
gridBackground.className = 'grid-background';
|
||||
document.body.appendChild(gridBackground);
|
||||
|
||||
// 创建光晕效果
|
||||
const glowEffect = document.createElement('div');
|
||||
glowEffect.className = 'glow-effect';
|
||||
document.body.appendChild(glowEffect);
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
showLoading() {
|
||||
const loading = document.getElementById('loading');
|
||||
const ipInfo = document.getElementById('ipInfo');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
const queryBtn = document.getElementById('queryBtn');
|
||||
|
||||
if (loading) loading.style.display = 'block';
|
||||
if (ipInfo) ipInfo.style.display = 'none';
|
||||
if (errorMessage) errorMessage.style.display = 'none';
|
||||
if (queryBtn) {
|
||||
queryBtn.disabled = true;
|
||||
queryBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 查询中...';
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏加载状态
|
||||
hideLoading() {
|
||||
const loading = document.getElementById('loading');
|
||||
const queryBtn = document.getElementById('queryBtn');
|
||||
|
||||
if (loading) loading.style.display = 'none';
|
||||
if (queryBtn) {
|
||||
queryBtn.disabled = false;
|
||||
queryBtn.innerHTML = '<i class="fas fa-search"></i> 查询我的IP';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示错误信息
|
||||
showError(message) {
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
const ipInfo = document.getElementById('ip-info');
|
||||
|
||||
if (errorMessage) {
|
||||
errorMessage.style.display = 'block';
|
||||
const errorText = errorMessage.querySelector('p');
|
||||
if (errorText) errorText.textContent = message || '获取IP信息失败,请稍后重试';
|
||||
}
|
||||
if (ipInfo) ipInfo.style.display = 'none';
|
||||
|
||||
this.hideLoading();
|
||||
}
|
||||
|
||||
// 查询IP地址
|
||||
async queryIP() {
|
||||
try {
|
||||
this.showLoading();
|
||||
console.log('开始查询IP地址...');
|
||||
|
||||
const response = await fetch(this.apiEndpoint, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('API响应状态:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP错误: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('API返回数据:', data);
|
||||
|
||||
if (data.code === 200 && data.data) {
|
||||
this.displayIPInfo(data.data);
|
||||
} else {
|
||||
throw new Error(data.message || '获取IP信息失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('查询IP失败:', error);
|
||||
this.showError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示IP信息
|
||||
displayIPInfo(data) {
|
||||
const ipInfo = document.getElementById('ip-info');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
// 更新IP地址显示
|
||||
const ipAddressElement = document.getElementById('ip-address');
|
||||
if (ipAddressElement && data.ip) {
|
||||
ipAddressElement.textContent = data.ip;
|
||||
}
|
||||
|
||||
// 更新查询时间
|
||||
const queryTimeElement = document.getElementById('query-time');
|
||||
if (queryTimeElement) {
|
||||
const now = new Date();
|
||||
queryTimeElement.textContent = now.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
// 更新详细信息
|
||||
this.updateDetailItem('location', data.location || '未知');
|
||||
this.updateDetailItem('isp', data.isp || '未知');
|
||||
this.updateDetailItem('country', data.country || '未知');
|
||||
this.updateDetailItem('region', data.region || '未知');
|
||||
this.updateDetailItem('city', data.city || '未知');
|
||||
this.updateDetailItem('timezone', data.timezone || '未知');
|
||||
|
||||
// 显示IP信息,隐藏错误信息
|
||||
if (ipInfo) ipInfo.style.display = 'block';
|
||||
if (errorMessage) errorMessage.style.display = 'none';
|
||||
|
||||
this.hideLoading();
|
||||
console.log('IP信息显示完成');
|
||||
}
|
||||
|
||||
// 更新详细信息项
|
||||
updateDetailItem(id, value) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 复制IP地址
|
||||
async copyIP() {
|
||||
const ipAddressElement = document.getElementById('ip-address');
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
|
||||
if (!ipAddressElement || !ipAddressElement.textContent) {
|
||||
this.showToast('没有可复制的IP地址', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(ipAddressElement.textContent);
|
||||
|
||||
// 更新按钮状态
|
||||
if (copyBtn) {
|
||||
const originalHTML = copyBtn.innerHTML;
|
||||
copyBtn.innerHTML = '<i class="fas fa-check"></i>';
|
||||
copyBtn.style.background = '#50c878';
|
||||
|
||||
setTimeout(() => {
|
||||
copyBtn.innerHTML = originalHTML;
|
||||
copyBtn.style.background = '#4a90e2';
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
this.showToast('IP地址已复制到剪贴板', 'success');
|
||||
console.log('IP地址已复制:', ipAddressElement.textContent);
|
||||
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error);
|
||||
this.showToast('复制失败,请手动选择复制', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示提示消息
|
||||
showToast(message, type = 'info') {
|
||||
// 移除已存在的toast
|
||||
const existingToast = document.querySelector('.toast');
|
||||
if (existingToast) {
|
||||
existingToast.remove();
|
||||
}
|
||||
|
||||
// 创建新的toast
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.innerHTML = `
|
||||
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
|
||||
// 添加toast样式
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${type === 'success' ? '#50c878' : type === 'error' ? '#ff6b6b' : '#4a90e2'};
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
animation: slideInRight 0.3s ease;
|
||||
max-width: 300px;
|
||||
`;
|
||||
|
||||
// 添加动画样式
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes slideOutRight {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 3秒后自动移除
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOutRight 0.3s ease';
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.remove();
|
||||
}
|
||||
if (style.parentNode) {
|
||||
style.remove();
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
formatTime(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
const app = new IPQueryApp();
|
||||
|
||||
// 导出到全局作用域(用于调试)
|
||||
window.IPQueryApp = app;
|
||||
3
frontend/60sapi/实用功能/公网IP地址/接口集合.json
Normal file
3
frontend/60sapi/实用功能/公网IP地址/接口集合.json
Normal file
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"https://60s.viki.moe/v2/ip"
|
||||
]
|
||||
0
frontend/60sapi/实用功能/公网IP地址/返回接口.json
Normal file
0
frontend/60sapi/实用功能/公网IP地址/返回接口.json
Normal file
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
561
frontend/60sapi/实用功能/哈希解压压缩/css/style.css
Normal file
561
frontend/60sapi/实用功能/哈希解压压缩/css/style.css
Normal file
@@ -0,0 +1,561 @@
|
||||
/* Reset and Base Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Header Styles */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
padding: 40px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-content::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(45deg, rgba(255, 255, 255, 0.1) 0%, transparent 50%, rgba(255, 255, 255, 0.1) 100%);
|
||||
animation: shimmer 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0%, 100% { transform: translateX(-100%); }
|
||||
50% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.logo i {
|
||||
font-size: 48px;
|
||||
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
text-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Floating Shapes */
|
||||
.header-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.floating-shapes {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(45deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shape-1 {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.shape-2 {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
top: 20%;
|
||||
right: 15%;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.shape-3 {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
bottom: 15%;
|
||||
left: 20%;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
33% { transform: translateY(-20px) rotate(120deg); }
|
||||
66% { transform: translateY(10px) rotate(240deg); }
|
||||
}
|
||||
|
||||
/* Input Section */
|
||||
.input-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.input-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header i {
|
||||
font-size: 24px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#inputText {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
#inputText:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
color: #666;
|
||||
border: 1px solid #e1e5e9;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Results Section */
|
||||
.results-section {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.results-section.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.results-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4);
|
||||
}
|
||||
|
||||
.result-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.result-card .card-header h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.result-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.result-item label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(248, 250, 252, 0.8);
|
||||
border: 1px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.result-value:hover {
|
||||
background: rgba(248, 250, 252, 0.95);
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.result-value .placeholder {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #667eea;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: #5a67d8;
|
||||
}
|
||||
|
||||
/* Loading Overlay */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 4px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Toast Notification */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
background: linear-gradient(135deg, #4ecdc4, #44a08d);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transform: translateX(400px);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.toast i {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.logo i {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.results-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.input-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
left: 20px;
|
||||
transform: translateY(100px);
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.input-card,
|
||||
.result-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header h2,
|
||||
.card-header h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-size: 12px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation Classes */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-in {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
221
frontend/60sapi/实用功能/哈希解压压缩/index.html
Normal file
221
frontend/60sapi/实用功能/哈希解压压缩/index.html
Normal file
@@ -0,0 +1,221 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>多功能哈希工具 - Hash Toolkit</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header Section -->
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<div class="logo">
|
||||
<i class="fas fa-fingerprint"></i>
|
||||
<h1>Hash Toolkit</h1>
|
||||
</div>
|
||||
<p class="subtitle">多功能哈希、编码与压缩工具</p>
|
||||
</div>
|
||||
<div class="header-decoration">
|
||||
<div class="floating-shapes">
|
||||
<div class="shape shape-1"></div>
|
||||
<div class="shape shape-2"></div>
|
||||
<div class="shape shape-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Input Section -->
|
||||
<section class="input-section">
|
||||
<div class="input-card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-edit"></i>
|
||||
<h2>输入内容</h2>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<textarea
|
||||
id="inputText"
|
||||
placeholder="请输入要处理的文本内容...\n支持中文、英文、特殊字符等"
|
||||
rows="6"
|
||||
></textarea>
|
||||
<div class="input-actions">
|
||||
<button id="clearBtn" class="btn btn-secondary">
|
||||
<i class="fas fa-trash"></i>
|
||||
清空
|
||||
</button>
|
||||
<button id="processBtn" class="btn btn-primary">
|
||||
<i class="fas fa-cogs"></i>
|
||||
开始处理
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Results Section -->
|
||||
<section class="results-section" id="resultsSection">
|
||||
<div class="results-grid">
|
||||
<!-- Hash Results -->
|
||||
<div class="result-card hash-card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-hashtag"></i>
|
||||
<h3>哈希算法</h3>
|
||||
</div>
|
||||
<div class="result-items">
|
||||
<div class="result-item">
|
||||
<label>MD5</label>
|
||||
<div class="result-value" id="md5Result">
|
||||
<span class="placeholder">等待处理...</span>
|
||||
<button class="copy-btn" data-target="md5Result">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<label>SHA1</label>
|
||||
<div class="result-value" id="sha1Result">
|
||||
<span class="placeholder">等待处理...</span>
|
||||
<button class="copy-btn" data-target="sha1Result">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<label>SHA256</label>
|
||||
<div class="result-value" id="sha256Result">
|
||||
<span class="placeholder">等待处理...</span>
|
||||
<button class="copy-btn" data-target="sha256Result">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<label>SHA512</label>
|
||||
<div class="result-value" id="sha512Result">
|
||||
<span class="placeholder">等待处理...</span>
|
||||
<button class="copy-btn" data-target="sha512Result">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Encoding Results -->
|
||||
<div class="result-card encoding-card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-code"></i>
|
||||
<h3>编码转换</h3>
|
||||
</div>
|
||||
<div class="result-items">
|
||||
<div class="result-item">
|
||||
<label>Base64 编码</label>
|
||||
<div class="result-value" id="base64EncodeResult">
|
||||
<span class="placeholder">等待处理...</span>
|
||||
<button class="copy-btn" data-target="base64EncodeResult">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<label>Base64 解码</label>
|
||||
<div class="result-value" id="base64DecodeResult">
|
||||
<span class="placeholder">等待处理...</span>
|
||||
<button class="copy-btn" data-target="base64DecodeResult">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<label>URL 编码</label>
|
||||
<div class="result-value" id="urlEncodeResult">
|
||||
<span class="placeholder">等待处理...</span>
|
||||
<button class="copy-btn" data-target="urlEncodeResult">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<label>URL 解码</label>
|
||||
<div class="result-value" id="urlDecodeResult">
|
||||
<span class="placeholder">等待处理...</span>
|
||||
<button class="copy-btn" data-target="urlDecodeResult">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compression Results -->
|
||||
<div class="result-card compression-card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-compress-alt"></i>
|
||||
<h3>压缩算法</h3>
|
||||
</div>
|
||||
<div class="result-items">
|
||||
<div class="result-item">
|
||||
<label>Gzip 压缩</label>
|
||||
<div class="result-value" id="gzipCompressResult">
|
||||
<span class="placeholder">等待处理...</span>
|
||||
<button class="copy-btn" data-target="gzipCompressResult">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<label>Gzip 解压</label>
|
||||
<div class="result-value" id="gzipDecompressResult">
|
||||
<span class="placeholder">等待处理...</span>
|
||||
<button class="copy-btn" data-target="gzipDecompressResult">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<label>Deflate 压缩</label>
|
||||
<div class="result-value" id="deflateCompressResult">
|
||||
<span class="placeholder">等待处理...</span>
|
||||
<button class="copy-btn" data-target="deflateCompressResult">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<label>Brotli 压缩</label>
|
||||
<div class="result-value" id="brotliCompressResult">
|
||||
<span class="placeholder">等待处理...</span>
|
||||
<button class="copy-btn" data-target="brotliCompressResult">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div class="loading-overlay" id="loadingOverlay">
|
||||
<div class="loading-spinner">
|
||||
<div class="spinner"></div>
|
||||
<p>正在处理中...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification -->
|
||||
<div class="toast" id="toast">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span id="toastMessage">复制成功!</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
380
frontend/60sapi/实用功能/哈希解压压缩/js/script.js
Normal file
380
frontend/60sapi/实用功能/哈希解压压缩/js/script.js
Normal file
@@ -0,0 +1,380 @@
|
||||
// API配置
|
||||
const API_BASE_URL = 'https://60s.viki.moe/v2/hash';
|
||||
|
||||
// DOM元素
|
||||
const elements = {
|
||||
inputText: document.getElementById('inputText'),
|
||||
processBtn: document.getElementById('processBtn'),
|
||||
clearBtn: document.getElementById('clearBtn'),
|
||||
resultsSection: document.getElementById('resultsSection'),
|
||||
loadingOverlay: document.getElementById('loadingOverlay'),
|
||||
toast: document.getElementById('toast'),
|
||||
toastMessage: document.getElementById('toastMessage')
|
||||
};
|
||||
|
||||
// 结果元素映射
|
||||
const resultElements = {
|
||||
md5: document.getElementById('md5Result'),
|
||||
sha1: document.getElementById('sha1Result'),
|
||||
sha256: document.getElementById('sha256Result'),
|
||||
sha512: document.getElementById('sha512Result'),
|
||||
base64Encode: document.getElementById('base64EncodeResult'),
|
||||
base64Decode: document.getElementById('base64DecodeResult'),
|
||||
urlEncode: document.getElementById('urlEncodeResult'),
|
||||
urlDecode: document.getElementById('urlDecodeResult'),
|
||||
gzipCompress: document.getElementById('gzipCompressResult'),
|
||||
gzipDecompress: document.getElementById('gzipDecompressResult'),
|
||||
deflateCompress: document.getElementById('deflateCompressResult'),
|
||||
brotliCompress: document.getElementById('brotliCompressResult')
|
||||
};
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeEventListeners();
|
||||
addInputAnimation();
|
||||
});
|
||||
|
||||
// 事件监听器初始化
|
||||
function initializeEventListeners() {
|
||||
// 处理按钮点击
|
||||
elements.processBtn.addEventListener('click', handleProcess);
|
||||
|
||||
// 清空按钮点击
|
||||
elements.clearBtn.addEventListener('click', handleClear);
|
||||
|
||||
// 输入框回车键
|
||||
elements.inputText.addEventListener('keydown', function(e) {
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
handleProcess();
|
||||
}
|
||||
});
|
||||
|
||||
// 复制按钮事件委托
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.copy-btn')) {
|
||||
const copyBtn = e.target.closest('.copy-btn');
|
||||
const targetId = copyBtn.getAttribute('data-target');
|
||||
const targetElement = document.getElementById(targetId);
|
||||
const textContent = targetElement.textContent.trim();
|
||||
|
||||
if (textContent && textContent !== '等待处理...' && textContent !== '处理失败') {
|
||||
copyToClipboard(textContent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 输入框实时验证
|
||||
elements.inputText.addEventListener('input', function() {
|
||||
const hasContent = this.value.trim().length > 0;
|
||||
elements.processBtn.disabled = !hasContent;
|
||||
|
||||
if (hasContent) {
|
||||
elements.processBtn.classList.remove('disabled');
|
||||
} else {
|
||||
elements.processBtn.classList.add('disabled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加输入动画效果
|
||||
function addInputAnimation() {
|
||||
elements.inputText.addEventListener('focus', function() {
|
||||
this.parentElement.classList.add('focused');
|
||||
});
|
||||
|
||||
elements.inputText.addEventListener('blur', function() {
|
||||
this.parentElement.classList.remove('focused');
|
||||
});
|
||||
}
|
||||
|
||||
// 处理主要功能
|
||||
async function handleProcess() {
|
||||
const inputValue = elements.inputText.value.trim();
|
||||
|
||||
if (!inputValue) {
|
||||
showToast('请输入要处理的内容', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
showLoading(true);
|
||||
resetResults();
|
||||
|
||||
try {
|
||||
// 调用API
|
||||
const response = await fetch(`${API_BASE_URL}?content=${encodeURIComponent(inputValue)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.code === 200 && data.data) {
|
||||
displayResults(data.data);
|
||||
showResultsSection();
|
||||
showToast('处理完成!', 'success');
|
||||
} else {
|
||||
throw new Error(data.message || '处理失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('处理错误:', error);
|
||||
showToast(`处理失败: ${error.message}`, 'error');
|
||||
displayError();
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示结果
|
||||
function displayResults(data) {
|
||||
try {
|
||||
// 哈希结果
|
||||
updateResultElement('md5', data.md5 || '不可用');
|
||||
|
||||
// SHA系列
|
||||
if (data.sha) {
|
||||
updateResultElement('sha1', data.sha.sha1 || '不可用');
|
||||
updateResultElement('sha256', data.sha.sha256 || '不可用');
|
||||
updateResultElement('sha512', data.sha.sha512 || '不可用');
|
||||
}
|
||||
|
||||
// Base64编码
|
||||
if (data.base64) {
|
||||
updateResultElement('base64Encode', data.base64.encode || '不可用');
|
||||
updateResultElement('base64Decode', data.base64.decode || '不可用');
|
||||
}
|
||||
|
||||
// URL编码
|
||||
if (data.url) {
|
||||
updateResultElement('urlEncode', data.url.encode || '不可用');
|
||||
updateResultElement('urlDecode', data.url.decode || '不可用');
|
||||
}
|
||||
|
||||
// 压缩结果
|
||||
if (data.gzip) {
|
||||
updateResultElement('gzipCompress', data.gzip.compress || '不可用');
|
||||
updateResultElement('gzipDecompress', data.gzip.decompress || '不可用');
|
||||
}
|
||||
|
||||
if (data.deflate) {
|
||||
updateResultElement('deflateCompress', data.deflate.compress || '不可用');
|
||||
}
|
||||
|
||||
if (data.brotli) {
|
||||
updateResultElement('brotliCompress', data.brotli.compress || '不可用');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('显示结果时出错:', error);
|
||||
showToast('显示结果时出错', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新单个结果元素
|
||||
function updateResultElement(key, value) {
|
||||
const element = resultElements[key];
|
||||
if (element) {
|
||||
const textSpan = element.querySelector('span') || element;
|
||||
textSpan.textContent = value;
|
||||
textSpan.classList.remove('placeholder');
|
||||
|
||||
// 添加动画效果
|
||||
element.classList.add('slide-in');
|
||||
setTimeout(() => {
|
||||
element.classList.remove('slide-in');
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// 重置结果
|
||||
function resetResults() {
|
||||
Object.values(resultElements).forEach(element => {
|
||||
if (element) {
|
||||
const textSpan = element.querySelector('span') || element;
|
||||
textSpan.textContent = '等待处理...';
|
||||
textSpan.classList.add('placeholder');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示错误状态
|
||||
function displayError() {
|
||||
Object.values(resultElements).forEach(element => {
|
||||
if (element) {
|
||||
const textSpan = element.querySelector('span') || element;
|
||||
textSpan.textContent = '处理失败';
|
||||
textSpan.classList.add('placeholder');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示结果区域
|
||||
function showResultsSection() {
|
||||
elements.resultsSection.classList.add('show');
|
||||
|
||||
// 平滑滚动到结果区域
|
||||
setTimeout(() => {
|
||||
elements.resultsSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 清空功能
|
||||
function handleClear() {
|
||||
elements.inputText.value = '';
|
||||
elements.inputText.focus();
|
||||
elements.resultsSection.classList.remove('show');
|
||||
resetResults();
|
||||
elements.processBtn.disabled = true;
|
||||
elements.processBtn.classList.add('disabled');
|
||||
|
||||
showToast('内容已清空', 'info');
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
async function copyToClipboard(text) {
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
// 降级方案
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
textArea.remove();
|
||||
}
|
||||
|
||||
showToast('复制成功!', 'success');
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error);
|
||||
showToast('复制失败,请手动复制', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示/隐藏加载状态
|
||||
function showLoading(show) {
|
||||
if (show) {
|
||||
elements.loadingOverlay.classList.add('show');
|
||||
elements.processBtn.disabled = true;
|
||||
elements.processBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 处理中...';
|
||||
} else {
|
||||
elements.loadingOverlay.classList.remove('show');
|
||||
elements.processBtn.disabled = false;
|
||||
elements.processBtn.innerHTML = '<i class="fas fa-cogs"></i> 开始处理';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示提示消息
|
||||
function showToast(message, type = 'success') {
|
||||
elements.toastMessage.textContent = message;
|
||||
|
||||
// 设置图标和样式
|
||||
const icon = elements.toast.querySelector('i');
|
||||
icon.className = getToastIcon(type);
|
||||
|
||||
elements.toast.className = `toast ${type}`;
|
||||
elements.toast.classList.add('show');
|
||||
|
||||
// 自动隐藏
|
||||
setTimeout(() => {
|
||||
elements.toast.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 获取提示图标
|
||||
function getToastIcon(type) {
|
||||
const icons = {
|
||||
success: 'fas fa-check-circle',
|
||||
error: 'fas fa-exclamation-circle',
|
||||
info: 'fas fa-info-circle',
|
||||
warning: 'fas fa-exclamation-triangle'
|
||||
};
|
||||
return icons[type] || icons.success;
|
||||
}
|
||||
|
||||
// 工具函数:防抖
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// 工具函数:节流
|
||||
function throttle(func, limit) {
|
||||
let inThrottle;
|
||||
return function() {
|
||||
const args = arguments;
|
||||
const context = this;
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加键盘快捷键支持
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// Ctrl+Enter 处理
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (!elements.processBtn.disabled) {
|
||||
handleProcess();
|
||||
}
|
||||
}
|
||||
|
||||
// Escape 清空
|
||||
if (e.key === 'Escape') {
|
||||
handleClear();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面可见性变化处理
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.hidden) {
|
||||
// 页面隐藏时的处理
|
||||
console.log('页面已隐藏');
|
||||
} else {
|
||||
// 页面显示时的处理
|
||||
console.log('页面已显示');
|
||||
}
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
window.addEventListener('error', function(e) {
|
||||
console.error('全局错误:', e.error);
|
||||
showToast('发生未知错误,请刷新页面重试', 'error');
|
||||
});
|
||||
|
||||
// 未处理的Promise拒绝
|
||||
window.addEventListener('unhandledrejection', function(e) {
|
||||
console.error('未处理的Promise拒绝:', e.reason);
|
||||
showToast('网络请求失败,请检查网络连接', 'error');
|
||||
});
|
||||
|
||||
// 导出函数供测试使用
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
handleProcess,
|
||||
copyToClipboard,
|
||||
showToast,
|
||||
debounce,
|
||||
throttle
|
||||
};
|
||||
}
|
||||
3
frontend/60sapi/实用功能/哈希解压压缩/接口集合.json
Normal file
3
frontend/60sapi/实用功能/哈希解压压缩/接口集合.json
Normal file
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
33
frontend/60sapi/实用功能/哈希解压压缩/返回接口.json
Normal file
33
frontend/60sapi/实用功能/哈希解压压缩/返回接口.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "处理成功",
|
||||
"data": {
|
||||
"source": "你好👋",
|
||||
"md5": "a1b2c3d4e5f6789012345678901234567",
|
||||
"sha": {
|
||||
"sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"sha512": "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
|
||||
},
|
||||
"base64": {
|
||||
"encode": "5L2g5aW9",
|
||||
"decode": "你好"
|
||||
},
|
||||
"url": {
|
||||
"encode": "%E4%BD%A0%E5%A5%BD%F0%9F%91%8B",
|
||||
"decode": "你好👋"
|
||||
},
|
||||
"gzip": {
|
||||
"compress": "H4sIAAAAAAAAA...(压缩后的数据)",
|
||||
"decompress": "你好👋"
|
||||
},
|
||||
"deflate": {
|
||||
"compress": "eJwrz8kvTUlMy...(压缩后的数据)",
|
||||
"decompress": "你好👋"
|
||||
},
|
||||
"brotli": {
|
||||
"compress": "CwWAaGVsbG8g...(压缩后的数据)",
|
||||
"decompress": "你好👋"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
8
frontend/60sapi/实用功能/生成要求模板.txt
Normal file
8
frontend/60sapi/实用功能/生成要求模板.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
1.生成为静态网页,js,css,html分离出来,不要混合在一起放入html里,难以阅读
|
||||
2.网页要适配手机端,电脑端和平板端三个设备分别做不同的css格式,优先优化手机端用户体验
|
||||
3.网页默认风格以淡绿色清新风格为主,除非用户要求
|
||||
4.尽量不要引用外部css,js,实在要引用就使用中国国内的cdn,否则用户可能加载不出来
|
||||
5.返回接口.json储存了网页api返回的数据格式
|
||||
6.严格按照用户要求执行,不得随意添加什么注解,如“以下数据来自...”
|
||||
7.接口集合.json保存了所有已知的后端API接口,一个访问不了尝试自动切换另一个
|
||||
8.在css中有关背景的css单独一个css文件,方便我直接迁移
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
232
frontend/60sapi/实用功能/链接OG信息/css/background.css
Normal file
232
frontend/60sapi/实用功能/链接OG信息/css/background.css
Normal file
@@ -0,0 +1,232 @@
|
||||
/* 高维度背景特效样式 - 神秘高级风格 */
|
||||
|
||||
/* 背景容器 */
|
||||
.background-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(ellipse at center,
|
||||
rgba(15, 0, 30, 0.95) 0%,
|
||||
rgba(5, 0, 15, 0.98) 50%,
|
||||
rgba(0, 0, 0, 1) 100%);
|
||||
}
|
||||
|
||||
/* 几何网格层 */
|
||||
.geometric-grid {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
linear-gradient(rgba(138, 43, 226, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(138, 43, 226, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(rgba(75, 0, 130, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(75, 0, 130, 0.05) 1px, transparent 1px);
|
||||
background-size: 100px 100px, 100px 100px, 20px 20px, 20px 20px;
|
||||
animation: gridPulse 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes gridPulse {
|
||||
0%, 100% { opacity: 0.3; transform: scale(1); }
|
||||
50% { opacity: 0.6; transform: scale(1.02); }
|
||||
}
|
||||
|
||||
/* 神经网络效果 */
|
||||
.neural-network {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(138, 43, 226, 0.15) 2px, transparent 2px),
|
||||
radial-gradient(circle at 80% 20%, rgba(75, 0, 130, 0.12) 1px, transparent 1px),
|
||||
radial-gradient(circle at 40% 70%, rgba(147, 0, 211, 0.1) 1.5px, transparent 1.5px),
|
||||
radial-gradient(circle at 90% 80%, rgba(138, 43, 226, 0.08) 1px, transparent 1px),
|
||||
radial-gradient(circle at 10% 90%, rgba(75, 0, 130, 0.1) 2px, transparent 2px);
|
||||
background-size: 200px 200px, 150px 150px, 300px 300px, 180px 180px, 250px 250px;
|
||||
animation: neuralFlow 15s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes neuralFlow {
|
||||
0% { transform: translate(0, 0) rotate(0deg); }
|
||||
25% { transform: translate(-10px, -5px) rotate(90deg); }
|
||||
50% { transform: translate(-5px, -10px) rotate(180deg); }
|
||||
75% { transform: translate(5px, -5px) rotate(270deg); }
|
||||
100% { transform: translate(0, 0) rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 粒子系统 */
|
||||
.particle-system {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
radial-gradient(circle, rgba(138, 43, 226, 0.4) 1px, transparent 1px),
|
||||
radial-gradient(circle, rgba(75, 0, 130, 0.3) 0.5px, transparent 0.5px),
|
||||
radial-gradient(circle, rgba(147, 0, 211, 0.2) 0.8px, transparent 0.8px);
|
||||
background-size: 80px 80px, 120px 120px, 160px 160px;
|
||||
background-position: 0 0, 40px 40px, 80px 80px;
|
||||
animation: particleFloat 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes particleFloat {
|
||||
0%, 100% { transform: translateY(0px) translateX(0px); }
|
||||
25% { transform: translateY(-20px) translateX(10px); }
|
||||
50% { transform: translateY(-10px) translateX(-15px); }
|
||||
75% { transform: translateY(-30px) translateX(5px); }
|
||||
}
|
||||
|
||||
/* 扫描线效果 */
|
||||
.scan-lines {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent 0px,
|
||||
transparent 2px,
|
||||
rgba(138, 43, 226, 0.03) 2px,
|
||||
rgba(138, 43, 226, 0.03) 4px
|
||||
);
|
||||
animation: scanMove 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes scanMove {
|
||||
0% { transform: translateY(-100%); }
|
||||
100% { transform: translateY(100%); }
|
||||
}
|
||||
|
||||
/* 全息投影效果 */
|
||||
.holographic-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
linear-gradient(45deg,
|
||||
transparent 30%,
|
||||
rgba(138, 43, 226, 0.05) 50%,
|
||||
transparent 70%),
|
||||
linear-gradient(-45deg,
|
||||
transparent 30%,
|
||||
rgba(75, 0, 130, 0.03) 50%,
|
||||
transparent 70%);
|
||||
background-size: 200px 200px, 150px 150px;
|
||||
animation: holographicShift 12s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes holographicShift {
|
||||
0%, 100% {
|
||||
background-position: 0% 0%, 100% 100%;
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 100%, 0% 0%;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 数据流效果 */
|
||||
.data-stream {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(138, 43, 226, 0.1) 50%,
|
||||
transparent 100%);
|
||||
background-size: 300px 100%;
|
||||
animation: dataFlow 8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dataFlow {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
/* 量子波动效果 */
|
||||
.quantum-waves {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(ellipse 200px 100px at 50% 0%,
|
||||
rgba(138, 43, 226, 0.1) 0%,
|
||||
transparent 50%),
|
||||
radial-gradient(ellipse 300px 150px at 50% 100%,
|
||||
rgba(75, 0, 130, 0.08) 0%,
|
||||
transparent 50%);
|
||||
animation: quantumPulse 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes quantumPulse {
|
||||
0%, 100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1) rotate(180deg);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式优化 */
|
||||
@media (max-width: 768px) {
|
||||
.geometric-grid {
|
||||
background-size: 50px 50px, 50px 50px, 10px 10px, 10px 10px;
|
||||
}
|
||||
|
||||
.neural-network {
|
||||
background-size: 100px 100px, 75px 75px, 150px 150px, 90px 90px, 125px 125px;
|
||||
}
|
||||
|
||||
.particle-system {
|
||||
background-size: 40px 40px, 60px 60px, 80px 80px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动画偏好 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.geometric-grid,
|
||||
.neural-network,
|
||||
.particle-system,
|
||||
.scan-lines,
|
||||
.holographic-overlay,
|
||||
.data-stream,
|
||||
.quantum-waves {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 高对比度模式 */
|
||||
@media (prefers-contrast: high) {
|
||||
.background-container {
|
||||
background: radial-gradient(ellipse at center,
|
||||
rgba(25, 0, 50, 0.95) 0%,
|
||||
rgba(10, 0, 25, 0.98) 50%,
|
||||
rgba(0, 0, 0, 1) 100%);
|
||||
}
|
||||
|
||||
.geometric-grid {
|
||||
background-image:
|
||||
linear-gradient(rgba(200, 100, 255, 0.2) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(200, 100, 255, 0.2) 1px, transparent 1px);
|
||||
}
|
||||
}
|
||||
1159
frontend/60sapi/实用功能/链接OG信息/css/style.css
Normal file
1159
frontend/60sapi/实用功能/链接OG信息/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
223
frontend/60sapi/实用功能/链接OG信息/index.html
Normal file
223
frontend/60sapi/实用功能/链接OG信息/index.html
Normal file
@@ -0,0 +1,223 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>链接OG信息查询 - 神秘解析器</title>
|
||||
<meta name="description" content="高级链接OG信息查询工具,解析网页元数据">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/background.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 背景特效容器 -->
|
||||
<div class="background-container">
|
||||
<div class="matrix-rain"></div>
|
||||
<div class="geometric-shapes"></div>
|
||||
<div class="neural-network"></div>
|
||||
</div>
|
||||
|
||||
<!-- 主容器 -->
|
||||
<div class="main-container">
|
||||
<!-- 头部区域 -->
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<div class="logo-section">
|
||||
<i class="fas fa-link logo-icon"></i>
|
||||
<h1 class="title">OG 解析器</h1>
|
||||
<span class="subtitle">链接元数据神秘解析</span>
|
||||
</div>
|
||||
<div class="status-indicator">
|
||||
<div class="pulse-dot"></div>
|
||||
<span class="status-text">系统就绪</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 查询区域 -->
|
||||
<section class="query-section">
|
||||
<div class="input-container">
|
||||
<div class="input-wrapper">
|
||||
<i class="fas fa-globe input-icon"></i>
|
||||
<input type="url" id="url-input" placeholder="输入链接地址进行深度解析..." class="url-input">
|
||||
<div class="input-border"></div>
|
||||
</div>
|
||||
<button id="analyze-btn" class="analyze-btn">
|
||||
<span class="btn-text">开始解析</span>
|
||||
<div class="btn-effects">
|
||||
<div class="ripple"></div>
|
||||
<div class="glow"></div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div id="loading" class="loading-container" style="display: none;">
|
||||
<div class="loading-content">
|
||||
<div class="scanner">
|
||||
<div class="scanner-line"></div>
|
||||
<div class="scanner-grid">
|
||||
<div class="grid-line"></div>
|
||||
<div class="grid-line"></div>
|
||||
<div class="grid-line"></div>
|
||||
<div class="grid-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-text">
|
||||
<span class="loading-title">正在解析链接</span>
|
||||
<span class="loading-subtitle">深度扫描元数据中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果展示区域 -->
|
||||
<section id="results" class="results-section" style="display: none;">
|
||||
<div class="results-header">
|
||||
<h2 class="results-title">
|
||||
<i class="fas fa-chart-network"></i>
|
||||
解析结果
|
||||
</h2>
|
||||
<div class="results-actions">
|
||||
<button id="copy-btn" class="action-btn">
|
||||
<i class="fas fa-copy"></i>
|
||||
<span>复制数据</span>
|
||||
</button>
|
||||
<button id="clear-btn" class="action-btn">
|
||||
<i class="fas fa-trash"></i>
|
||||
<span>清除结果</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="og-card">
|
||||
<!-- 基础信息 -->
|
||||
<div class="info-section basic-info">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span>基础信息</span>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>标题</label>
|
||||
<div id="og-title" class="info-value">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>描述</label>
|
||||
<div id="og-description" class="info-value">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>网站名称</label>
|
||||
<div id="og-site-name" class="info-value">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>类型</label>
|
||||
<div id="og-type" class="info-value">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 媒体信息 -->
|
||||
<div class="info-section media-info">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-image"></i>
|
||||
<span>媒体信息</span>
|
||||
</div>
|
||||
<div class="media-preview" id="media-preview">
|
||||
<div class="no-media">
|
||||
<i class="fas fa-image-slash"></i>
|
||||
<span>暂无媒体内容</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-details">
|
||||
<div class="info-item">
|
||||
<label>图片URL</label>
|
||||
<div id="og-image" class="info-value url-value">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>图片尺寸</label>
|
||||
<div id="og-image-size" class="info-value">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 技术信息 -->
|
||||
<div class="info-section tech-info">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-code"></i>
|
||||
<span>技术信息</span>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>URL</label>
|
||||
<div id="og-url" class="info-value url-value">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>域名</label>
|
||||
<div id="og-domain" class="info-value">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>语言</label>
|
||||
<div id="og-locale" class="info-value">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>字符编码</label>
|
||||
<div id="og-charset" class="info-value">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div id="error" class="error-container" style="display: none;">
|
||||
<div class="error-content">
|
||||
<div class="error-icon">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<div class="error-text">
|
||||
<h3 class="error-title">解析失败</h3>
|
||||
<p id="error-message" class="error-message">未知错误</p>
|
||||
</div>
|
||||
<button id="retryBtn" class="retry-btn">
|
||||
<i class="fas fa-redo"></i>
|
||||
<span>重新尝试</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示消息 -->
|
||||
<div id="tip-message" class="tip-container">
|
||||
<div class="tip-content">
|
||||
<i class="fas fa-lightbulb tip-icon"></i>
|
||||
<span class="tip-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast消息 -->
|
||||
<div id="toast" class="toast-container">
|
||||
<div class="toast-content">
|
||||
<i class="toast-icon"></i>
|
||||
<span class="toast-message"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<p class="footer-text">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
高级链接解析系统 | 神秘数据挖掘
|
||||
</p>
|
||||
<div class="footer-links">
|
||||
<span class="footer-link">隐私保护</span>
|
||||
<span class="footer-divider">|</span>
|
||||
<span class="footer-link">安全解析</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
555
frontend/60sapi/实用功能/链接OG信息/js/script.js
Normal file
555
frontend/60sapi/实用功能/链接OG信息/js/script.js
Normal file
@@ -0,0 +1,555 @@
|
||||
// 链接OG信息查询 - JavaScript功能代码
|
||||
// 神秘高级风格的交互体验
|
||||
|
||||
class OGAnalyzer {
|
||||
constructor() {
|
||||
this.apiUrl = 'https://60s.viki.moe/v2/og';
|
||||
this.isAnalyzing = false;
|
||||
this.currentUrl = '';
|
||||
this.animationFrameId = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.createBackgroundEffects();
|
||||
this.initializeAnimations();
|
||||
this.showWelcomeMessage();
|
||||
this.initPageAnimations();
|
||||
}
|
||||
|
||||
// 初始化页面动画
|
||||
initPageAnimations() {
|
||||
// 延迟添加动画类,确保CSS已加载
|
||||
setTimeout(() => {
|
||||
const header = document.querySelector('.header');
|
||||
const querySection = document.querySelector('.query-section');
|
||||
|
||||
if (header) header.classList.add('animate-in');
|
||||
if (querySection) querySection.classList.add('animate-in');
|
||||
}, 100);
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
const urlInput = document.getElementById('url-input');
|
||||
const analyzeBtn = document.getElementById('analyze-btn');
|
||||
const copyBtn = document.getElementById('copy-btn');
|
||||
const clearBtn = document.getElementById('clear-btn');
|
||||
|
||||
// 输入框事件
|
||||
urlInput.addEventListener('input', (e) => this.handleUrlInput(e));
|
||||
urlInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter' && !this.isAnalyzing) {
|
||||
this.analyzeUrl();
|
||||
}
|
||||
});
|
||||
urlInput.addEventListener('focus', () => this.handleInputFocus());
|
||||
urlInput.addEventListener('blur', () => this.handleInputBlur());
|
||||
|
||||
// 按钮事件
|
||||
analyzeBtn.addEventListener('click', () => this.analyzeUrl());
|
||||
copyBtn.addEventListener('click', () => this.copyResults());
|
||||
clearBtn.addEventListener('click', () => this.clearResults());
|
||||
|
||||
// 键盘快捷键
|
||||
document.addEventListener('keydown', (e) => this.handleKeyboard(e));
|
||||
}
|
||||
|
||||
handleUrlInput(e) {
|
||||
const url = e.target.value.trim();
|
||||
const analyzeBtn = document.getElementById('analyze-btn');
|
||||
|
||||
if (this.isValidUrl(url)) {
|
||||
analyzeBtn.classList.add('ready');
|
||||
e.target.classList.remove('error');
|
||||
} else {
|
||||
analyzeBtn.classList.remove('ready');
|
||||
if (url.length > 0) {
|
||||
e.target.classList.add('error');
|
||||
} else {
|
||||
e.target.classList.remove('error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleInputFocus() {
|
||||
const inputContainer = document.querySelector('.input-container');
|
||||
inputContainer.classList.add('focused');
|
||||
this.createInputGlow();
|
||||
}
|
||||
|
||||
handleInputBlur() {
|
||||
const inputContainer = document.querySelector('.input-container');
|
||||
inputContainer.classList.remove('focused');
|
||||
}
|
||||
|
||||
handleKeyboard(e) {
|
||||
// Ctrl/Cmd + Enter 快速分析
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (!this.isAnalyzing) {
|
||||
this.analyzeUrl();
|
||||
}
|
||||
}
|
||||
|
||||
// Escape 清除结果
|
||||
if (e.key === 'Escape') {
|
||||
this.clearResults();
|
||||
}
|
||||
}
|
||||
|
||||
isValidUrl(string) {
|
||||
try {
|
||||
const url = new URL(string);
|
||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async analyzeUrl() {
|
||||
const urlInput = document.getElementById('url-input');
|
||||
const url = urlInput.value.trim();
|
||||
|
||||
if (!this.isValidUrl(url)) {
|
||||
this.showError('请输入有效的URL地址');
|
||||
this.shakeInput();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isAnalyzing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentUrl = url;
|
||||
this.isAnalyzing = true;
|
||||
this.showLoading();
|
||||
this.hideError();
|
||||
this.hideResults();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiUrl}?url=${encodeURIComponent(url)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.code === 200 && data.data) {
|
||||
await this.displayResults(data.data);
|
||||
this.showSuccessMessage('分析完成!');
|
||||
|
||||
// 添加按钮闪烁效果
|
||||
const analyzeBtn = document.getElementById('analyze-btn');
|
||||
analyzeBtn.classList.add('flash');
|
||||
setTimeout(() => {
|
||||
analyzeBtn.classList.remove('flash');
|
||||
}, 300);
|
||||
} else {
|
||||
throw new Error(data.message || '获取OG信息失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('分析失败:', error);
|
||||
this.showError(`分析失败: ${error.message}`);
|
||||
} finally {
|
||||
this.isAnalyzing = false;
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
const loadingElement = document.getElementById('loading');
|
||||
const analyzeBtn = document.getElementById('analyze-btn');
|
||||
|
||||
loadingElement.classList.add('active');
|
||||
analyzeBtn.disabled = true;
|
||||
analyzeBtn.textContent = '分析中...';
|
||||
|
||||
this.startScannerAnimation();
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
const loadingElement = document.getElementById('loading');
|
||||
const analyzeBtn = document.getElementById('analyze-btn');
|
||||
|
||||
loadingElement.classList.remove('active');
|
||||
analyzeBtn.disabled = false;
|
||||
analyzeBtn.textContent = '开始分析';
|
||||
|
||||
this.stopScannerAnimation();
|
||||
}
|
||||
|
||||
async displayResults(data) {
|
||||
const resultsElement = document.getElementById('results');
|
||||
const ogCard = document.getElementById('og-card');
|
||||
|
||||
// 基础信息
|
||||
this.updateElement('og-title', data.title || '未获取到标题');
|
||||
this.updateElement('og-description', data.description || '未获取到描述');
|
||||
this.updateElement('og-url', data.url || this.currentUrl);
|
||||
this.updateElement('og-site-name', data.site_name || '未知站点');
|
||||
this.updateElement('og-type', data.type || 'website');
|
||||
|
||||
// 媒体信息
|
||||
this.updateImageElement('og-image', data.image);
|
||||
this.updateElement('og-image-alt', data.image_alt || '图片描述不可用');
|
||||
|
||||
// 技术信息
|
||||
this.updateElement('og-locale', data.locale || '未指定');
|
||||
this.updateElement('og-updated-time', this.formatDate(data.updated_time));
|
||||
this.updateElement('response-time', `${Date.now() - this.startTime}ms`);
|
||||
|
||||
// 显示结果
|
||||
resultsElement.classList.add('active');
|
||||
|
||||
// 添加动画效果
|
||||
await this.animateResults();
|
||||
|
||||
// 启用操作按钮
|
||||
document.getElementById('copy-btn').disabled = false;
|
||||
document.getElementById('clear-btn').disabled = false;
|
||||
}
|
||||
|
||||
updateElement(id, content) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.textContent = content;
|
||||
}
|
||||
}
|
||||
|
||||
updateImageElement(id, imageSrc) {
|
||||
const element = document.getElementById(id);
|
||||
if (element && imageSrc) {
|
||||
element.src = imageSrc;
|
||||
element.style.display = 'block';
|
||||
element.onerror = () => {
|
||||
element.style.display = 'none';
|
||||
const placeholder = element.nextElementSibling;
|
||||
if (placeholder && placeholder.classList.contains('image-placeholder')) {
|
||||
placeholder.style.display = 'flex';
|
||||
}
|
||||
};
|
||||
} else if (element) {
|
||||
element.style.display = 'none';
|
||||
const placeholder = element.nextElementSibling;
|
||||
if (placeholder && placeholder.classList.contains('image-placeholder')) {
|
||||
placeholder.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formatDate(timestamp) {
|
||||
if (!timestamp) return '未知';
|
||||
try {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('zh-CN');
|
||||
} catch (e) {
|
||||
return '格式错误';
|
||||
}
|
||||
}
|
||||
|
||||
async animateResults() {
|
||||
const cards = document.querySelectorAll('.info-card');
|
||||
|
||||
for (let i = 0; i < cards.length; i++) {
|
||||
setTimeout(() => {
|
||||
cards[i].classList.add('animate-in');
|
||||
}, i * 100);
|
||||
}
|
||||
|
||||
// 等待动画完成
|
||||
await new Promise(resolve => setTimeout(resolve, cards.length * 100 + 300));
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const errorElement = document.getElementById('error-message');
|
||||
const errorText = errorElement.querySelector('.error-text');
|
||||
const inputContainer = document.querySelector('.input-container');
|
||||
|
||||
errorText.textContent = message;
|
||||
errorElement.classList.add('active');
|
||||
|
||||
// 添加震动效果
|
||||
if (inputContainer) {
|
||||
inputContainer.classList.add('shake');
|
||||
setTimeout(() => {
|
||||
inputContainer.classList.remove('shake');
|
||||
}, 600);
|
||||
}
|
||||
|
||||
// 自动隐藏错误信息
|
||||
setTimeout(() => {
|
||||
this.hideError();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
hideError() {
|
||||
const errorElement = document.getElementById('error-message');
|
||||
errorElement.classList.remove('active');
|
||||
}
|
||||
|
||||
hideResults() {
|
||||
const resultsElement = document.getElementById('results');
|
||||
resultsElement.classList.remove('active');
|
||||
|
||||
// 重置动画状态
|
||||
const cards = document.querySelectorAll('.info-card');
|
||||
cards.forEach(card => card.classList.remove('animate-in'));
|
||||
}
|
||||
|
||||
showSuccessMessage(message) {
|
||||
const tipElement = document.getElementById('tip-message');
|
||||
const tipText = tipElement.querySelector('.tip-text');
|
||||
|
||||
tipText.textContent = message;
|
||||
tipElement.classList.add('active');
|
||||
|
||||
setTimeout(() => {
|
||||
tipElement.classList.remove('active');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
shakeInput() {
|
||||
const inputContainer = document.querySelector('.input-container');
|
||||
inputContainer.classList.add('shake');
|
||||
|
||||
setTimeout(() => {
|
||||
inputContainer.classList.remove('shake');
|
||||
}, 600);
|
||||
}
|
||||
|
||||
copyResults() {
|
||||
const ogData = {
|
||||
title: document.getElementById('og-title').textContent,
|
||||
description: document.getElementById('og-description').textContent,
|
||||
url: document.getElementById('og-url').textContent,
|
||||
site_name: document.getElementById('og-site-name').textContent,
|
||||
type: document.getElementById('og-type').textContent,
|
||||
image: document.getElementById('og-image').src,
|
||||
locale: document.getElementById('og-locale').textContent
|
||||
};
|
||||
|
||||
const jsonString = JSON.stringify(ogData, null, 2);
|
||||
|
||||
navigator.clipboard.writeText(jsonString).then(() => {
|
||||
this.showSuccessMessage('结果已复制到剪贴板!');
|
||||
this.flashCopyButton();
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
this.showError('复制失败,请手动选择内容');
|
||||
});
|
||||
}
|
||||
|
||||
flashCopyButton() {
|
||||
const copyBtn = document.getElementById('copy-btn');
|
||||
copyBtn.classList.add('flash');
|
||||
|
||||
setTimeout(() => {
|
||||
copyBtn.classList.remove('flash');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
clearResults() {
|
||||
const urlInput = document.getElementById('url-input');
|
||||
const resultsElement = document.getElementById('results');
|
||||
const errorElement = document.getElementById('error-message');
|
||||
|
||||
urlInput.value = '';
|
||||
urlInput.classList.remove('error');
|
||||
resultsElement.classList.remove('active');
|
||||
errorElement.classList.remove('active');
|
||||
|
||||
document.getElementById('analyze-btn').classList.remove('ready');
|
||||
document.getElementById('copy-btn').disabled = true;
|
||||
document.getElementById('clear-btn').disabled = true;
|
||||
|
||||
this.currentUrl = '';
|
||||
|
||||
// 重置动画状态
|
||||
const cards = document.querySelectorAll('.info-card');
|
||||
cards.forEach(card => card.classList.remove('animate-in'));
|
||||
|
||||
this.showSuccessMessage('已清除所有内容');
|
||||
}
|
||||
|
||||
createBackgroundEffects() {
|
||||
const container = document.querySelector('.background-container');
|
||||
|
||||
// 创建各种背景效果层
|
||||
const effects = [
|
||||
'geometric-grid',
|
||||
'neural-network',
|
||||
'particle-system',
|
||||
'scan-lines',
|
||||
'holographic-overlay',
|
||||
'data-stream',
|
||||
'quantum-waves'
|
||||
];
|
||||
|
||||
effects.forEach(effectClass => {
|
||||
const layer = document.createElement('div');
|
||||
layer.className = effectClass;
|
||||
container.appendChild(layer);
|
||||
});
|
||||
}
|
||||
|
||||
createInputGlow() {
|
||||
const inputContainer = document.querySelector('.input-container');
|
||||
|
||||
// 创建光晕效果
|
||||
const glow = document.createElement('div');
|
||||
glow.className = 'input-glow';
|
||||
inputContainer.appendChild(glow);
|
||||
|
||||
setTimeout(() => {
|
||||
if (glow.parentNode) {
|
||||
glow.remove();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
startScannerAnimation() {
|
||||
const scanner = document.querySelector('.scanner');
|
||||
if (scanner) {
|
||||
scanner.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
stopScannerAnimation() {
|
||||
const scanner = document.querySelector('.scanner');
|
||||
if (scanner) {
|
||||
scanner.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
initializeAnimations() {
|
||||
// 初始化页面动画
|
||||
const header = document.querySelector('.header');
|
||||
const querySection = document.querySelector('.query-section');
|
||||
|
||||
setTimeout(() => {
|
||||
header.classList.add('animate-in');
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
querySection.classList.add('animate-in');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
showWelcomeMessage() {
|
||||
const tips = [
|
||||
'支持分析网页的标题、描述、图片等元信息',
|
||||
'可以预览社交媒体分享时的显示效果',
|
||||
'检测网页的SEO优化情况',
|
||||
'分析Open Graph协议标签'
|
||||
];
|
||||
|
||||
setTimeout(() => {
|
||||
this.showSuccessMessage('欢迎使用链接OG信息分析器!');
|
||||
}, 1000);
|
||||
|
||||
// 显示提示信息
|
||||
this.showTips(tips);
|
||||
}
|
||||
|
||||
// 显示提示信息
|
||||
showTips(tips) {
|
||||
const tipElement = document.getElementById('tip-message');
|
||||
const tipText = tipElement.querySelector('.tip-text');
|
||||
|
||||
let currentTip = 0;
|
||||
|
||||
const showNextTip = () => {
|
||||
tipText.textContent = tips[currentTip];
|
||||
tipElement.classList.add('active');
|
||||
tipElement.style.animation = 'fadeInUp 0.5s ease-out';
|
||||
|
||||
setTimeout(() => {
|
||||
tipElement.style.animation = 'fadeOutDown 0.5s ease-in';
|
||||
setTimeout(() => {
|
||||
tipElement.classList.remove('active');
|
||||
currentTip = (currentTip + 1) % tips.length;
|
||||
}, 500);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// 首次显示
|
||||
setTimeout(showNextTip, 2000);
|
||||
|
||||
// 每8秒显示一次
|
||||
setInterval(showNextTip, 8000);
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
function throttle(func, limit) {
|
||||
let inThrottle;
|
||||
return function() {
|
||||
const args = arguments;
|
||||
const context = this;
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 检查必要的DOM元素
|
||||
const requiredElements = [
|
||||
'url-input', 'analyze-btn', 'copy-btn', 'clear-btn',
|
||||
'loading', 'results', 'error-message', 'tip-message'
|
||||
];
|
||||
|
||||
const missingElements = requiredElements.filter(id => !document.getElementById(id));
|
||||
|
||||
if (missingElements.length > 0) {
|
||||
console.error('缺少必要的DOM元素:', missingElements);
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
window.ogAnalyzer = new OGAnalyzer();
|
||||
|
||||
// 添加全局错误处理
|
||||
window.addEventListener('error', (e) => {
|
||||
console.error('全局错误:', e.error);
|
||||
if (window.ogAnalyzer) {
|
||||
window.ogAnalyzer.showError('发生未知错误,请刷新页面重试');
|
||||
}
|
||||
});
|
||||
|
||||
// 添加网络状态监听
|
||||
window.addEventListener('online', () => {
|
||||
if (window.ogAnalyzer) {
|
||||
window.ogAnalyzer.showSuccessMessage('网络连接已恢复');
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
if (window.ogAnalyzer) {
|
||||
window.ogAnalyzer.showError('网络连接已断开');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 导出给其他模块使用
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { OGAnalyzer, debounce, throttle };
|
||||
}
|
||||
3
frontend/60sapi/实用功能/链接OG信息/接口集合.json
Normal file
3
frontend/60sapi/实用功能/链接OG信息/接口集合.json
Normal file
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
66
frontend/60sapi/实用功能/链接OG信息/返回接口.json
Normal file
66
frontend/60sapi/实用功能/链接OG信息/返回接口.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"url": "https://example.com",
|
||||
"title": "示例网站标题",
|
||||
"description": "这是一个示例网站的描述信息,用于展示OG标签解析功能。",
|
||||
"image": "https://example.com/og-image.jpg",
|
||||
"site_name": "示例网站",
|
||||
"type": "website",
|
||||
"locale": "zh_CN",
|
||||
"author": "网站作者",
|
||||
"keywords": "示例,网站,OG标签,元数据",
|
||||
"favicon": "https://example.com/favicon.ico",
|
||||
"canonical_url": "https://example.com",
|
||||
"robots": "index,follow",
|
||||
"viewport": "width=device-width, initial-scale=1.0",
|
||||
"charset": "UTF-8",
|
||||
"language": "zh-CN",
|
||||
"published_time": "2024-01-01T00:00:00Z",
|
||||
"modified_time": "2024-01-15T12:30:00Z",
|
||||
"section": "技术",
|
||||
"tags": ["前端", "元数据", "SEO"],
|
||||
"twitter": {
|
||||
"card": "summary_large_image",
|
||||
"site": "@example",
|
||||
"creator": "@author",
|
||||
"title": "Twitter标题",
|
||||
"description": "Twitter描述",
|
||||
"image": "https://example.com/twitter-image.jpg"
|
||||
},
|
||||
"facebook": {
|
||||
"app_id": "123456789",
|
||||
"admins": "987654321"
|
||||
},
|
||||
"structured_data": {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "示例网页",
|
||||
"description": "示例网页描述",
|
||||
"url": "https://example.com"
|
||||
},
|
||||
"meta_tags": {
|
||||
"generator": "WordPress 6.0",
|
||||
"theme-color": "#000000",
|
||||
"msapplication-TileColor": "#ffffff",
|
||||
"apple-mobile-web-app-capable": "yes",
|
||||
"apple-mobile-web-app-status-bar-style": "default"
|
||||
},
|
||||
"performance": {
|
||||
"load_time": 1.25,
|
||||
"page_size": "2.3MB",
|
||||
"requests_count": 45
|
||||
},
|
||||
"seo_score": {
|
||||
"overall": 85,
|
||||
"title_score": 90,
|
||||
"description_score": 80,
|
||||
"image_score": 85,
|
||||
"structure_score": 88
|
||||
}
|
||||
},
|
||||
"timestamp": "2024-01-15T12:30:45Z",
|
||||
"request_id": "req_123456789",
|
||||
"processing_time": 0.85
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
107
frontend/60sapi/热搜榜单/哔哩哔哩热搜榜/css/background.css
Normal file
107
frontend/60sapi/热搜榜单/哔哩哔哩热搜榜/css/background.css
Normal file
@@ -0,0 +1,107 @@
|
||||
.background-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.green-gradient {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(76, 175, 80, 0.3) 0%,
|
||||
rgba(129, 199, 132, 0.2) 25%,
|
||||
rgba(165, 214, 167, 0.1) 50%,
|
||||
rgba(200, 230, 201, 0.2) 75%,
|
||||
rgba(76, 175, 80, 0.3) 100%
|
||||
);
|
||||
animation: green-flow 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.green-gradient::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(
|
||||
circle at 30% 70%,
|
||||
rgba(139, 195, 74, 0.4) 0%,
|
||||
transparent 50%
|
||||
), radial-gradient(
|
||||
circle at 70% 30%,
|
||||
rgba(102, 187, 106, 0.3) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
animation: green-pulse 15s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes green-flow {
|
||||
0%, 100% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
25% {
|
||||
transform: rotate(90deg) scale(1.1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg) scale(0.9);
|
||||
opacity: 0.9;
|
||||
}
|
||||
75% {
|
||||
transform: rotate(270deg) scale(1.05);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes green-pulse {
|
||||
0% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2) rotate(180deg);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) rotate(360deg);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机端背景优化 */
|
||||
@media (max-width: 768px) {
|
||||
.green-gradient {
|
||||
animation-duration: 25s;
|
||||
}
|
||||
|
||||
.green-gradient::before {
|
||||
animation-duration: 18s;
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动画以节省电池 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.green-gradient,
|
||||
.green-gradient::before {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.green-gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(76, 175, 80, 0.2) 0%,
|
||||
rgba(165, 214, 167, 0.1) 50%,
|
||||
rgba(200, 230, 201, 0.15) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
248
frontend/60sapi/热搜榜单/哔哩哔哩热搜榜/css/style.css
Normal file
248
frontend/60sapi/热搜榜单/哔哩哔哩热搜榜/css/style.css
Normal file
@@ -0,0 +1,248 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
color: #333;
|
||||
background-color: #f0f8f0;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 128, 0, 0.1);
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e8f5e8;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: #2d8f47;
|
||||
margin-bottom: 12px;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(45, 143, 71, 0.1);
|
||||
}
|
||||
|
||||
.update-time {
|
||||
color: #6b8e6b;
|
||||
font-size: 0.95rem;
|
||||
background-color: #f0f8f0;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hot-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.hot-item {
|
||||
padding: 18px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 10px;
|
||||
background-color: white;
|
||||
box-shadow: 0 3px 8px rgba(0, 128, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
|
||||
.hot-item:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 16px rgba(0, 128, 0, 0.15);
|
||||
border-left-color: #4caf50;
|
||||
}
|
||||
|
||||
.hot-rank {
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
color: #4caf50;
|
||||
margin-right: 18px;
|
||||
min-width: 35px;
|
||||
text-align: center;
|
||||
background-color: #f0f8f0;
|
||||
border-radius: 50%;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hot-rank.top-1 {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hot-rank.top-2 {
|
||||
background: linear-gradient(135deg, #ffa726, #ffb74d);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hot-rank.top-3 {
|
||||
background: linear-gradient(135deg, #ffca28, #ffd54f);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hot-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.hot-title {
|
||||
font-size: 1.15rem;
|
||||
margin-bottom: 6px;
|
||||
color: #2c3e2c;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
line-height: 1.4;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hot-title:hover {
|
||||
color: #2d8f47;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #6b8e6b;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e8f5e8;
|
||||
color: #6b8e6b;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 平板端适配 (768px - 1024px) */
|
||||
@media (max-width: 1024px) and (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 90%;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.hot-item {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.hot-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机端适配 (最大768px) */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
background-color: #f8fdf8;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 95%;
|
||||
margin: 10px auto;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 0.85rem;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.hot-item {
|
||||
padding: 14px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.hot-rank {
|
||||
font-size: 1.1rem;
|
||||
margin-right: 12px;
|
||||
min-width: 30px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.hot-title {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏手机适配 (最大480px) */
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
margin: 5px auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.hot-item {
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.hot-rank {
|
||||
font-size: 1rem;
|
||||
margin-right: 10px;
|
||||
min-width: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.hot-title {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 0.8rem;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
34
frontend/60sapi/热搜榜单/哔哩哔哩热搜榜/index.html
Normal file
34
frontend/60sapi/热搜榜单/哔哩哔哩热搜榜/index.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>哔哩哔哩热搜榜</title>
|
||||
<link rel="stylesheet" href="./css/style.css">
|
||||
<link rel="stylesheet" href="./css/background.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="background-container">
|
||||
<div class="green-gradient"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>哔哩哔哩热搜榜</h1>
|
||||
<div class="update-time" id="updateTime"></div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="hot-list" id="hotList">
|
||||
<div class="loading">加载中...</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>数据来源于哔哩哔哩热搜榜</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="./js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
134
frontend/60sapi/热搜榜单/哔哩哔哩热搜榜/js/main.js
Normal file
134
frontend/60sapi/热搜榜单/哔哩哔哩热搜榜/js/main.js
Normal file
@@ -0,0 +1,134 @@
|
||||
// API接口列表
|
||||
const API_ENDPOINTS = [
|
||||
"https://60s.viki.moe/v2/bili",
|
||||
"https://60s-cf.viki.moe/v2/bili",
|
||||
"https://60s.b23.run/v2/bili",
|
||||
"https://60s.114128.xyz/v2/bili",
|
||||
"https://60s-cf.114128.xyz/v2/bili"
|
||||
];
|
||||
|
||||
// 当前使用的API索引
|
||||
let currentApiIndex = 0;
|
||||
|
||||
// DOM元素
|
||||
const hotListElement = document.getElementById('hotList');
|
||||
const updateTimeElement = document.getElementById('updateTime');
|
||||
|
||||
// 格式化时间
|
||||
function formatDate(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
// 渲染热搜列表
|
||||
function renderHotList(data) {
|
||||
hotListElement.innerHTML = '';
|
||||
|
||||
data.forEach((item, index) => {
|
||||
const hotItem = document.createElement('div');
|
||||
hotItem.className = 'hot-item';
|
||||
|
||||
const rankClass = index < 3 ? `top-${index + 1}` : '';
|
||||
|
||||
hotItem.innerHTML = `
|
||||
<div class="hot-rank ${rankClass}">${index + 1}</div>
|
||||
<div class="hot-content">
|
||||
<a href="${item.link}" class="hot-title" target="_blank">${item.title}</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
hotListElement.appendChild(hotItem);
|
||||
});
|
||||
|
||||
// 更新时间
|
||||
updateTimeElement.textContent = `更新时间:${formatDate(new Date())}`;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
function showLoading() {
|
||||
hotListElement.innerHTML = '<div class="loading">加载中...</div>';
|
||||
}
|
||||
|
||||
// 显示错误状态
|
||||
function showError(message) {
|
||||
hotListElement.innerHTML = `<div class="loading">${message}</div>`;
|
||||
}
|
||||
|
||||
// 获取哔哩哔哩热搜数据
|
||||
async function fetchBiliHotList() {
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch(API_ENDPOINTS[currentApiIndex]);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 200 && result.data && Array.isArray(result.data)) {
|
||||
if (result.data.length > 0) {
|
||||
renderHotList(result.data);
|
||||
} else {
|
||||
showError('暂无热搜数据');
|
||||
}
|
||||
} else {
|
||||
throw new Error('数据格式错误或无数据');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
|
||||
// 尝试切换到下一个API
|
||||
const nextApiIndex = (currentApiIndex + 1) % API_ENDPOINTS.length;
|
||||
|
||||
if (nextApiIndex !== 0) {
|
||||
// 还有其他API可以尝试
|
||||
currentApiIndex = nextApiIndex;
|
||||
showError('获取数据失败,正在尝试其他接口...');
|
||||
|
||||
// 延迟后重试
|
||||
setTimeout(fetchBiliHotList, 2000);
|
||||
} else {
|
||||
// 所有API都尝试过了
|
||||
currentApiIndex = 0;
|
||||
showError('所有接口都无法访问,请稍后再试');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 手动刷新数据
|
||||
function refreshData() {
|
||||
currentApiIndex = 0; // 重置API索引
|
||||
fetchBiliHotList();
|
||||
}
|
||||
|
||||
// 页面加载完成后获取数据
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchBiliHotList();
|
||||
|
||||
// 每隔5分钟刷新一次数据
|
||||
setInterval(fetchBiliHotList, 5 * 60 * 1000);
|
||||
|
||||
// 添加键盘快捷键支持(按R键刷新)
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'r' || event.key === 'R') {
|
||||
event.preventDefault();
|
||||
refreshData();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 页面可见性变化时的处理
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden) {
|
||||
// 页面重新可见时刷新数据
|
||||
refreshData();
|
||||
}
|
||||
});
|
||||
3
frontend/60sapi/热搜榜单/哔哩哔哩热搜榜/接口集合.json
Normal file
3
frontend/60sapi/热搜榜单/哔哩哔哩热搜榜/接口集合.json
Normal file
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
18
frontend/60sapi/热搜榜单/哔哩哔哩热搜榜/返回接口.json
Normal file
18
frontend/60sapi/热搜榜单/哔哩哔哩热搜榜/返回接口.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||
"data": [
|
||||
{
|
||||
"title": "18次阅兵76年逆袭路",
|
||||
"link": "https://search.bilibili.com/all?keyword=18%E6%AC%A1%E9%98%85%E5%85%B576%E5%B9%B4%E9%80%86%E8%A2%AD%E8%B7%AF"
|
||||
},
|
||||
{
|
||||
"title": "80年前的今天日本签署投降书",
|
||||
"link": "https://search.bilibili.com/all?keyword=80%E5%B9%B4%E5%89%8D%E7%9A%84%E4%BB%8A%E5%A4%A9%E6%97%A5%E6%9C%AC%E7%AD%BE%E7%BD%B2%E6%8A%95%E9%99%8D%E4%B9%A6"
|
||||
},
|
||||
{
|
||||
"title": "九三阅兵具体安排公布",
|
||||
"link": "https://search.bilibili.com/all?keyword=%E4%B9%9D%E4%B8%89%E9%98%85%E5%85%B5%E5%85%B7%E4%BD%93%E5%AE%89%E6%8E%92%E5%85%AC%E5%B8%83"
|
||||
}
|
||||
]
|
||||
}
|
||||
107
frontend/60sapi/热搜榜单/头条热搜榜/css/background.css
Normal file
107
frontend/60sapi/热搜榜单/头条热搜榜/css/background.css
Normal file
@@ -0,0 +1,107 @@
|
||||
.background-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.green-gradient {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(76, 175, 80, 0.3) 0%,
|
||||
rgba(129, 199, 132, 0.2) 25%,
|
||||
rgba(165, 214, 167, 0.1) 50%,
|
||||
rgba(200, 230, 201, 0.2) 75%,
|
||||
rgba(76, 175, 80, 0.3) 100%
|
||||
);
|
||||
animation: green-flow 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.green-gradient::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(
|
||||
circle at 30% 70%,
|
||||
rgba(139, 195, 74, 0.4) 0%,
|
||||
transparent 50%
|
||||
), radial-gradient(
|
||||
circle at 70% 30%,
|
||||
rgba(102, 187, 106, 0.3) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
animation: green-pulse 15s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes green-flow {
|
||||
0%, 100% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
25% {
|
||||
transform: rotate(90deg) scale(1.1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg) scale(0.9);
|
||||
opacity: 0.9;
|
||||
}
|
||||
75% {
|
||||
transform: rotate(270deg) scale(1.05);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes green-pulse {
|
||||
0% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2) rotate(180deg);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) rotate(360deg);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机端背景优化 */
|
||||
@media (max-width: 768px) {
|
||||
.green-gradient {
|
||||
animation-duration: 25s;
|
||||
}
|
||||
|
||||
.green-gradient::before {
|
||||
animation-duration: 18s;
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动画以节省电池 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.green-gradient,
|
||||
.green-gradient::before {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.green-gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(76, 175, 80, 0.2) 0%,
|
||||
rgba(165, 214, 167, 0.1) 50%,
|
||||
rgba(200, 230, 201, 0.15) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
291
frontend/60sapi/热搜榜单/头条热搜榜/css/style.css
Normal file
291
frontend/60sapi/热搜榜单/头条热搜榜/css/style.css
Normal file
@@ -0,0 +1,291 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
color: #333;
|
||||
background-color: #f0f8f0;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 128, 0, 0.1);
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e8f5e8;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: #2d8f47;
|
||||
margin-bottom: 12px;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(45, 143, 71, 0.1);
|
||||
}
|
||||
|
||||
.update-time {
|
||||
color: #6b8e6b;
|
||||
font-size: 0.95rem;
|
||||
background-color: #f0f8f0;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hot-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.hot-item {
|
||||
padding: 18px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 10px;
|
||||
background-color: white;
|
||||
box-shadow: 0 3px 8px rgba(0, 128, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-left: 4px solid transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hot-item:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 16px rgba(0, 128, 0, 0.15);
|
||||
border-left-color: #4caf50;
|
||||
}
|
||||
|
||||
.hot-rank {
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
color: #4caf50;
|
||||
margin-right: 18px;
|
||||
min-width: 35px;
|
||||
text-align: center;
|
||||
background-color: #f0f8f0;
|
||||
border-radius: 50%;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hot-rank.top-1 {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hot-rank.top-2 {
|
||||
background: linear-gradient(135deg, #ffa726, #ffb74d);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hot-rank.top-3 {
|
||||
background: linear-gradient(135deg, #ffca28, #ffd54f);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hot-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hot-title {
|
||||
font-size: 1.15rem;
|
||||
margin-bottom: 6px;
|
||||
color: #2c3e2c;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
line-height: 1.4;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hot-title:hover {
|
||||
color: #2d8f47;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.hot-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.hot-value {
|
||||
background: linear-gradient(135deg, #4caf50, #66bb6a);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.hot-tag {
|
||||
background-color: #e8f5e8;
|
||||
color: #2d8f47;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #6b8e6b;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e8f5e8;
|
||||
color: #6b8e6b;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 平板端适配 (768px - 1024px) */
|
||||
@media (max-width: 1024px) and (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 90%;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.hot-item {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.hot-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机端适配 (最大768px) */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
background-color: #f8fdf8;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 95%;
|
||||
margin: 10px auto;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 0.85rem;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.hot-item {
|
||||
padding: 14px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.hot-rank {
|
||||
font-size: 1.1rem;
|
||||
margin-right: 12px;
|
||||
min-width: 30px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.hot-title {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.hot-stats {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏手机适配 (最大480px) */
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
margin: 5px auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.hot-item {
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.hot-rank {
|
||||
font-size: 1rem;
|
||||
margin-right: 10px;
|
||||
min-width: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.hot-title {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 0.8rem;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
34
frontend/60sapi/热搜榜单/头条热搜榜/index.html
Normal file
34
frontend/60sapi/热搜榜单/头条热搜榜/index.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>头条热搜榜</title>
|
||||
<link rel="stylesheet" href="./css/style.css">
|
||||
<link rel="stylesheet" href="./css/background.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="background-container">
|
||||
<div class="green-gradient"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>头条热搜榜</h1>
|
||||
<div class="update-time" id="updateTime"></div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="hot-list" id="hotList">
|
||||
<div class="loading">加载中...</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>数据来源于头条热搜榜</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="./js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
156
frontend/60sapi/热搜榜单/头条热搜榜/js/main.js
Normal file
156
frontend/60sapi/热搜榜单/头条热搜榜/js/main.js
Normal file
@@ -0,0 +1,156 @@
|
||||
// API接口列表
|
||||
const API_ENDPOINTS = [
|
||||
"https://60s.viki.moe/v2/toutiao",
|
||||
"https://60s-cf.viki.moe/v2/toutiao",
|
||||
"https://60s.b23.run/v2/toutiao",
|
||||
"https://60s.114128.xyz/v2/toutiao",
|
||||
"https://60s-cf.114128.xyz/v2/toutiao"
|
||||
];
|
||||
|
||||
// 当前使用的API索引
|
||||
let currentApiIndex = 0;
|
||||
|
||||
// DOM元素
|
||||
const hotListElement = document.getElementById('hotList');
|
||||
const updateTimeElement = document.getElementById('updateTime');
|
||||
|
||||
// 格式化时间
|
||||
function formatDate(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
// 格式化数字
|
||||
function formatNumber(num) {
|
||||
if (num >= 10000) {
|
||||
return (num / 10000).toFixed(1) + '万';
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
// 渲染热搜列表
|
||||
function renderHotList(data) {
|
||||
hotListElement.innerHTML = '';
|
||||
|
||||
data.forEach((item, index) => {
|
||||
const hotItem = document.createElement('div');
|
||||
hotItem.className = 'hot-item';
|
||||
|
||||
const rankClass = index < 3 ? `top-${index + 1}` : '';
|
||||
|
||||
// 处理热度值显示
|
||||
const hotValueDisplay = item.hot_value ?
|
||||
`<div class="stat-item"><span class="hot-value">${formatNumber(item.hot_value)} 热度</span></div>` : '';
|
||||
|
||||
// 处理标签显示
|
||||
const tagDisplay = item.tag ?
|
||||
`<div class="stat-item"><span class="hot-tag">${item.tag}</span></div>` : '';
|
||||
|
||||
hotItem.innerHTML = `
|
||||
<div class="hot-rank ${rankClass}">${index + 1}</div>
|
||||
<div class="hot-content">
|
||||
<a href="${item.link}" class="hot-title" target="_blank">${item.title}</a>
|
||||
<div class="hot-stats">
|
||||
${hotValueDisplay}
|
||||
${tagDisplay}
|
||||
${item.source ? `<div class="stat-item">📰 ${item.source}</div>` : ''}
|
||||
${item.time ? `<div class="stat-item">🕒 ${item.time}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
hotListElement.appendChild(hotItem);
|
||||
});
|
||||
|
||||
// 更新时间
|
||||
updateTimeElement.textContent = `更新时间:${formatDate(new Date())}`;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
function showLoading() {
|
||||
hotListElement.innerHTML = '<div class="loading">加载中...</div>';
|
||||
}
|
||||
|
||||
// 显示错误状态
|
||||
function showError(message) {
|
||||
hotListElement.innerHTML = `<div class="loading">${message}</div>`;
|
||||
}
|
||||
|
||||
// 获取头条热搜数据
|
||||
async function fetchToutiaoHotList() {
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch(API_ENDPOINTS[currentApiIndex]);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 200 && result.data && Array.isArray(result.data)) {
|
||||
if (result.data.length > 0) {
|
||||
renderHotList(result.data);
|
||||
} else {
|
||||
showError('暂无热搜数据');
|
||||
}
|
||||
} else {
|
||||
throw new Error('数据格式错误或无数据');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
|
||||
// 尝试切换到下一个API
|
||||
const nextApiIndex = (currentApiIndex + 1) % API_ENDPOINTS.length;
|
||||
|
||||
if (nextApiIndex !== 0) {
|
||||
// 还有其他API可以尝试
|
||||
currentApiIndex = nextApiIndex;
|
||||
showError('获取数据失败,正在尝试其他接口...');
|
||||
|
||||
// 延迟后重试
|
||||
setTimeout(fetchToutiaoHotList, 2000);
|
||||
} else {
|
||||
// 所有API都尝试过了
|
||||
currentApiIndex = 0;
|
||||
showError('所有接口都无法访问,请稍后再试');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 手动刷新数据
|
||||
function refreshData() {
|
||||
currentApiIndex = 0; // 重置API索引
|
||||
fetchToutiaoHotList();
|
||||
}
|
||||
|
||||
// 页面加载完成后获取数据
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchToutiaoHotList();
|
||||
|
||||
// 每隔5分钟刷新一次数据
|
||||
setInterval(fetchToutiaoHotList, 5 * 60 * 1000);
|
||||
|
||||
// 添加键盘快捷键支持(按R键刷新)
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'r' || event.key === 'R') {
|
||||
event.preventDefault();
|
||||
refreshData();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 页面可见性变化时的处理
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden) {
|
||||
// 页面重新可见时刷新数据
|
||||
refreshData();
|
||||
}
|
||||
});
|
||||
3
frontend/60sapi/热搜榜单/头条热搜榜/接口集合.json
Normal file
3
frontend/60sapi/热搜榜单/头条热搜榜/接口集合.json
Normal file
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
30
frontend/60sapi/热搜榜单/头条热搜榜/返回接口.json
Normal file
30
frontend/60sapi/热搜榜单/头条热搜榜/返回接口.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||
"data": [
|
||||
{
|
||||
"title": "九三阅兵具体安排公布",
|
||||
"hot_value": 11821633,
|
||||
"tag": "热",
|
||||
"source": "头条新闻",
|
||||
"time": "2小时前",
|
||||
"link": "https://www.toutiao.com/article/7404567890123456789/"
|
||||
},
|
||||
{
|
||||
"title": "九月第一天",
|
||||
"hot_value": 11327170,
|
||||
"tag": "新",
|
||||
"source": "今日头条",
|
||||
"time": "1小时前",
|
||||
"link": "https://www.toutiao.com/article/7404567890123456790/"
|
||||
},
|
||||
{
|
||||
"title": "遇见上合共享津彩",
|
||||
"hot_value": 11222444,
|
||||
"tag": "推荐",
|
||||
"source": "头条资讯",
|
||||
"time": "3小时前",
|
||||
"link": "https://www.toutiao.com/article/7404567890123456791/"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// API接口列表
|
||||
// 本地后端API接口
|
||||
const LOCAL_API_BASE = 'http://localhost:5000/api/60s';
|
||||
|
||||
// API接口列表(备用)
|
||||
const API_ENDPOINTS = [
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
@@ -9,6 +12,7 @@ const API_ENDPOINTS = [
|
||||
|
||||
// 当前使用的API索引
|
||||
let currentApiIndex = 0;
|
||||
let useLocalApi = true;
|
||||
|
||||
// DOM元素
|
||||
const loadingElement = document.getElementById('loading');
|
||||
@@ -46,6 +50,30 @@ async function loadHotList() {
|
||||
|
||||
// 获取数据
|
||||
async function fetchData() {
|
||||
// 优先尝试本地API
|
||||
if (useLocalApi) {
|
||||
try {
|
||||
const response = await fetch(`${LOCAL_API_BASE}/douyin`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.code === 200 && data.data) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('本地API请求失败,切换到外部API:', error);
|
||||
useLocalApi = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用外部API作为备用
|
||||
for (let i = 0; i < API_ENDPOINTS.length; i++) {
|
||||
const apiUrl = API_ENDPOINTS[currentApiIndex];
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
107
frontend/60sapi/热搜榜单/知乎热门话题/css/background.css
Normal file
107
frontend/60sapi/热搜榜单/知乎热门话题/css/background.css
Normal file
@@ -0,0 +1,107 @@
|
||||
.background-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.green-gradient {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(76, 175, 80, 0.3) 0%,
|
||||
rgba(129, 199, 132, 0.2) 25%,
|
||||
rgba(165, 214, 167, 0.1) 50%,
|
||||
rgba(200, 230, 201, 0.2) 75%,
|
||||
rgba(76, 175, 80, 0.3) 100%
|
||||
);
|
||||
animation: green-flow 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.green-gradient::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(
|
||||
circle at 30% 70%,
|
||||
rgba(139, 195, 74, 0.4) 0%,
|
||||
transparent 50%
|
||||
), radial-gradient(
|
||||
circle at 70% 30%,
|
||||
rgba(102, 187, 106, 0.3) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
animation: green-pulse 15s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes green-flow {
|
||||
0%, 100% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
25% {
|
||||
transform: rotate(90deg) scale(1.1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg) scale(0.9);
|
||||
opacity: 0.9;
|
||||
}
|
||||
75% {
|
||||
transform: rotate(270deg) scale(1.05);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes green-pulse {
|
||||
0% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2) rotate(180deg);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) rotate(360deg);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机端背景优化 */
|
||||
@media (max-width: 768px) {
|
||||
.green-gradient {
|
||||
animation-duration: 25s;
|
||||
}
|
||||
|
||||
.green-gradient::before {
|
||||
animation-duration: 18s;
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动画以节省电池 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.green-gradient,
|
||||
.green-gradient::before {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.green-gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(76, 175, 80, 0.2) 0%,
|
||||
rgba(165, 214, 167, 0.1) 50%,
|
||||
rgba(200, 230, 201, 0.15) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
331
frontend/60sapi/热搜榜单/知乎热门话题/css/style.css
Normal file
331
frontend/60sapi/热搜榜单/知乎热门话题/css/style.css
Normal file
@@ -0,0 +1,331 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
color: #333;
|
||||
background-color: #f0f8f0;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 128, 0, 0.1);
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e8f5e8;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: #2d8f47;
|
||||
margin-bottom: 12px;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(45, 143, 71, 0.1);
|
||||
}
|
||||
|
||||
.update-time {
|
||||
color: #6b8e6b;
|
||||
font-size: 0.95rem;
|
||||
background-color: #f0f8f0;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.topic-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.topic-item {
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 12px;
|
||||
background-color: white;
|
||||
box-shadow: 0 3px 10px rgba(0, 128, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
border-left: 4px solid transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.topic-item:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 20px rgba(0, 128, 0, 0.15);
|
||||
border-left-color: #4caf50;
|
||||
}
|
||||
|
||||
.topic-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.topic-rank {
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
color: #4caf50;
|
||||
margin-right: 18px;
|
||||
min-width: 35px;
|
||||
text-align: center;
|
||||
background-color: #f0f8f0;
|
||||
border-radius: 50%;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.topic-rank.top-1 {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.topic-rank.top-2 {
|
||||
background: linear-gradient(135deg, #ffa726, #ffb74d);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.topic-rank.top-3 {
|
||||
background: linear-gradient(135deg, #ffca28, #ffd54f);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.topic-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.topic-title {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 8px;
|
||||
color: #2c3e2c;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
line-height: 1.4;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.topic-title:hover {
|
||||
color: #2d8f47;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.topic-detail {
|
||||
font-size: 0.95rem;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 10px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topic-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.hot-value {
|
||||
background: linear-gradient(135deg, #4caf50, #66bb6a);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.topic-cover {
|
||||
width: 80px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
margin-left: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #6b8e6b;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e8f5e8;
|
||||
color: #6b8e6b;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 平板端适配 (768px - 1024px) */
|
||||
@media (max-width: 1024px) and (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 90%;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.topic-item {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.topic-title {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.topic-cover {
|
||||
width: 70px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机端适配 (最大768px) */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
background-color: #f8fdf8;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 95%;
|
||||
margin: 10px auto;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 0.85rem;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.topic-item {
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.topic-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.topic-rank {
|
||||
font-size: 1.1rem;
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.topic-title {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.topic-detail {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.topic-stats {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.topic-cover {
|
||||
width: 60px;
|
||||
height: 45px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏手机适配 (最大480px) */
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
margin: 5px auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.topic-item {
|
||||
padding: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.topic-rank {
|
||||
font-size: 1rem;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.topic-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.topic-detail {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.topic-cover {
|
||||
width: 50px;
|
||||
height: 38px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 0.8rem;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
34
frontend/60sapi/热搜榜单/知乎热门话题/index.html
Normal file
34
frontend/60sapi/热搜榜单/知乎热门话题/index.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>知乎热门话题</title>
|
||||
<link rel="stylesheet" href="./css/style.css">
|
||||
<link rel="stylesheet" href="./css/background.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="background-container">
|
||||
<div class="green-gradient"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>知乎热门话题</h1>
|
||||
<div class="update-time" id="updateTime"></div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="topic-list" id="topicList">
|
||||
<div class="loading">加载中...</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>数据来源于知乎热门话题</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="./js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
155
frontend/60sapi/热搜榜单/知乎热门话题/js/main.js
Normal file
155
frontend/60sapi/热搜榜单/知乎热门话题/js/main.js
Normal file
@@ -0,0 +1,155 @@
|
||||
// API接口列表
|
||||
const API_ENDPOINTS = [
|
||||
"https://60s.viki.moe/v2/zhihu",
|
||||
"https://60s-cf.viki.moe/v2/zhihu",
|
||||
"https://60s.b23.run/v2/zhihu",
|
||||
"https://60s.114128.xyz/v2/zhihu",
|
||||
"https://60s-cf.114128.xyz/v2/zhihu"
|
||||
];
|
||||
|
||||
// 当前使用的API索引
|
||||
let currentApiIndex = 0;
|
||||
|
||||
// DOM元素
|
||||
const topicListElement = document.getElementById('topicList');
|
||||
const updateTimeElement = document.getElementById('updateTime');
|
||||
|
||||
// 格式化时间
|
||||
function formatDate(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
// 格式化数字
|
||||
function formatNumber(num) {
|
||||
if (num >= 10000) {
|
||||
return (num / 10000).toFixed(1) + '万';
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
// 渲染话题列表
|
||||
function renderTopicList(data) {
|
||||
topicListElement.innerHTML = '';
|
||||
|
||||
data.forEach((item, index) => {
|
||||
const topicItem = document.createElement('div');
|
||||
topicItem.className = 'topic-item';
|
||||
|
||||
const rankClass = index < 3 ? `top-${index + 1}` : '';
|
||||
|
||||
// 处理封面图片
|
||||
const coverImg = item.cover ?
|
||||
`<img src="${item.cover}" alt="话题封面" class="topic-cover" onerror="this.style.display='none'">` : '';
|
||||
|
||||
topicItem.innerHTML = `
|
||||
<div class="topic-header">
|
||||
<div class="topic-rank ${rankClass}">${index + 1}</div>
|
||||
<div class="topic-content">
|
||||
<a href="${item.link}" class="topic-title" target="_blank">${item.title}</a>
|
||||
${item.detail ? `<div class="topic-detail">${item.detail}</div>` : ''}
|
||||
<div class="topic-stats">
|
||||
${item.hot_value_desc ? `<div class="stat-item"><span class="hot-value">${item.hot_value_desc}</span></div>` : ''}
|
||||
${item.answer_cnt ? `<div class="stat-item">💬 ${formatNumber(item.answer_cnt)} 回答</div>` : ''}
|
||||
${item.follower_cnt ? `<div class="stat-item">👥 ${formatNumber(item.follower_cnt)} 关注</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
${coverImg}
|
||||
</div>
|
||||
`;
|
||||
|
||||
topicListElement.appendChild(topicItem);
|
||||
});
|
||||
|
||||
// 更新时间
|
||||
updateTimeElement.textContent = `更新时间:${formatDate(new Date())}`;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
function showLoading() {
|
||||
topicListElement.innerHTML = '<div class="loading">加载中...</div>';
|
||||
}
|
||||
|
||||
// 显示错误状态
|
||||
function showError(message) {
|
||||
topicListElement.innerHTML = `<div class="loading">${message}</div>`;
|
||||
}
|
||||
|
||||
// 获取知乎热门话题数据
|
||||
async function fetchZhihuTopics() {
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch(API_ENDPOINTS[currentApiIndex]);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 200 && result.data && Array.isArray(result.data)) {
|
||||
if (result.data.length > 0) {
|
||||
renderTopicList(result.data);
|
||||
} else {
|
||||
showError('暂无热门话题数据');
|
||||
}
|
||||
} else {
|
||||
throw new Error('数据格式错误或无数据');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
|
||||
// 尝试切换到下一个API
|
||||
const nextApiIndex = (currentApiIndex + 1) % API_ENDPOINTS.length;
|
||||
|
||||
if (nextApiIndex !== 0) {
|
||||
// 还有其他API可以尝试
|
||||
currentApiIndex = nextApiIndex;
|
||||
showError('获取数据失败,正在尝试其他接口...');
|
||||
|
||||
// 延迟后重试
|
||||
setTimeout(fetchZhihuTopics, 2000);
|
||||
} else {
|
||||
// 所有API都尝试过了
|
||||
currentApiIndex = 0;
|
||||
showError('所有接口都无法访问,请稍后再试');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 手动刷新数据
|
||||
function refreshData() {
|
||||
currentApiIndex = 0; // 重置API索引
|
||||
fetchZhihuTopics();
|
||||
}
|
||||
|
||||
// 页面加载完成后获取数据
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchZhihuTopics();
|
||||
|
||||
// 每隔5分钟刷新一次数据
|
||||
setInterval(fetchZhihuTopics, 5 * 60 * 1000);
|
||||
|
||||
// 添加键盘快捷键支持(按R键刷新)
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'r' || event.key === 'R') {
|
||||
event.preventDefault();
|
||||
refreshData();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 页面可见性变化时的处理
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden) {
|
||||
// 页面重新可见时刷新数据
|
||||
refreshData();
|
||||
}
|
||||
});
|
||||
7
frontend/60sapi/热搜榜单/知乎热门话题/接口集合.json
Normal file
7
frontend/60sapi/热搜榜单/知乎热门话题/接口集合.json
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
]
|
||||
30
frontend/60sapi/热搜榜单/知乎热门话题/返回接口.json
Normal file
30
frontend/60sapi/热搜榜单/知乎热门话题/返回接口.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||
"data": [
|
||||
{
|
||||
"title": "你是什么时候意识到抗日战争很艰难的?",
|
||||
"detail": "就不像抗日神剧",
|
||||
"cover": "https://picx.zhimg.com/v2-a07a53b2e23887c7a2440cc3f1984122.png",
|
||||
"hot_value_desc": "1311 万热度",
|
||||
"answer_cnt": 739,
|
||||
"follower_cnt": 1739,
|
||||
"comment_cnt": 0,
|
||||
"created_at": 1622774952000,
|
||||
"created": "2021/06/04 10:49:12",
|
||||
"link": "https://api.zhihu.com/questions/463076881"
|
||||
},
|
||||
{
|
||||
"title": "为什么中国在很多领域都要求自主研发?",
|
||||
"detail": "中国在很多领域都要求自主研发,似乎确认很多东西都有个中国版本,真有这个必要吗?",
|
||||
"cover": "https://pic3.zhimg.com/80/v2-bb4dfa56f138980078da003df436e661_hd.png",
|
||||
"hot_value_desc": "816 万热度",
|
||||
"answer_cnt": 2476,
|
||||
"follower_cnt": 12545,
|
||||
"comment_cnt": 0,
|
||||
"created_at": 1352205751000,
|
||||
"created": "2012/11/06 20:42:31",
|
||||
"link": "https://api.zhihu.com/questions/20579464"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
|
||||
BIN
frontend/assets/App Logo 设计 (2).png
Normal file
BIN
frontend/assets/App Logo 设计 (2).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
BIN
frontend/assets/logo.png
Normal file
BIN
frontend/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 930 KiB |
20652
frontend/react-app/package-lock.json
generated
Normal file
20652
frontend/react-app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
frontend/react-app/package.json
Normal file
49
frontend/react-app/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "infogenie-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "✨ 神奇万事通 - 前端React应用",
|
||||
"keywords": ["react", "api", "mobile-first", "responsive"],
|
||||
"author": "神奇万事通",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"axios": "^1.5.0",
|
||||
"styled-components": "^6.0.7",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"dev": "react-scripts start"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:5000"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/* 背景样式文件 */
|
||||
|
||||
/* 主体背景 */
|
||||
body {
|
||||
background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 50%, #a5d6a7 100%);
|
||||
background-attachment: fixed;
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
}
|
||||
|
||||
/* 背景动画 */
|
||||
@keyframes gradientShift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 装饰性背景元素 */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 80%, rgba(76, 175, 80, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(129, 199, 132, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(165, 214, 167, 0.08) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* 浮动装饰圆点 */
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
radial-gradient(2px 2px at 20px 30px, rgba(76, 175, 80, 0.3), transparent),
|
||||
radial-gradient(2px 2px at 40px 70px, rgba(129, 199, 132, 0.2), transparent),
|
||||
radial-gradient(1px 1px at 90px 40px, rgba(165, 214, 167, 0.3), transparent),
|
||||
radial-gradient(1px 1px at 130px 80px, rgba(76, 175, 80, 0.2), transparent),
|
||||
radial-gradient(2px 2px at 160px 30px, rgba(129, 199, 132, 0.3), transparent);
|
||||
background-repeat: repeat;
|
||||
background-size: 200px 100px;
|
||||
animation: float 20s linear infinite;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 题目容器背景增强 */
|
||||
.question-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(26, 77, 26, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 错误容器背景 */
|
||||
.error-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* 结果容器背景 */
|
||||
.result-container {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
/* 代码块背景 */
|
||||
.code-block {
|
||||
background: rgba(248, 249, 250, 0.9);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
/* 选项背景 */
|
||||
.option {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: rgba(76, 175, 80, 0.05);
|
||||
}
|
||||
|
||||
.option.selected {
|
||||
background: linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(76, 175, 80, 0.08));
|
||||
}
|
||||
|
||||
/* 按钮背景增强 */
|
||||
.submit-btn {
|
||||
background: linear-gradient(135deg, #4caf50, #45a049);
|
||||
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
.show-answer-btn {
|
||||
background: linear-gradient(135deg, #2196f3, #1976d2);
|
||||
box-shadow: 0 4px 15px rgba(33, 150, 243, 0.2);
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: linear-gradient(135deg, #ff9800, #f57c00);
|
||||
box-shadow: 0 4px 15px rgba(255, 152, 0, 0.2);
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: linear-gradient(135deg, #4caf50, #45a049);
|
||||
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
/* 移动端背景优化 */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
background-attachment: scroll;
|
||||
}
|
||||
|
||||
body::after {
|
||||
opacity: 0.4;
|
||||
background-size: 150px 75px;
|
||||
}
|
||||
|
||||
.question-container {
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 高对比度模式支持 */
|
||||
@media (prefers-contrast: high) {
|
||||
body {
|
||||
background: #f0f8f0;
|
||||
}
|
||||
|
||||
body::before,
|
||||
body::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.question-container {
|
||||
background: #ffffff;
|
||||
border: 2px solid #4caf50;
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动画模式支持 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body {
|
||||
animation: none;
|
||||
background: #e8f5e8;
|
||||
}
|
||||
|
||||
body::after {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,597 @@
|
||||
/* 基础样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #2d5a27;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 容器布局 */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
color: #1a4d1a;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 2px 4px rgba(26, 77, 26, 0.1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: #4a7c59;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 主内容区域 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #e8f5e8;
|
||||
border-top: 4px solid #4caf50;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading p {
|
||||
color: #4a7c59;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* 题目容器 */
|
||||
.question-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 10px 30px rgba(26, 77, 26, 0.1);
|
||||
border: 2px solid rgba(76, 175, 80, 0.2);
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* 题目头部 */
|
||||
.question-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #e8f5e8;
|
||||
}
|
||||
|
||||
.question-id {
|
||||
font-size: 1.1rem;
|
||||
color: #4caf50;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #e8f5e8, #c8e6c9);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: linear-gradient(135deg, #4caf50, #45a049);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
transform: rotate(180deg) scale(1.1);
|
||||
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
/* 题目文本 */
|
||||
.question-text h2 {
|
||||
font-size: 1.5rem;
|
||||
color: #1a4d1a;
|
||||
margin-bottom: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
.code-block {
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #e8f5e8;
|
||||
border-radius: 12px;
|
||||
margin: 25px 0;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
margin: 0;
|
||||
padding: 25px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
color: #2d5a27;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.code-block code {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 代码高亮自定义样式 - 丰富的语法高亮 */
|
||||
.code-block .hljs {
|
||||
background: transparent !important;
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
/* JavaScript 关键字 - 蓝色 */
|
||||
.code-block .hljs-keyword {
|
||||
color: #0066cc !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 字符串 - 绿色 */
|
||||
.code-block .hljs-string {
|
||||
color: #22aa22 !important;
|
||||
}
|
||||
|
||||
/* 数字 - 橙色 */
|
||||
.code-block .hljs-number {
|
||||
color: #ff6600 !important;
|
||||
}
|
||||
|
||||
/* 函数名 - 紫色 */
|
||||
.code-block .hljs-function,
|
||||
.code-block .hljs-title.function_ {
|
||||
color: #9933cc !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 变量名 - 深蓝色 */
|
||||
.code-block .hljs-variable,
|
||||
.code-block .hljs-name {
|
||||
color: #0066aa !important;
|
||||
}
|
||||
|
||||
/* 注释 - 灰色 */
|
||||
.code-block .hljs-comment {
|
||||
color: #888888 !important;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 内置对象和方法 - 深紫色 */
|
||||
.code-block .hljs-built_in {
|
||||
color: #663399 !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 字面量 (true, false, null) - 红色 */
|
||||
.code-block .hljs-literal {
|
||||
color: #cc0000 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 操作符 - 深灰色 */
|
||||
.code-block .hljs-operator {
|
||||
color: #666666 !important;
|
||||
}
|
||||
|
||||
/* 标点符号 - 深灰色 */
|
||||
.code-block .hljs-punctuation {
|
||||
color: #666666 !important;
|
||||
}
|
||||
|
||||
/* 属性名 - 深蓝色 */
|
||||
.code-block .hljs-property,
|
||||
.code-block .hljs-attr {
|
||||
color: #0066aa !important;
|
||||
}
|
||||
|
||||
/* 类名和构造函数 - 深绿色 */
|
||||
.code-block .hljs-title.class_,
|
||||
.code-block .hljs-title {
|
||||
color: #228833 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 参数 - 深蓝色 */
|
||||
.code-block .hljs-params {
|
||||
color: #0066aa !important;
|
||||
}
|
||||
|
||||
/* 正则表达式 - 深红色 */
|
||||
.code-block .hljs-regexp {
|
||||
color: #aa0066 !important;
|
||||
}
|
||||
|
||||
/* 模板字符串 - 深绿色 */
|
||||
.code-block .hljs-template-variable,
|
||||
.code-block .hljs-template-tag {
|
||||
color: #228833 !important;
|
||||
}
|
||||
|
||||
/* 选项容器 */
|
||||
.options-container {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.option {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border: 2px solid #e8f5e8;
|
||||
border-radius: 12px;
|
||||
padding: 15px 20px;
|
||||
margin: 12px 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 1rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
border-color: #4caf50;
|
||||
background: rgba(76, 175, 80, 0.05);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.option.selected {
|
||||
border-color: #4caf50;
|
||||
background: linear-gradient(135deg, rgba(76, 175, 80, 0.1), rgba(76, 175, 80, 0.05));
|
||||
color: #1a4d1a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.option.correct {
|
||||
border-color: #4caf50;
|
||||
background: linear-gradient(135deg, rgba(76, 175, 80, 0.2), rgba(76, 175, 80, 0.1));
|
||||
color: #1a4d1a;
|
||||
}
|
||||
|
||||
.option.incorrect {
|
||||
border-color: #f44336;
|
||||
background: linear-gradient(135deg, rgba(244, 67, 54, 0.1), rgba(244, 67, 54, 0.05));
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin: 30px 0;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.submit-btn, .show-answer-btn, .retry-btn, .export-btn {
|
||||
padding: 12px 30px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: linear-gradient(135deg, #4caf50, #45a049);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
background: #cccccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.show-answer-btn {
|
||||
background: linear-gradient(135deg, #2196f3, #1976d2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.show-answer-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: linear-gradient(135deg, #ff9800, #f57c00);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(255, 152, 0, 0.3);
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
background: linear-gradient(135deg, #9c27b0, #7b1fa2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(156, 39, 176, 0.3);
|
||||
}
|
||||
|
||||
.export-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* 结果容器 */
|
||||
.result-container {
|
||||
margin-top: 30px;
|
||||
padding: 25px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 15px;
|
||||
border: 2px solid #e8f5e8;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #e8f5e8;
|
||||
}
|
||||
|
||||
.result-status {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result-status.correct {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.result-status.incorrect {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.correct-answer {
|
||||
font-weight: 600;
|
||||
color: #4caf50;
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
padding: 5px 12px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
color: #2d5a27;
|
||||
line-height: 1.7;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.explanation pre {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e8f5e8;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
overflow-x: auto;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* 错误容器 */
|
||||
.error-container {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 20px;
|
||||
border: 2px solid rgba(244, 67, 54, 0.2);
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error-container h3 {
|
||||
color: #f44336;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.error-container p {
|
||||
color: #666;
|
||||
margin-bottom: 25px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* 底部 */
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 30px 0;
|
||||
margin-top: 40px;
|
||||
color: #4a7c59;
|
||||
opacity: 0.7;
|
||||
border-top: 1px solid rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
/* 平板端适配 (768px - 1024px) */
|
||||
@media (max-width: 1024px) and (min-width: 768px) {
|
||||
.container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.question-container {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机端适配 (最大768px) */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 25px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.question-container {
|
||||
padding: 20px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.question-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.question-text h2 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
padding: 15px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: 12px 15px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.submit-btn, .show-answer-btn, .retry-btn {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.explanation pre {
|
||||
padding: 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏手机适配 (最大480px) */
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.question-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.question-id {
|
||||
font-size: 1rem;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
font-size: 0.8rem;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: 10px 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.submit-btn, .show-answer-btn, .retry-btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>随机JavaScript趣味题</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/background.css">
|
||||
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>JavaScript趣味题</h1>
|
||||
<p class="subtitle">测试你的JavaScript知识</p>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在加载题目...</p>
|
||||
</div>
|
||||
|
||||
<div class="question-container" id="questionContainer" style="display: none;">
|
||||
<div class="question-header">
|
||||
<span class="question-id" id="questionId">题目 #1</span>
|
||||
<button class="refresh-btn" id="refreshBtn" title="获取新题目">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<polyline points="1 20 1 14 7 14"></polyline>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="question-text" id="questionText">
|
||||
<h2>输出是什么?</h2>
|
||||
</div>
|
||||
|
||||
<div class="code-block" id="codeBlock">
|
||||
<pre><code id="codeContent" class="language-javascript"></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="options-container" id="optionsContainer">
|
||||
<!-- 选项将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="submit-btn" id="submitBtn" disabled>提交答案</button>
|
||||
<button class="show-answer-btn" id="showAnswerBtn">查看答案</button>
|
||||
<button class="export-btn" id="exportBtn" title="导出为Markdown文件">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
导出MD
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="result-container" id="resultContainer" style="display: none;">
|
||||
<div class="result-header">
|
||||
<span class="result-status" id="resultStatus"></span>
|
||||
<span class="correct-answer" id="correctAnswer"></span>
|
||||
</div>
|
||||
<div class="explanation" id="explanation">
|
||||
<!-- 解析内容 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error-container" id="errorContainer" style="display: none;">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<h3>加载失败</h3>
|
||||
<p id="errorMessage">网络连接异常,请稍后重试</p>
|
||||
<button class="retry-btn" id="retryBtn">重新加载</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>JavaScript趣味题集合</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
565
frontend/react-app/public/60sapi/娱乐消遣/随机JavaScript趣味题/js/script.js
vendored
Normal file
565
frontend/react-app/public/60sapi/娱乐消遣/随机JavaScript趣味题/js/script.js
vendored
Normal file
@@ -0,0 +1,565 @@
|
||||
// JavaScript趣味题应用
|
||||
class JSQuizApp {
|
||||
constructor() {
|
||||
this.apiEndpoints = [
|
||||
'https://60s-cf.viki.moe',
|
||||
'https://60s.viki.moe',
|
||||
'https://60s.b23.run',
|
||||
'https://60s.114128.xyz',
|
||||
'https://60s-cf.114128.xyz'
|
||||
];
|
||||
this.currentApiIndex = 0;
|
||||
this.currentQuestion = null;
|
||||
this.selectedOption = null;
|
||||
this.isAnswered = false;
|
||||
|
||||
this.initElements();
|
||||
this.bindEvents();
|
||||
this.loadQuestion();
|
||||
}
|
||||
|
||||
// 初始化DOM元素
|
||||
initElements() {
|
||||
this.elements = {
|
||||
loading: document.getElementById('loading'),
|
||||
questionContainer: document.getElementById('questionContainer'),
|
||||
errorContainer: document.getElementById('errorContainer'),
|
||||
questionId: document.getElementById('questionId'),
|
||||
questionText: document.getElementById('questionText'),
|
||||
codeContent: document.getElementById('codeContent'),
|
||||
optionsContainer: document.getElementById('optionsContainer'),
|
||||
submitBtn: document.getElementById('submitBtn'),
|
||||
showAnswerBtn: document.getElementById('showAnswerBtn'),
|
||||
refreshBtn: document.getElementById('refreshBtn'),
|
||||
retryBtn: document.getElementById('retryBtn'),
|
||||
exportBtn: document.getElementById('exportBtn'),
|
||||
resultContainer: document.getElementById('resultContainer'),
|
||||
resultStatus: document.getElementById('resultStatus'),
|
||||
correctAnswer: document.getElementById('correctAnswer'),
|
||||
explanation: document.getElementById('explanation'),
|
||||
errorMessage: document.getElementById('errorMessage')
|
||||
};
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvents() {
|
||||
this.elements.submitBtn.addEventListener('click', () => this.submitAnswer());
|
||||
this.elements.showAnswerBtn.addEventListener('click', () => this.showAnswer());
|
||||
this.elements.refreshBtn.addEventListener('click', () => this.loadQuestion());
|
||||
this.elements.retryBtn.addEventListener('click', () => this.loadQuestion());
|
||||
this.elements.exportBtn.addEventListener('click', () => this.exportToMarkdown());
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
showLoading() {
|
||||
this.elements.loading.style.display = 'block';
|
||||
this.elements.questionContainer.style.display = 'none';
|
||||
this.elements.errorContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// 显示题目
|
||||
showQuestion() {
|
||||
this.elements.loading.style.display = 'none';
|
||||
this.elements.questionContainer.style.display = 'block';
|
||||
this.elements.errorContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// 显示错误
|
||||
showError(message) {
|
||||
this.elements.loading.style.display = 'none';
|
||||
this.elements.questionContainer.style.display = 'none';
|
||||
this.elements.errorContainer.style.display = 'block';
|
||||
this.elements.errorMessage.textContent = message;
|
||||
}
|
||||
|
||||
// 获取当前API地址
|
||||
getCurrentApiUrl() {
|
||||
return `${this.apiEndpoints[this.currentApiIndex]}/v2/awesome-js`;
|
||||
}
|
||||
|
||||
// 切换到下一个API
|
||||
switchToNextApi() {
|
||||
this.currentApiIndex = (this.currentApiIndex + 1) % this.apiEndpoints.length;
|
||||
}
|
||||
|
||||
// 加载题目
|
||||
async loadQuestion() {
|
||||
this.showLoading();
|
||||
this.resetQuestion();
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = this.apiEndpoints.length;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const response = await fetch(this.getCurrentApiUrl(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.code === 200 && data.data) {
|
||||
this.currentQuestion = data.data;
|
||||
this.displayQuestion();
|
||||
return;
|
||||
} else {
|
||||
throw new Error(data.message || '数据格式错误');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`API ${this.getCurrentApiUrl()} 请求失败:`, error.message);
|
||||
attempts++;
|
||||
|
||||
if (attempts < maxAttempts) {
|
||||
this.switchToNextApi();
|
||||
console.log(`切换到备用API: ${this.getCurrentApiUrl()}`);
|
||||
} else {
|
||||
this.showError(`所有API接口都无法访问,请检查网络连接后重试。\n最后尝试的错误: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重置题目状态
|
||||
resetQuestion() {
|
||||
this.selectedOption = null;
|
||||
this.isAnswered = false;
|
||||
this.elements.resultContainer.style.display = 'none';
|
||||
this.elements.submitBtn.disabled = true;
|
||||
this.elements.submitBtn.textContent = '提交答案';
|
||||
this.elements.showAnswerBtn.style.display = 'inline-block';
|
||||
|
||||
// 清空选项容器,防止重复显示
|
||||
this.elements.optionsContainer.innerHTML = '';
|
||||
|
||||
// 移除所有选项的事件监听器
|
||||
const existingOptions = document.querySelectorAll('.option');
|
||||
existingOptions.forEach(option => {
|
||||
option.removeEventListener('click', this.selectOption);
|
||||
});
|
||||
}
|
||||
|
||||
// 显示题目内容
|
||||
displayQuestion() {
|
||||
const question = this.currentQuestion;
|
||||
|
||||
console.log('显示题目:', question);
|
||||
|
||||
// 设置题目ID
|
||||
this.elements.questionId.textContent = `题目 #${question.id}`;
|
||||
|
||||
// 设置题目文本
|
||||
this.elements.questionText.innerHTML = `<h2>${this.escapeHtml(question.question)}</h2>`;
|
||||
|
||||
// 设置代码内容并应用语法高亮
|
||||
this.elements.codeContent.textContent = question.code;
|
||||
|
||||
// 应用语法高亮
|
||||
if (typeof hljs !== 'undefined') {
|
||||
hljs.highlightElement(this.elements.codeContent);
|
||||
}
|
||||
|
||||
// 确保选项容器已清空
|
||||
this.elements.optionsContainer.innerHTML = '';
|
||||
|
||||
// 生成选项
|
||||
this.generateOptions(question.options);
|
||||
|
||||
this.showQuestion();
|
||||
}
|
||||
|
||||
// 生成选项
|
||||
generateOptions(options) {
|
||||
// 确保清空容器
|
||||
this.elements.optionsContainer.innerHTML = '';
|
||||
|
||||
// 验证选项数据
|
||||
if (!Array.isArray(options) || options.length === 0) {
|
||||
console.error('选项数据无效:', options);
|
||||
return;
|
||||
}
|
||||
|
||||
// 移除可能存在的重复选项
|
||||
const uniqueOptions = [...new Set(options)];
|
||||
|
||||
uniqueOptions.forEach((option, index) => {
|
||||
const optionElement = document.createElement('div');
|
||||
optionElement.className = 'option';
|
||||
optionElement.textContent = option;
|
||||
optionElement.dataset.index = index;
|
||||
optionElement.dataset.value = option.charAt(0); // A, B, C, D
|
||||
|
||||
optionElement.addEventListener('click', () => this.selectOption(optionElement));
|
||||
|
||||
this.elements.optionsContainer.appendChild(optionElement);
|
||||
});
|
||||
|
||||
console.log('生成选项:', uniqueOptions);
|
||||
}
|
||||
|
||||
// 选择选项
|
||||
selectOption(optionElement) {
|
||||
if (this.isAnswered) return;
|
||||
|
||||
// 移除之前的选中状态
|
||||
document.querySelectorAll('.option.selected').forEach(el => {
|
||||
el.classList.remove('selected');
|
||||
});
|
||||
|
||||
// 设置当前选中
|
||||
optionElement.classList.add('selected');
|
||||
this.selectedOption = optionElement.dataset.value;
|
||||
|
||||
// 启用提交按钮
|
||||
this.elements.submitBtn.disabled = false;
|
||||
}
|
||||
|
||||
// 提交答案
|
||||
submitAnswer() {
|
||||
if (!this.selectedOption || this.isAnswered) return;
|
||||
|
||||
this.isAnswered = true;
|
||||
this.elements.submitBtn.disabled = true;
|
||||
this.elements.submitBtn.textContent = '已提交';
|
||||
this.elements.showAnswerBtn.style.display = 'none';
|
||||
|
||||
const isCorrect = this.selectedOption === this.currentQuestion.answer;
|
||||
|
||||
// 显示结果
|
||||
this.showResult(isCorrect);
|
||||
|
||||
// 标记选项
|
||||
this.markOptions();
|
||||
}
|
||||
|
||||
// 显示答案
|
||||
showAnswer() {
|
||||
this.isAnswered = true;
|
||||
this.elements.submitBtn.disabled = true;
|
||||
this.elements.submitBtn.textContent = '已显示答案';
|
||||
this.elements.showAnswerBtn.style.display = 'none';
|
||||
|
||||
// 显示结果(不判断对错)
|
||||
this.showResult(null);
|
||||
|
||||
// 标记正确答案
|
||||
this.markCorrectAnswer();
|
||||
}
|
||||
|
||||
// 显示结果
|
||||
showResult(isCorrect) {
|
||||
const resultContainer = this.elements.resultContainer;
|
||||
const resultStatus = this.elements.resultStatus;
|
||||
const correctAnswer = this.elements.correctAnswer;
|
||||
const explanation = this.elements.explanation;
|
||||
|
||||
// 设置结果状态
|
||||
if (isCorrect === true) {
|
||||
resultStatus.textContent = '✅ 回答正确!';
|
||||
resultStatus.className = 'result-status correct';
|
||||
} else if (isCorrect === false) {
|
||||
resultStatus.textContent = '❌ 回答错误';
|
||||
resultStatus.className = 'result-status incorrect';
|
||||
} else {
|
||||
resultStatus.textContent = '💡 答案解析';
|
||||
resultStatus.className = 'result-status';
|
||||
}
|
||||
|
||||
// 设置正确答案
|
||||
correctAnswer.textContent = `正确答案: ${this.currentQuestion.answer}`;
|
||||
|
||||
// 设置解析内容
|
||||
explanation.innerHTML = this.formatExplanation(this.currentQuestion.explanation);
|
||||
|
||||
// 显示结果容器
|
||||
resultContainer.style.display = 'block';
|
||||
|
||||
// 滚动到结果区域
|
||||
setTimeout(() => {
|
||||
resultContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 标记选项
|
||||
markOptions() {
|
||||
const options = document.querySelectorAll('.option');
|
||||
const correctAnswer = this.currentQuestion.answer;
|
||||
|
||||
options.forEach(option => {
|
||||
const optionValue = option.dataset.value;
|
||||
|
||||
if (optionValue === correctAnswer) {
|
||||
option.classList.add('correct');
|
||||
} else if (option.classList.contains('selected')) {
|
||||
option.classList.add('incorrect');
|
||||
}
|
||||
|
||||
// 禁用点击
|
||||
option.style.pointerEvents = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// 标记正确答案
|
||||
markCorrectAnswer() {
|
||||
const options = document.querySelectorAll('.option');
|
||||
const correctAnswer = this.currentQuestion.answer;
|
||||
|
||||
options.forEach(option => {
|
||||
const optionValue = option.dataset.value;
|
||||
|
||||
if (optionValue === correctAnswer) {
|
||||
option.classList.add('correct');
|
||||
}
|
||||
|
||||
// 禁用点击
|
||||
option.style.pointerEvents = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化解析内容
|
||||
formatExplanation(explanation) {
|
||||
// 转义HTML
|
||||
let formatted = this.escapeHtml(explanation);
|
||||
|
||||
// 处理代码块
|
||||
formatted = formatted.replace(/```js\n([\s\S]*?)\n```/g, '<pre><code>$1</code></pre>');
|
||||
formatted = formatted.replace(/```javascript\n([\s\S]*?)\n```/g, '<pre><code>$1</code></pre>');
|
||||
formatted = formatted.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
||||
|
||||
// 处理行内代码
|
||||
formatted = formatted.replace(/`([^`]+)`/g, '<code style="background: #f0f0f0; padding: 2px 4px; border-radius: 3px; font-family: monospace;">$1</code>');
|
||||
|
||||
// 处理换行
|
||||
formatted = formatted.replace(/\n\n/g, '</p><p>');
|
||||
formatted = formatted.replace(/\n/g, '<br>');
|
||||
|
||||
// 包装段落
|
||||
if (!formatted.includes('<p>')) {
|
||||
formatted = '<p>' + formatted + '</p>';
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
// HTML转义
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 导出为Markdown
|
||||
exportToMarkdown() {
|
||||
if (!this.currentQuestion) {
|
||||
alert('请先加载题目后再导出!');
|
||||
return;
|
||||
}
|
||||
|
||||
const question = this.currentQuestion;
|
||||
const timestamp = new Date().toLocaleString('zh-CN');
|
||||
|
||||
// 构建Markdown内容
|
||||
let markdown = `# JavaScript趣味题 #${question.id}\n\n`;
|
||||
markdown += `> 导出时间: ${timestamp}\n\n`;
|
||||
|
||||
// 题目部分
|
||||
markdown += `## 题目\n\n`;
|
||||
markdown += `${question.question}\n\n`;
|
||||
|
||||
// 代码部分
|
||||
markdown += `## 代码\n\n`;
|
||||
markdown += `\`\`\`javascript\n${question.code}\n\`\`\`\n\n`;
|
||||
|
||||
// 选项部分
|
||||
markdown += `## 选项\n\n`;
|
||||
question.options.forEach((option, index) => {
|
||||
const letter = String.fromCharCode(65 + index); // A, B, C, D
|
||||
const isCorrect = letter === question.answer;
|
||||
markdown += `${letter}. ${option}${isCorrect ? ' ✅' : ''}\n`;
|
||||
});
|
||||
markdown += `\n`;
|
||||
|
||||
// 答案部分
|
||||
markdown += `## 正确答案\n\n`;
|
||||
markdown += `**${question.answer}**\n\n`;
|
||||
|
||||
// 解析部分
|
||||
markdown += `## 答案解析\n\n`;
|
||||
// 清理解析内容中的HTML标签,转换为Markdown格式
|
||||
let explanation = question.explanation;
|
||||
explanation = explanation.replace(/<br\s*\/?>/gi, '\n');
|
||||
explanation = explanation.replace(/<p>/gi, '\n');
|
||||
explanation = explanation.replace(/<\/p>/gi, '\n');
|
||||
explanation = explanation.replace(/<code[^>]*>/gi, '`');
|
||||
explanation = explanation.replace(/<\/code>/gi, '`');
|
||||
explanation = explanation.replace(/<pre><code>/gi, '\n```javascript\n');
|
||||
explanation = explanation.replace(/<\/code><\/pre>/gi, '\n```\n');
|
||||
explanation = explanation.replace(/<[^>]*>/g, ''); // 移除其他HTML标签
|
||||
explanation = explanation.replace(/\n\s*\n/g, '\n\n'); // 清理多余空行
|
||||
markdown += explanation.trim() + '\n\n';
|
||||
|
||||
// 添加页脚
|
||||
markdown += `---\n\n`;
|
||||
markdown += `*本题目来源于JavaScript趣味题集合*\n`;
|
||||
markdown += `*导出工具: JavaScript趣味题网页版*\n`;
|
||||
|
||||
// 创建下载
|
||||
this.downloadMarkdown(markdown, `JavaScript趣味题_${question.id}_${new Date().getTime()}.md`);
|
||||
}
|
||||
|
||||
// 下载Markdown文件
|
||||
downloadMarkdown(content, filename) {
|
||||
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.style.display = 'none';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 清理URL对象
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 100);
|
||||
|
||||
// 显示成功提示
|
||||
this.showExportSuccess(filename);
|
||||
}
|
||||
|
||||
// 显示导出成功提示
|
||||
showExportSuccess(filename) {
|
||||
// 创建临时提示元素
|
||||
const toast = document.createElement('div');
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: linear-gradient(135deg, #4caf50, #45a049);
|
||||
color: white;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
||||
z-index: 10000;
|
||||
font-size: 14px;
|
||||
max-width: 300px;
|
||||
word-wrap: break-word;
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
`;
|
||||
|
||||
toast.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
<div>
|
||||
<div style="font-weight: 600;">导出成功!</div>
|
||||
<div style="font-size: 12px; opacity: 0.9; margin-top: 2px;">${filename}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 添加动画样式
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes slideOutRight {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOutRight 0.3s ease-in';
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
document.body.removeChild(toast);
|
||||
}
|
||||
if (style.parentNode) {
|
||||
document.head.removeChild(style);
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化应用
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new JSQuizApp();
|
||||
});
|
||||
|
||||
// 添加键盘快捷键支持
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// 按R键刷新题目
|
||||
if (e.key.toLowerCase() === 'r' && !e.ctrlKey && !e.metaKey) {
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
if (refreshBtn && !document.querySelector('.loading').style.display !== 'none') {
|
||||
refreshBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
// 按数字键1-4选择选项
|
||||
if (['1', '2', '3', '4'].includes(e.key)) {
|
||||
const options = document.querySelectorAll('.option');
|
||||
const index = parseInt(e.key) - 1;
|
||||
if (options[index] && !options[index].style.pointerEvents) {
|
||||
options[index].click();
|
||||
}
|
||||
}
|
||||
|
||||
// 按Enter键提交答案
|
||||
if (e.key === 'Enter') {
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
if (submitBtn && !submitBtn.disabled) {
|
||||
submitBtn.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 添加触摸设备支持
|
||||
if ('ontouchstart' in window) {
|
||||
document.addEventListener('touchstart', () => {}, { passive: true });
|
||||
}
|
||||
|
||||
// 添加网络状态监听
|
||||
if ('navigator' in window && 'onLine' in navigator) {
|
||||
window.addEventListener('online', () => {
|
||||
console.log('网络连接已恢复');
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
console.log('网络连接已断开');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||
"data": {
|
||||
"id": 11,
|
||||
"question": "输出是什么?",
|
||||
"code": "function Person(firstName, lastName) {\n this.firstName = firstName;\n this.lastName = lastName;\n}\n\nconst member = new Person(\"Lydia\", \"Hallie\");\nPerson.getFullName = function () {\n return `${this.firstName} ${this.lastName}`;\n}\n\nconsole.log(member.getFullName());",
|
||||
"options": [
|
||||
"A: `TypeError`",
|
||||
"B: `SyntaxError`",
|
||||
"C: `Lydia Hallie`",
|
||||
"D: `undefined` `undefined`"
|
||||
],
|
||||
"answer": "A",
|
||||
"explanation": "你不能像常规对象那样,给构造函数添加属性。如果你想一次性给所有实例添加特性,你应该使用原型。因此本例中,使用如下方式:\n\n```js\nPerson.prototype.getFullName = function () {\n return `${this.firstName} ${this.lastName}`;\n}\n```\n\n这才会使 `member.getFullName()` 起作用。为什么这么做有益的?假设我们将这个方法添加到构造函数本身里。也许不是每个 `Person` 实例都需要这个方法。这将浪费大量内存空间,因为它们仍然具有该属性,这将占用每个实例的内存空间。相反,如果我们只将它添加到原型中,那么它只存在于内存中的一个位置,但是所有实例都可以访问它!"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/* 背景样式文件 */
|
||||
body {
|
||||
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 25%, #ffd3a5 50%, #a8e6cf 75%, #88d8a3 100%);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 背景动画 */
|
||||
@keyframes gradientShift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 背景装饰元素 */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 80%, rgba(39, 174, 96, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(46, 204, 113, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(26, 188, 156, 0.05) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* 浮动装饰圆点 */
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
radial-gradient(2px 2px at 20px 30px, rgba(39, 174, 96, 0.3), transparent),
|
||||
radial-gradient(2px 2px at 40px 70px, rgba(46, 204, 113, 0.2), transparent),
|
||||
radial-gradient(1px 1px at 90px 40px, rgba(26, 188, 156, 0.3), transparent),
|
||||
radial-gradient(1px 1px at 130px 80px, rgba(39, 174, 96, 0.2), transparent),
|
||||
radial-gradient(2px 2px at 160px 30px, rgba(46, 204, 113, 0.3), transparent);
|
||||
background-repeat: repeat;
|
||||
background-size: 200px 100px;
|
||||
animation: floatDots 20s linear infinite;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes floatDots {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式背景调整 */
|
||||
@media (max-width: 768px) {
|
||||
body::after {
|
||||
background-size: 150px 75px;
|
||||
animation-duration: 25s;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
body::after {
|
||||
background-size: 100px 50px;
|
||||
animation-duration: 30s;
|
||||
}
|
||||
}
|
||||
339
frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/css/style.css
Normal file
339
frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/css/style.css
Normal file
@@ -0,0 +1,339 @@
|
||||
/* 基础样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #2c3e50;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 容器布局 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #27ae60;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 2px 4px rgba(39, 174, 96, 0.2);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: #7f8c8d;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(39, 174, 96, 0.1);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.content-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* KFC文案内容 */
|
||||
.kfc-content {
|
||||
min-height: 200px;
|
||||
padding: 30px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 15px;
|
||||
border-left: 5px solid #27ae60;
|
||||
margin-bottom: 30px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.kfc-content::before {
|
||||
content: '"';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 15px;
|
||||
font-size: 3rem;
|
||||
color: #27ae60;
|
||||
opacity: 0.3;
|
||||
font-family: serif;
|
||||
}
|
||||
|
||||
.kfc-content p {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
color: #2c3e50;
|
||||
margin-left: 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 按钮组 */
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.generate-btn, .copy-btn {
|
||||
padding: 15px 30px;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3);
|
||||
}
|
||||
|
||||
.generate-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(39, 174, 96, 0.4);
|
||||
}
|
||||
|
||||
.generate-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.generate-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: linear-gradient(135deg, #3498db 0%, #5dade2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
|
||||
}
|
||||
|
||||
/* 编号信息 */
|
||||
.index-info {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background: rgba(39, 174, 96, 0.1);
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(39, 174, 96, 0.2);
|
||||
}
|
||||
|
||||
.index-text {
|
||||
color: #27ae60;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#indexNumber {
|
||||
color: #2c3e50;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 底部 */
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
color: #7f8c8d;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
/* 提示框 */
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
padding: 15px 25px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
transform: translateX(400px);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 1000;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* 平板端适配 (768px - 1024px) */
|
||||
@media (max-width: 1024px) and (min-width: 768px) {
|
||||
.container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
padding: 35px;
|
||||
max-width: 550px;
|
||||
}
|
||||
|
||||
.kfc-content {
|
||||
padding: 25px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.generate-btn, .copy-btn {
|
||||
padding: 12px 25px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机端适配 (最大768px) */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
padding: 25px;
|
||||
margin: 0 5px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.kfc-content {
|
||||
padding: 20px;
|
||||
min-height: 150px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.kfc-content::before {
|
||||
font-size: 2.5rem;
|
||||
top: 5px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.kfc-content p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.generate-btn, .copy-btn {
|
||||
padding: 12px 20px;
|
||||
font-size: 0.9rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 0.8rem;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
transform: translateY(-100px);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏手机适配 (最大480px) */
|
||||
@media (max-width: 480px) {
|
||||
.title {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.kfc-content {
|
||||
padding: 15px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.kfc-content p {
|
||||
font-size: 0.95rem;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.generate-btn, .copy-btn {
|
||||
padding: 10px 15px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
46
frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/index.html
Normal file
46
frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/index.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>随机KFC文案生成器</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/background.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1 class="title">🍗 随机KFC文案生成器</h1>
|
||||
<p class="subtitle">疯狂星期四,文案来一套!</p>
|
||||
</header>
|
||||
|
||||
<main class="main">
|
||||
<div class="content-card">
|
||||
<div class="kfc-content" id="kfcContent">
|
||||
<p class="loading-text">点击按钮获取随机KFC文案...</p>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="generate-btn" id="generateBtn">
|
||||
<span class="btn-text">生成文案</span>
|
||||
<span class="btn-loading" style="display: none;">生成中...</span>
|
||||
</button>
|
||||
<button class="copy-btn" id="copyBtn" style="display: none;">复制文案</button>
|
||||
</div>
|
||||
|
||||
<div class="index-info" id="indexInfo" style="display: none;">
|
||||
<span class="index-text">文案编号: <span id="indexNumber"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© 2024 KFC文案生成器 | 让每个星期四都疯狂起来</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
240
frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/js/main.js
vendored
Normal file
240
frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/js/main.js
vendored
Normal file
@@ -0,0 +1,240 @@
|
||||
// KFC文案生成器主要功能
|
||||
class KFCGenerator {
|
||||
constructor() {
|
||||
this.apiEndpoints = [];
|
||||
this.currentApiIndex = 0;
|
||||
this.isLoading = false;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 初始化
|
||||
async init() {
|
||||
await this.loadApiEndpoints();
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
// 加载API接口列表
|
||||
async loadApiEndpoints() {
|
||||
try {
|
||||
const response = await fetch('./接口集合.json');
|
||||
this.apiEndpoints = await response.json();
|
||||
} catch (error) {
|
||||
console.error('加载API接口列表失败:', error);
|
||||
this.showToast('加载接口配置失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvents() {
|
||||
const generateBtn = document.getElementById('generateBtn');
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
|
||||
generateBtn.addEventListener('click', () => this.generateKFC());
|
||||
copyBtn.addEventListener('click', () => this.copyContent());
|
||||
}
|
||||
|
||||
// 生成KFC文案
|
||||
async generateKFC() {
|
||||
if (this.isLoading) return;
|
||||
|
||||
this.setLoadingState(true);
|
||||
|
||||
let success = false;
|
||||
let attempts = 0;
|
||||
const maxAttempts = this.apiEndpoints.length;
|
||||
|
||||
while (!success && attempts < maxAttempts) {
|
||||
try {
|
||||
const apiUrl = this.apiEndpoints[this.currentApiIndex];
|
||||
const data = await this.fetchKFCData(apiUrl);
|
||||
|
||||
if (data && data.code === 200 && data.data && data.data.kfc) {
|
||||
this.displayKFC(data.data);
|
||||
success = true;
|
||||
} else {
|
||||
throw new Error('API返回数据格式错误');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`API ${this.currentApiIndex + 1} 请求失败:`, error);
|
||||
this.currentApiIndex = (this.currentApiIndex + 1) % this.apiEndpoints.length;
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
this.showError('所有API接口都无法访问,请稍后重试');
|
||||
}
|
||||
|
||||
this.setLoadingState(false);
|
||||
}
|
||||
|
||||
// 请求KFC数据
|
||||
async fetchKFCData(apiUrl) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/v2/kfc`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示KFC文案
|
||||
displayKFC(data) {
|
||||
const contentElement = document.getElementById('kfcContent');
|
||||
const indexElement = document.getElementById('indexNumber');
|
||||
const indexInfo = document.getElementById('indexInfo');
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
|
||||
// 显示文案内容
|
||||
contentElement.innerHTML = `<p>${this.escapeHtml(data.kfc)}</p>`;
|
||||
|
||||
// 显示编号信息
|
||||
if (data.index) {
|
||||
indexElement.textContent = data.index;
|
||||
indexInfo.style.display = 'block';
|
||||
} else {
|
||||
indexInfo.style.display = 'none';
|
||||
}
|
||||
|
||||
// 显示复制按钮
|
||||
copyBtn.style.display = 'inline-block';
|
||||
|
||||
// 添加显示动画
|
||||
contentElement.style.opacity = '0';
|
||||
contentElement.style.transform = 'translateY(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
contentElement.style.transition = 'all 0.5s ease';
|
||||
contentElement.style.opacity = '1';
|
||||
contentElement.style.transform = 'translateY(0)';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 显示错误信息
|
||||
showError(message) {
|
||||
const contentElement = document.getElementById('kfcContent');
|
||||
contentElement.innerHTML = `<p class="loading-text" style="color: #e74c3c;">${this.escapeHtml(message)}</p>`;
|
||||
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
const indexInfo = document.getElementById('indexInfo');
|
||||
copyBtn.style.display = 'none';
|
||||
indexInfo.style.display = 'none';
|
||||
}
|
||||
|
||||
// 复制文案内容
|
||||
async copyContent() {
|
||||
const contentElement = document.getElementById('kfcContent');
|
||||
const textContent = contentElement.querySelector('p')?.textContent;
|
||||
|
||||
if (!textContent || textContent.includes('点击按钮获取') || textContent.includes('失败')) {
|
||||
this.showToast('没有可复制的内容', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(textContent);
|
||||
} else {
|
||||
// 降级方案
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = textContent;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
textArea.remove();
|
||||
}
|
||||
|
||||
this.showToast('文案已复制到剪贴板', 'success');
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error);
|
||||
this.showToast('复制失败,请手动选择复制', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 设置加载状态
|
||||
setLoadingState(loading) {
|
||||
this.isLoading = loading;
|
||||
const generateBtn = document.getElementById('generateBtn');
|
||||
const btnText = generateBtn.querySelector('.btn-text');
|
||||
const btnLoading = generateBtn.querySelector('.btn-loading');
|
||||
|
||||
if (loading) {
|
||||
generateBtn.disabled = true;
|
||||
btnText.style.display = 'none';
|
||||
btnLoading.style.display = 'inline';
|
||||
} else {
|
||||
generateBtn.disabled = false;
|
||||
btnText.style.display = 'inline';
|
||||
btnLoading.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示提示消息
|
||||
showToast(message, type = 'success') {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.className = `toast ${type}`;
|
||||
toast.classList.add('show');
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// HTML转义
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const generator = new KFCGenerator();
|
||||
// 页面加载完成后自动生成一条文案
|
||||
setTimeout(() => {
|
||||
generator.generateKFC();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// 添加键盘快捷键支持
|
||||
document.addEventListener('keydown', (event) => {
|
||||
// 按空格键生成文案
|
||||
if (event.code === 'Space' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') {
|
||||
event.preventDefault();
|
||||
document.getElementById('generateBtn').click();
|
||||
}
|
||||
|
||||
// Ctrl+C 复制文案
|
||||
if (event.ctrlKey && event.key === 'c' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') {
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
if (copyBtn.style.display !== 'none') {
|
||||
event.preventDefault();
|
||||
copyBtn.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
7
frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/接口集合.json
Normal file
7
frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/接口集合.json
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
]
|
||||
1
frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/返回接口.json
Normal file
1
frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/返回接口.json
Normal file
@@ -0,0 +1 @@
|
||||
{"code":200,"message":"获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841","data":{"index":369,"kfc":"我不想你带着情绪过夜,这样会把你推的越来越远,我不想你离开我,我希望你可以多爱我一点,我不想因为一点点小问题就让我们两之间的感情产生隔阂。对我来说,双向奔赴的感情相处才有意义,看不到希望的事情我没办法坚持太久,我不想浪费时间在一些无意义无结果的事情上,时间应该用来做我该做的事情。很多人走不到最后,是因为理解不同步,但我跟你在一起,我知道你什样的人,我了解你甚至比了解自己还多,我和你在一起,不是因为其他,而是因为今天肯德基疯狂星期四,我希望你能请我吃!"}}
|
||||
167
frontend/react-app/public/60sapi/娱乐消遣/随机一言/css/background.css
Normal file
167
frontend/react-app/public/60sapi/娱乐消遣/随机一言/css/background.css
Normal file
@@ -0,0 +1,167 @@
|
||||
/* 背景样式文件 - 金色光辉主题 */
|
||||
|
||||
/* 主背景 */
|
||||
body {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#fff8dc 0%,
|
||||
#ffeaa7 25%,
|
||||
#fdcb6e 50%,
|
||||
#e17055 75%,
|
||||
#d63031 100%
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 背景装饰层 */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(255, 215, 0, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 223, 0, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(212, 175, 55, 0.05) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 动态光点效果 */
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
radial-gradient(2px 2px at 20px 30px, rgba(255, 215, 0, 0.3), transparent),
|
||||
radial-gradient(2px 2px at 40px 70px, rgba(255, 223, 0, 0.2), transparent),
|
||||
radial-gradient(1px 1px at 90px 40px, rgba(212, 175, 55, 0.4), transparent),
|
||||
radial-gradient(1px 1px at 130px 80px, rgba(255, 215, 0, 0.2), transparent),
|
||||
radial-gradient(2px 2px at 160px 30px, rgba(255, 223, 0, 0.3), transparent);
|
||||
background-repeat: repeat;
|
||||
background-size: 200px 100px;
|
||||
animation: sparkle 20s linear infinite;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 背景动画 */
|
||||
@keyframes gradientShift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sparkle {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-100vh);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式背景调整 */
|
||||
|
||||
/* 平板端背景 */
|
||||
@media (min-width: 768px) and (max-width: 1024px) {
|
||||
body::after {
|
||||
background-size: 250px 120px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 电脑端背景 */
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
background-size: 300% 300%;
|
||||
}
|
||||
|
||||
body::after {
|
||||
background-size: 300px 150px;
|
||||
animation-duration: 25s;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机端背景优化 */
|
||||
@media (max-width: 767px) {
|
||||
body {
|
||||
background-size: 200% 200%;
|
||||
animation-duration: 10s;
|
||||
}
|
||||
|
||||
body::before {
|
||||
background:
|
||||
radial-gradient(circle at 30% 70%, rgba(255, 215, 0, 0.08) 0%, transparent 40%),
|
||||
radial-gradient(circle at 70% 30%, rgba(255, 223, 0, 0.08) 0%, transparent 40%);
|
||||
}
|
||||
|
||||
body::after {
|
||||
background-size: 150px 80px;
|
||||
animation-duration: 15s;
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕背景 */
|
||||
@media (max-width: 479px) {
|
||||
body {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#fff8dc 0%,
|
||||
#ffeaa7 50%,
|
||||
#fdcb6e 100%
|
||||
);
|
||||
background-size: 150% 150%;
|
||||
}
|
||||
|
||||
body::after {
|
||||
background-size: 120px 60px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#2c1810 0%,
|
||||
#3d2914 25%,
|
||||
#4a3319 50%,
|
||||
#5c3e1f 75%,
|
||||
#6b4423 100%
|
||||
);
|
||||
}
|
||||
|
||||
body::before {
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(255, 215, 0, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 223, 0, 0.05) 0%, transparent 50%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动画效果(用户偏好) */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body,
|
||||
body::before,
|
||||
body::after {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #fff8dc 0%, #ffeaa7 50%, #fdcb6e 100%);
|
||||
}
|
||||
}
|
||||
357
frontend/react-app/public/60sapi/娱乐消遣/随机一言/css/style.css
Normal file
357
frontend/react-app/public/60sapi/娱乐消遣/随机一言/css/style.css
Normal file
@@ -0,0 +1,357 @@
|
||||
/* 基础样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #2c1810;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 容器布局 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #d4af37;
|
||||
text-shadow:
|
||||
0 0 10px rgba(212, 175, 55, 0.8),
|
||||
0 0 20px rgba(212, 175, 55, 0.6),
|
||||
0 0 30px rgba(212, 175, 55, 0.4);
|
||||
margin-bottom: 10px;
|
||||
animation: titleGlow 3s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: #b8860b;
|
||||
opacity: 0.9;
|
||||
text-shadow: 0 0 5px rgba(184, 134, 11, 0.5);
|
||||
}
|
||||
|
||||
/* 主内容区域 */
|
||||
.main-content {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 一言容器 */
|
||||
.quote-container {
|
||||
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1), rgba(255, 223, 0, 0.05));
|
||||
border: 2px solid rgba(212, 175, 55, 0.3);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
margin-bottom: 30px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(212, 175, 55, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quote-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(45deg, #ffd700, #ffed4e, #ffd700, #ffed4e);
|
||||
border-radius: 22px;
|
||||
z-index: -1;
|
||||
animation: borderGlow 4s linear infinite;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(212, 175, 55, 0.3);
|
||||
border-top: 4px solid #d4af37;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto 15px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 一言显示 */
|
||||
.quote-display {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quote-display.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.8;
|
||||
color: #2c1810;
|
||||
margin-bottom: 20px;
|
||||
text-shadow: 0 1px 2px rgba(212, 175, 55, 0.1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quote-index {
|
||||
font-size: 0.9rem;
|
||||
color: #b8860b;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 错误信息 */
|
||||
.error-message {
|
||||
display: none;
|
||||
text-align: center;
|
||||
color: #cd853f;
|
||||
}
|
||||
|
||||
.error-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 控制按钮 */
|
||||
.controls {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
padding: 15px 30px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #2c1810;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow:
|
||||
0 4px 15px rgba(212, 175, 55, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(212, 175, 55, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.refresh-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.refresh-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1.2rem;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.refresh-btn:hover .btn-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 底部 */
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
color: #b8860b;
|
||||
opacity: 0.8;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes titleGlow {
|
||||
0% {
|
||||
text-shadow:
|
||||
0 0 10px rgba(212, 175, 55, 0.8),
|
||||
0 0 20px rgba(212, 175, 55, 0.6),
|
||||
0 0 30px rgba(212, 175, 55, 0.4);
|
||||
}
|
||||
100% {
|
||||
text-shadow:
|
||||
0 0 15px rgba(212, 175, 55, 1),
|
||||
0 0 25px rgba(212, 175, 55, 0.8),
|
||||
0 0 35px rgba(212, 175, 55, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes borderGlow {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 平板端适配 (768px - 1024px) */
|
||||
@media (min-width: 768px) and (max-width: 1024px) {
|
||||
.container {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.quote-container {
|
||||
padding: 50px;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 电脑端适配 (1024px+) */
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.quote-container {
|
||||
padding: 60px;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 18px 36px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机端适配 (小于768px) */
|
||||
@media (max-width: 767px) {
|
||||
.container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.quote-container {
|
||||
padding: 25px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 12px 24px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕适配 (小于480px) */
|
||||
@media (max-width: 479px) {
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.quote-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
52
frontend/react-app/public/60sapi/娱乐消遣/随机一言/index.html
Normal file
52
frontend/react-app/public/60sapi/娱乐消遣/随机一言/index.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>随机一言 - 金色光辉</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/background.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1 class="title">随机一言</h1>
|
||||
<p class="subtitle">每一句话都是心灵的光芒</p>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="quote-container">
|
||||
<div class="loading" id="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>正在获取一言...</p>
|
||||
</div>
|
||||
|
||||
<div class="quote-display" id="quoteDisplay">
|
||||
<div class="quote-text" id="quoteText">
|
||||
点击下方按钮获取一言
|
||||
</div>
|
||||
<div class="quote-index" id="quoteIndex"></div>
|
||||
</div>
|
||||
|
||||
<div class="error-message" id="errorMessage">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<div class="error-text" id="errorText"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="refresh-btn" id="refreshBtn">
|
||||
<span class="btn-icon">🔄</span>
|
||||
<span class="btn-text">获取新一言</span>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>愿每一句话都能温暖你的心</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
225
frontend/react-app/public/60sapi/娱乐消遣/随机一言/js/script.js
vendored
Normal file
225
frontend/react-app/public/60sapi/娱乐消遣/随机一言/js/script.js
vendored
Normal file
@@ -0,0 +1,225 @@
|
||||
// 随机一言 JavaScript 功能实现
|
||||
|
||||
class HitokotoApp {
|
||||
constructor() {
|
||||
// API接口列表
|
||||
this.apiEndpoints = [
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
];
|
||||
|
||||
this.currentEndpointIndex = 0;
|
||||
this.isLoading = false;
|
||||
|
||||
// DOM 元素
|
||||
this.elements = {
|
||||
loading: document.getElementById('loading'),
|
||||
quoteDisplay: document.getElementById('quoteDisplay'),
|
||||
quoteText: document.getElementById('quoteText'),
|
||||
quoteIndex: document.getElementById('quoteIndex'),
|
||||
errorMessage: document.getElementById('errorMessage'),
|
||||
errorText: document.getElementById('errorText'),
|
||||
refreshBtn: document.getElementById('refreshBtn')
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.hideAllStates();
|
||||
this.showQuoteDisplay();
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvents() {
|
||||
this.elements.refreshBtn.addEventListener('click', () => {
|
||||
this.fetchHitokoto();
|
||||
});
|
||||
|
||||
// 键盘快捷键支持
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.code === 'Space' && !this.isLoading) {
|
||||
e.preventDefault();
|
||||
this.fetchHitokoto();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 隐藏所有状态
|
||||
hideAllStates() {
|
||||
this.elements.loading.classList.remove('show');
|
||||
this.elements.quoteDisplay.classList.remove('hide');
|
||||
this.elements.errorMessage.classList.remove('show');
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
showLoading() {
|
||||
this.hideAllStates();
|
||||
this.elements.loading.classList.add('show');
|
||||
this.elements.quoteDisplay.classList.add('hide');
|
||||
this.elements.refreshBtn.disabled = true;
|
||||
this.isLoading = true;
|
||||
}
|
||||
|
||||
// 显示一言内容
|
||||
showQuoteDisplay() {
|
||||
this.hideAllStates();
|
||||
this.elements.quoteDisplay.classList.remove('hide');
|
||||
this.elements.refreshBtn.disabled = false;
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
// 显示错误信息
|
||||
showError(message) {
|
||||
this.hideAllStates();
|
||||
this.elements.errorMessage.classList.add('show');
|
||||
this.elements.errorText.textContent = message;
|
||||
this.elements.refreshBtn.disabled = false;
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
// 获取一言数据
|
||||
async fetchHitokoto() {
|
||||
if (this.isLoading) return;
|
||||
|
||||
this.showLoading();
|
||||
|
||||
// 尝试所有API接口
|
||||
for (let i = 0; i < this.apiEndpoints.length; i++) {
|
||||
const endpointIndex = (this.currentEndpointIndex + i) % this.apiEndpoints.length;
|
||||
const endpoint = this.apiEndpoints[endpointIndex];
|
||||
|
||||
try {
|
||||
const result = await this.tryFetchFromEndpoint(endpoint);
|
||||
if (result.success) {
|
||||
this.currentEndpointIndex = endpointIndex;
|
||||
this.displayHitokoto(result.data);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`接口 ${endpoint} 请求失败:`, error.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有接口都失败
|
||||
this.showError('所有接口都无法访问,请检查网络连接或稍后重试');
|
||||
}
|
||||
|
||||
// 尝试从指定接口获取数据
|
||||
async tryFetchFromEndpoint(endpoint) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
|
||||
|
||||
try {
|
||||
const response = await fetch(`${endpoint}/v2/hitokoto?encoding=text`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 验证返回数据格式
|
||||
if (data.code === 200 && data.data && data.data.hitokoto) {
|
||||
return {
|
||||
success: true,
|
||||
data: data.data
|
||||
};
|
||||
} else {
|
||||
throw new Error('返回数据格式不正确');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('请求超时');
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示一言内容
|
||||
displayHitokoto(data) {
|
||||
// 更新一言文本
|
||||
this.elements.quoteText.textContent = data.hitokoto;
|
||||
|
||||
// 更新序号信息
|
||||
if (data.index) {
|
||||
this.elements.quoteIndex.textContent = `第 ${data.index} 条`;
|
||||
} else {
|
||||
this.elements.quoteIndex.textContent = '';
|
||||
}
|
||||
|
||||
// 添加淡入动画效果
|
||||
this.elements.quoteText.style.opacity = '0';
|
||||
this.elements.quoteIndex.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
this.elements.quoteText.style.transition = 'opacity 0.5s ease';
|
||||
this.elements.quoteIndex.style.transition = 'opacity 0.5s ease';
|
||||
this.elements.quoteText.style.opacity = '1';
|
||||
this.elements.quoteIndex.style.opacity = '1';
|
||||
}, 100);
|
||||
|
||||
this.showQuoteDisplay();
|
||||
|
||||
// 控制台输出调试信息
|
||||
console.log('一言获取成功:', {
|
||||
content: data.hitokoto,
|
||||
index: data.index,
|
||||
endpoint: this.apiEndpoints[this.currentEndpointIndex]
|
||||
});
|
||||
}
|
||||
|
||||
// 获取随机接口(用于负载均衡)
|
||||
getRandomEndpoint() {
|
||||
const randomIndex = Math.floor(Math.random() * this.apiEndpoints.length);
|
||||
return this.apiEndpoints[randomIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化应用
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const app = new HitokotoApp();
|
||||
|
||||
// 添加全局错误处理
|
||||
window.addEventListener('error', (event) => {
|
||||
console.error('页面发生错误:', event.error);
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
console.error('未处理的Promise拒绝:', event.reason);
|
||||
});
|
||||
|
||||
// 页面可见性变化时的处理
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden && !app.isLoading) {
|
||||
// 页面重新可见时,可以选择刷新内容
|
||||
console.log('页面重新可见');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('随机一言应用初始化完成');
|
||||
});
|
||||
|
||||
// 导出应用类(如果需要在其他地方使用)
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = HitokotoApp;
|
||||
}
|
||||
7
frontend/react-app/public/60sapi/娱乐消遣/随机一言/接口集合.json
Normal file
7
frontend/react-app/public/60sapi/娱乐消遣/随机一言/接口集合.json
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
]
|
||||
8
frontend/react-app/public/60sapi/娱乐消遣/随机一言/返回接口.json
Normal file
8
frontend/react-app/public/60sapi/娱乐消遣/随机一言/返回接口.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||
"data": {
|
||||
"index": 2862,
|
||||
"hitokoto": "你带上罪恶之冠,即使背负上所有罪恶和孤独,绝不让你受伤"
|
||||
}
|
||||
}
|
||||
251
frontend/react-app/public/60sapi/娱乐消遣/随机唱歌音频/css/style.css
Normal file
251
frontend/react-app/public/60sapi/娱乐消遣/随机唱歌音频/css/style.css
Normal file
@@ -0,0 +1,251 @@
|
||||
/* 随机唱歌音频 - 淡绿色清新风格样式 */
|
||||
|
||||
/* 重置样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 50%, #ffd3a5 100%);
|
||||
min-height: 100vh;
|
||||
color: #2d5016;
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 20px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
color: #2d5016;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #5a7c65;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 用户卡片 */
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 16px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 3px solid rgba(129, 199, 132, 0.5);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
color: #2d5016;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: #5a7c65;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 歌曲信息 */
|
||||
.song-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 16px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.song-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
color: #1b5e20;
|
||||
}
|
||||
|
||||
.song-meta {
|
||||
color: #5a7c65;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 歌词 */
|
||||
.lyrics {
|
||||
background: rgba(129, 199, 132, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.lyrics p {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* 音频播放器卡片 */
|
||||
.audio-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 16px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.audio-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 18px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 4px 12px rgba(129, 199, 132, 0.35);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 18px rgba(129, 199, 132, 0.45);
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #5a7c65;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 加载与错误 */
|
||||
.loading, .error {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 4px solid #e8f5e8;
|
||||
border-top: 4px solid #81c784;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 18px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 平板端适配 */
|
||||
@media (max-width: 1024px) and (min-width: 768px) {
|
||||
.container { padding: 16px; }
|
||||
.header h1 { font-size: 1.8rem; }
|
||||
}
|
||||
|
||||
/* 手机端优先优化 */
|
||||
@media (max-width: 767px) {
|
||||
.container { padding: 12px; }
|
||||
.header { padding: 18px; }
|
||||
.header h1 { font-size: 1.6rem; gap: 8px; }
|
||||
|
||||
.user-card {
|
||||
padding: 16px;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.song-card, .audio-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.lyrics {
|
||||
max-height: 180px;
|
||||
text-align: left;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.audio-actions {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: center;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user