60sapi接口搭建完毕,数据库连接测试成功,登录注册部分简单完成
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -205,3 +205,5 @@ cython_debug/
|
|||||||
marimo/_static/
|
marimo/_static/
|
||||||
marimo/_lsp/
|
marimo/_lsp/
|
||||||
__marimo__/
|
__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,聚合了许多神奇有趣的功能,帮助用户一键化解决问题
|
神奇万事通,一个支持Windows,Android和web的app,聚合了许多神奇有趣的功能,帮助用户一键化解决问题
|
||||||
前端使用React框架,后端使用Python的Flask框架
|
前端使用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,4 +1,7 @@
|
|||||||
// API接口列表
|
// 本地后端API接口
|
||||||
|
const LOCAL_API_BASE = 'http://localhost:5000/api/60s';
|
||||||
|
|
||||||
|
// API接口列表(备用)
|
||||||
const API_ENDPOINTS = [
|
const API_ENDPOINTS = [
|
||||||
"https://60s-cf.viki.moe",
|
"https://60s-cf.viki.moe",
|
||||||
"https://60s.viki.moe",
|
"https://60s.viki.moe",
|
||||||
@@ -9,6 +12,7 @@ const API_ENDPOINTS = [
|
|||||||
|
|
||||||
// 当前使用的API索引
|
// 当前使用的API索引
|
||||||
let currentApiIndex = 0;
|
let currentApiIndex = 0;
|
||||||
|
let useLocalApi = true;
|
||||||
|
|
||||||
// DOM元素
|
// DOM元素
|
||||||
const loadingElement = document.getElementById('loading');
|
const loadingElement = document.getElementById('loading');
|
||||||
@@ -46,6 +50,30 @@ async function loadHotList() {
|
|||||||
|
|
||||||
// 获取数据
|
// 获取数据
|
||||||
async function fetchData() {
|
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++) {
|
for (let i = 0; i < API_ENDPOINTS.length; i++) {
|
||||||
const apiUrl = API_ENDPOINTS[currentApiIndex];
|
const apiUrl = API_ENDPOINTS[currentApiIndex];
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
5.返回接口.json储存了网页api返回的数据格式
|
5.返回接口.json储存了网页api返回的数据格式
|
||||||
6.严格按照用户要求执行,不得随意添加什么注解,如“以下数据来自...”
|
6.严格按照用户要求执行,不得随意添加什么注解,如“以下数据来自...”
|
||||||
7.接口集合.json保存了所有已知的后端API接口,一个访问不了尝试自动切换另一个
|
7.接口集合.json保存了所有已知的后端API接口,一个访问不了尝试自动切换另一个
|
||||||
8.在css中有关背景的css单独一个css文件,方便我直接迁移
|
8.在css中有关背景的css单独一个css文件,方便我直接迁移
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
frontend/react-app/public/60sapi/娱乐消遣/随机唱歌音频/index.html
Normal file
67
frontend/react-app/public/60sapi/娱乐消遣/随机唱歌音频/index.html
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<title>随机唱歌音频 - 60s API 集合</title>
|
||||||
|
<meta name="description" content="随机唱歌音频,数据源自 60s.viki.moe,提供用户信息、歌曲信息、歌词与音频播放。" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="./css/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>
|
||||||
|
🎵 随机唱歌音频
|
||||||
|
</h1>
|
||||||
|
<p>数据来自官方/权威源头,以确保稳定与实时 · 支持本地数据回退</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 加载与错误状态 -->
|
||||||
|
<section id="loading" class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>正在加载中,请稍候…</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="error" class="error" style="display: none;">
|
||||||
|
<p>获取数据失败,请稍后重试</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<main id="content" style="display: none;" class="fade-in">
|
||||||
|
<!-- 用户信息 -->
|
||||||
|
<div class="user-card">
|
||||||
|
<img id="avatar" class="avatar" src="" alt="用户头像" />
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="nickname" id="nickname">-</div>
|
||||||
|
<div class="meta">性别:<span id="gender">-</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 歌曲信息 -->
|
||||||
|
<div class="song-card">
|
||||||
|
<div class="song-title" id="song-title">-</div>
|
||||||
|
<div class="song-meta" id="song-meta">-</div>
|
||||||
|
<div class="lyrics" id="lyrics"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 音频播放 -->
|
||||||
|
<div class="audio-card">
|
||||||
|
<audio id="audio" controls style="width: 100%;"></audio>
|
||||||
|
<div class="audio-actions">
|
||||||
|
<button class="btn" id="refresh-btn">换一首</button>
|
||||||
|
<div class="info">
|
||||||
|
❤ 喜欢:<span id="like-count">-</span>
|
||||||
|
· ⏱ 时长:<span id="duration">--:--</span>
|
||||||
|
· 🗓 发布:<span id="publish-time">-</span>
|
||||||
|
· 🔗 <a id="link" href="#" class="btn" style="padding: 6px 10px; border-radius: 8px; background: #81c784;">查看原帖</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="./js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
252
frontend/react-app/public/60sapi/娱乐消遣/随机唱歌音频/js/script.js
vendored
Normal file
252
frontend/react-app/public/60sapi/娱乐消遣/随机唱歌音频/js/script.js
vendored
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
// 随机唱歌音频 页面脚本
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const API = {
|
||||||
|
endpoints: [],
|
||||||
|
currentIndex: 0,
|
||||||
|
params: {
|
||||||
|
encoding: 'json'
|
||||||
|
},
|
||||||
|
localFallback: '返回接口.json',
|
||||||
|
// 初始化API接口列表
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('./接口集合.json');
|
||||||
|
const endpoints = await res.json();
|
||||||
|
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/changya`);
|
||||||
|
} catch (e) {
|
||||||
|
// 如果无法加载接口集合,使用默认接口
|
||||||
|
this.endpoints = ['https://60s.viki.moe/v2/changya'];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 获取当前接口URL
|
||||||
|
getCurrentUrl() {
|
||||||
|
if (this.endpoints.length === 0) return null;
|
||||||
|
const url = new URL(this.endpoints[this.currentIndex]);
|
||||||
|
Object.entries(this.params).forEach(([k, v]) => url.searchParams.append(k, v));
|
||||||
|
return url.toString();
|
||||||
|
},
|
||||||
|
// 切换到下一个接口
|
||||||
|
switchToNext() {
|
||||||
|
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length;
|
||||||
|
return this.currentIndex < this.endpoints.length;
|
||||||
|
},
|
||||||
|
// 重置到第一个接口
|
||||||
|
reset() {
|
||||||
|
this.currentIndex = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM 元素引用
|
||||||
|
const els = {
|
||||||
|
loading: null,
|
||||||
|
error: null,
|
||||||
|
container: null,
|
||||||
|
avatar: null,
|
||||||
|
nickname: null,
|
||||||
|
gender: null,
|
||||||
|
songTitle: null,
|
||||||
|
songMeta: null,
|
||||||
|
lyrics: null,
|
||||||
|
audio: null,
|
||||||
|
likeCount: null,
|
||||||
|
publishTime: null,
|
||||||
|
link: null,
|
||||||
|
refreshBtn: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function initDom() {
|
||||||
|
els.loading = document.getElementById('loading');
|
||||||
|
els.error = document.getElementById('error');
|
||||||
|
els.container = document.getElementById('content');
|
||||||
|
|
||||||
|
els.avatar = document.getElementById('avatar');
|
||||||
|
els.nickname = document.getElementById('nickname');
|
||||||
|
els.gender = document.getElementById('gender');
|
||||||
|
els.songTitle = document.getElementById('song-title');
|
||||||
|
els.songMeta = document.getElementById('song-meta');
|
||||||
|
els.lyrics = document.getElementById('lyrics');
|
||||||
|
|
||||||
|
els.audio = document.getElementById('audio');
|
||||||
|
els.likeCount = document.getElementById('like-count');
|
||||||
|
els.publishTime = document.getElementById('publish-time');
|
||||||
|
els.link = document.getElementById('link');
|
||||||
|
els.refreshBtn = document.getElementById('refresh-btn');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoading() {
|
||||||
|
els.loading.style.display = 'block';
|
||||||
|
els.error.style.display = 'none';
|
||||||
|
els.container.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(msg) {
|
||||||
|
els.loading.style.display = 'none';
|
||||||
|
els.error.style.display = 'block';
|
||||||
|
els.container.style.display = 'none';
|
||||||
|
els.error.querySelector('p').textContent = msg || '获取数据失败,请稍后重试';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showContent() {
|
||||||
|
els.loading.style.display = 'none';
|
||||||
|
els.error.style.display = 'none';
|
||||||
|
els.container.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(ms) {
|
||||||
|
if (!ms && ms !== 0) return '';
|
||||||
|
const totalSeconds = Math.floor(ms / 1000);
|
||||||
|
const m = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
|
||||||
|
const s = (totalSeconds % 60).toString().padStart(2, '0');
|
||||||
|
return `${m}:${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeText(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text == null ? '' : String(text);
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchFromAPI() {
|
||||||
|
// 初始化API接口列表
|
||||||
|
await API.init();
|
||||||
|
|
||||||
|
// 重置API索引到第一个接口
|
||||||
|
API.reset();
|
||||||
|
|
||||||
|
// 尝试所有API接口
|
||||||
|
for (let i = 0; i < API.endpoints.length; i++) {
|
||||||
|
try {
|
||||||
|
const url = API.getCurrentUrl();
|
||||||
|
console.log(`尝试接口 ${i + 1}/${API.endpoints.length}: ${url}`);
|
||||||
|
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
cache: 'no-store',
|
||||||
|
timeout: 10000 // 10秒超时
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
if (data && data.code === 200) {
|
||||||
|
console.log(`接口 ${i + 1} 请求成功`);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(data && data.message ? data.message : '接口返回异常');
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`接口 ${i + 1} 失败:`, e.message);
|
||||||
|
|
||||||
|
// 如果不是最后一个接口,切换到下一个
|
||||||
|
if (i < API.endpoints.length - 1) {
|
||||||
|
API.switchToNext();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有接口都失败了
|
||||||
|
console.warn('所有远程接口都失败,尝试本地数据');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchFromLocal() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(API.localFallback + `?t=${Date.now()}`);
|
||||||
|
if (!resp.ok) throw new Error(`本地文件HTTP ${resp.status}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('读取本地返回接口.json失败:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(data) {
|
||||||
|
const d = data?.data || {};
|
||||||
|
const user = d.user || {};
|
||||||
|
const song = d.song || {};
|
||||||
|
const audio = d.audio || {};
|
||||||
|
|
||||||
|
// 用户信息
|
||||||
|
els.avatar.src = user.avatar_url || '';
|
||||||
|
els.avatar.alt = (user.nickname || '用户') + ' 头像';
|
||||||
|
els.nickname.textContent = user.nickname || '未知用户';
|
||||||
|
els.gender.textContent = user.gender === 'female' ? '女' : user.gender === 'male' ? '男' : '未知';
|
||||||
|
|
||||||
|
// 歌曲信息
|
||||||
|
els.songTitle.textContent = song.name || '未知歌曲';
|
||||||
|
els.songMeta.textContent = song.singer ? `演唱:${song.singer}` : '';
|
||||||
|
|
||||||
|
els.lyrics.innerHTML = '';
|
||||||
|
if (Array.isArray(song.lyrics)) {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
song.lyrics.forEach(line => {
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.innerHTML = safeText(line);
|
||||||
|
frag.appendChild(p);
|
||||||
|
});
|
||||||
|
els.lyrics.appendChild(frag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 音频
|
||||||
|
els.audio.src = audio.url || '';
|
||||||
|
els.audio.preload = 'none';
|
||||||
|
|
||||||
|
// 其他信息
|
||||||
|
els.likeCount.textContent = typeof audio.like_count === 'number' ? audio.like_count : '-';
|
||||||
|
const publish = audio.publish || (audio.publish_at ? new Date(audio.publish_at).toLocaleString() : '');
|
||||||
|
els.publishTime.textContent = publish;
|
||||||
|
els.link.href = audio.link || '#';
|
||||||
|
els.link.target = '_blank';
|
||||||
|
|
||||||
|
// 时长信息
|
||||||
|
const durationEl = document.getElementById('duration');
|
||||||
|
durationEl.textContent = formatDuration(audio.duration);
|
||||||
|
|
||||||
|
showContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
showLoading();
|
||||||
|
try {
|
||||||
|
// 先尝试远程API
|
||||||
|
const data = await fetchFromAPI();
|
||||||
|
if (data) {
|
||||||
|
render(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 远程API失败,尝试本地数据
|
||||||
|
const localData = await fetchFromLocal();
|
||||||
|
if (localData) {
|
||||||
|
render(localData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 都失败了
|
||||||
|
showError('获取数据失败,请稍后重试');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载数据时发生错误:', e);
|
||||||
|
showError('获取数据失败,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
if (els.refreshBtn) {
|
||||||
|
els.refreshBtn.addEventListener('click', load);
|
||||||
|
}
|
||||||
|
// 快捷键 Ctrl+R 刷新(不拦截浏览器默认刷新)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initDom();
|
||||||
|
bindEvents();
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
})();
|
||||||
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"
|
||||||
|
]
|
||||||
32
frontend/react-app/public/60sapi/娱乐消遣/随机唱歌音频/返回接口.json
Normal file
32
frontend/react-app/public/60sapi/娱乐消遣/随机唱歌音频/返回接口.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||||
|
"data": {
|
||||||
|
"user": {
|
||||||
|
"nickname": "𝑮𝑺_迷鹿_",
|
||||||
|
"gender": "female",
|
||||||
|
"avatar_url": "http://img-cdn.api.singduck.cn/user-img/6afbebcfae6144478c150d0c1d0d5899.jpg"
|
||||||
|
},
|
||||||
|
"song": {
|
||||||
|
"name": "恶作剧",
|
||||||
|
"singer": "王蓝茵",
|
||||||
|
"lyrics": [
|
||||||
|
"我想我会开始想念你",
|
||||||
|
"可是我刚刚才遇见了你",
|
||||||
|
"我怀疑这奇遇只是个恶作剧",
|
||||||
|
"我想我已慢慢喜欢你",
|
||||||
|
"因为我拥有爱情的勇气",
|
||||||
|
"我任性投入你给的恶作剧",
|
||||||
|
"你给的恶作剧"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"url": "http://audio-cdn.api.singduck.cn/ugc/220929_965696173_b822a290c553.wav?auth_key=1755845643-0-0-4029539b73e17337dcac49cc4e0ecfcc",
|
||||||
|
"duration": 35050,
|
||||||
|
"like_count": 955,
|
||||||
|
"link": "https://m.api.singduck.cn/user-piece/toGZlBfZbukck2sHb",
|
||||||
|
"publish": "2022/09/29 18:33:51",
|
||||||
|
"publish_at": 1664447631000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
/* Epic Games 免费游戏 - 淡绿色清新风格样式 */
|
||||||
|
|
||||||
|
/* 重置样式 */
|
||||||
|
* {
|
||||||
|
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: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部 */
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
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: 2.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计信息 */
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 15px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1b5e20;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 游戏网格 */
|
||||||
|
.games-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 游戏卡片 */
|
||||||
|
.game-card {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 18px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 6px 25px rgba(45, 80, 22, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 35px rgba(45, 80, 22, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-info {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1b5e20;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.3;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-description {
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-price {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-price {
|
||||||
|
color: #81c784;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.free-price {
|
||||||
|
color: #2e7d32;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-seller {
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-dates {
|
||||||
|
background: rgba(129, 199, 132, 0.1);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
|
||||||
|
.free-period {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(129, 199, 132, 0.35);
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 18px rgba(129, 199, 132, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: linear-gradient(135deg, #a5d6a7 0%, #81c784 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态标签 */
|
||||||
|
.status-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-free {
|
||||||
|
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-upcoming {
|
||||||
|
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载与错误 */
|
||||||
|
.loading, .error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #e8f5e8;
|
||||||
|
border-top: 4px solid #81c784;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画 */
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.6s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板端适配 */
|
||||||
|
@media (max-width: 1024px) and (min-width: 768px) {
|
||||||
|
.container { padding: 16px; }
|
||||||
|
.header h1 { font-size: 2rem; }
|
||||||
|
.games-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机端优化 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.container { padding: 12px; }
|
||||||
|
.header { padding: 18px; }
|
||||||
|
.header h1 { font-size: 1.8rem; gap: 8px; }
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
padding: 10px 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.stat-number { font-size: 1.4rem; }
|
||||||
|
.stat-label { font-size: 0.75rem; }
|
||||||
|
|
||||||
|
.games-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card { margin: 0 4px; }
|
||||||
|
.game-cover { height: 160px; }
|
||||||
|
.game-info { padding: 14px; }
|
||||||
|
|
||||||
|
.game-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小屏手机优化 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-meta {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 高分辨率显示器优化 */
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.games-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<title>Epic Games 免费游戏 - 60s API 集合</title>
|
||||||
|
<meta name="description" content="Epic Games 免费游戏列表,数据源自 60s.viki.moe,提供当前免费和即将免费的游戏信息。" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="./css/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>
|
||||||
|
🎮 Epic Games 免费游戏
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 加载与错误状态 -->
|
||||||
|
<section id="loading" class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>正在加载游戏数据,请稍候…</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="error" class="error" style="display: none;">
|
||||||
|
<p>获取数据失败,请稍后重试</p>
|
||||||
|
<button id="refresh-btn" class="btn" style="margin-top: 15px;">重新加载</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<main id="content" style="display: none;" class="fade-in">
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number" id="total-games">0</div>
|
||||||
|
<div class="stat-label">总游戏数</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number" id="free-now">0</div>
|
||||||
|
<div class="stat-label">当前免费</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number" id="upcoming">0</div>
|
||||||
|
<div class="stat-label">即将免费</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 游戏列表 -->
|
||||||
|
<div class="games-grid" id="games-grid">
|
||||||
|
<!-- 游戏卡片将通过 JavaScript 动态生成 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 刷新按钮 -->
|
||||||
|
<div style="text-align: center; margin-top: 30px;">
|
||||||
|
<button id="refresh-btn" class="btn btn-secondary">🔄 刷新数据</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="./js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
266
frontend/react-app/public/60sapi/实用功能/EpicGames免费游戏/js/script.js
vendored
Normal file
266
frontend/react-app/public/60sapi/实用功能/EpicGames免费游戏/js/script.js
vendored
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
// Epic Games 免费游戏 页面脚本
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const API = {
|
||||||
|
endpoints: [],
|
||||||
|
currentIndex: 0,
|
||||||
|
// 初始化API接口列表
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('./接口集合.json');
|
||||||
|
const endpoints = await res.json();
|
||||||
|
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/epic`);
|
||||||
|
} catch (e) {
|
||||||
|
// 如果无法加载接口集合,使用默认接口
|
||||||
|
this.endpoints = ['https://60s-api.viki.moe/v2/epic'];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 获取当前接口URL
|
||||||
|
getCurrentUrl(encoding) {
|
||||||
|
if (this.endpoints.length === 0) return null;
|
||||||
|
const url = new URL(this.endpoints[this.currentIndex]);
|
||||||
|
if (encoding) url.searchParams.set('encoding', encoding);
|
||||||
|
return url.toString();
|
||||||
|
},
|
||||||
|
// 切换到下一个接口
|
||||||
|
switchToNext() {
|
||||||
|
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length;
|
||||||
|
return this.currentIndex < this.endpoints.length;
|
||||||
|
},
|
||||||
|
// 重置到第一个接口
|
||||||
|
reset() {
|
||||||
|
this.currentIndex = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM 元素引用
|
||||||
|
const els = {
|
||||||
|
loading: null,
|
||||||
|
error: null,
|
||||||
|
container: null,
|
||||||
|
gamesGrid: null,
|
||||||
|
totalGames: null,
|
||||||
|
freeNow: null,
|
||||||
|
upcoming: null,
|
||||||
|
refreshBtn: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function initDom() {
|
||||||
|
els.loading = document.getElementById('loading');
|
||||||
|
els.error = document.getElementById('error');
|
||||||
|
els.container = document.getElementById('content');
|
||||||
|
els.gamesGrid = document.getElementById('games-grid');
|
||||||
|
els.totalGames = document.getElementById('total-games');
|
||||||
|
els.freeNow = document.getElementById('free-now');
|
||||||
|
els.upcoming = document.getElementById('upcoming');
|
||||||
|
els.refreshBtn = document.getElementById('refresh-btn');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoading() {
|
||||||
|
els.loading.style.display = 'block';
|
||||||
|
els.error.style.display = 'none';
|
||||||
|
els.container.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(msg) {
|
||||||
|
els.loading.style.display = 'none';
|
||||||
|
els.error.style.display = 'block';
|
||||||
|
els.container.style.display = 'none';
|
||||||
|
els.error.querySelector('p').textContent = msg || '获取数据失败,请稍后重试';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showContent() {
|
||||||
|
els.loading.style.display = 'none';
|
||||||
|
els.error.style.display = 'none';
|
||||||
|
els.container.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeText(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text == null ? '' : String(text);
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
try {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchData(preferLocal = false) {
|
||||||
|
if (preferLocal) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('./返回接口.json', { cache: 'no-store' });
|
||||||
|
const json = await res.json();
|
||||||
|
return json;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('本地数据加载失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置API索引到第一个接口
|
||||||
|
API.reset();
|
||||||
|
|
||||||
|
// 尝试所有API接口
|
||||||
|
for (let i = 0; i < API.endpoints.length; i++) {
|
||||||
|
try {
|
||||||
|
const url = API.getCurrentUrl();
|
||||||
|
console.log(`尝试接口 ${i + 1}/${API.endpoints.length}: ${url}`);
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
cache: 'no-store',
|
||||||
|
timeout: 10000 // 10秒超时
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json && json.code === 200) {
|
||||||
|
console.log(`接口 ${i + 1} 请求成功`);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(json && json.message ? json.message : '接口返回异常');
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`接口 ${i + 1} 失败:`, e.message);
|
||||||
|
|
||||||
|
// 如果不是最后一个接口,切换到下一个
|
||||||
|
if (i < API.endpoints.length - 1) {
|
||||||
|
API.switchToNext();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有接口都失败了,尝试本地数据
|
||||||
|
console.warn('所有远程接口都失败,尝试本地数据');
|
||||||
|
try {
|
||||||
|
const res = await fetch('./返回接口.json', { cache: 'no-store' });
|
||||||
|
const json = await res.json();
|
||||||
|
return json;
|
||||||
|
} catch (e2) {
|
||||||
|
throw new Error('所有接口和本地数据都无法访问');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGameCard(game) {
|
||||||
|
const isFree = game.is_free_now;
|
||||||
|
const statusClass = isFree ? 'status-free' : 'status-upcoming';
|
||||||
|
const statusText = isFree ? '限时免费' : '即将免费';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="game-card fade-in">
|
||||||
|
<div class="status-badge ${statusClass}">${statusText}</div>
|
||||||
|
<img class="game-cover" src="${safeText(game.cover)}" alt="${safeText(game.title)} 封面" loading="lazy" />
|
||||||
|
<div class="game-info">
|
||||||
|
<h3 class="game-title">${safeText(game.title)}</h3>
|
||||||
|
<p class="game-description">${safeText(game.description)}</p>
|
||||||
|
|
||||||
|
<div class="game-meta">
|
||||||
|
<div class="game-price">
|
||||||
|
<span class="original-price">${safeText(game.original_price_desc)}</span>
|
||||||
|
<span class="free-price">免费</span>
|
||||||
|
</div>
|
||||||
|
<div class="game-seller">${safeText(game.seller)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="game-dates">
|
||||||
|
<div class="free-period">
|
||||||
|
<span>开始:${formatDate(game.free_start)}</span>
|
||||||
|
<span>结束:${formatDate(game.free_end)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="game-actions">
|
||||||
|
<a href="${safeText(game.link)}" target="_blank" class="btn">
|
||||||
|
${isFree ? '立即领取' : '查看详情'}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStats(games) {
|
||||||
|
const total = games.length;
|
||||||
|
const freeNow = games.filter(game => game.is_free_now).length;
|
||||||
|
const upcoming = total - freeNow;
|
||||||
|
|
||||||
|
els.totalGames.textContent = total;
|
||||||
|
els.freeNow.textContent = freeNow;
|
||||||
|
els.upcoming.textContent = upcoming;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGames(games) {
|
||||||
|
if (!Array.isArray(games) || games.length === 0) {
|
||||||
|
els.gamesGrid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: #5a7c65;">暂无游戏数据</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按状态排序:免费的在前
|
||||||
|
const sortedGames = [...games].sort((a, b) => {
|
||||||
|
if (a.is_free_now && !b.is_free_now) return -1;
|
||||||
|
if (!a.is_free_now && b.is_free_now) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = sortedGames.map(game => createGameCard(game)).join('');
|
||||||
|
els.gamesGrid.innerHTML = html;
|
||||||
|
|
||||||
|
updateStats(games);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(data) {
|
||||||
|
const games = data?.data || [];
|
||||||
|
renderGames(games);
|
||||||
|
showContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
// 初始化API接口列表
|
||||||
|
await API.init();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchData(false);
|
||||||
|
render(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('数据获取失败:', e);
|
||||||
|
showError(e.message || '获取数据失败,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
if (els.refreshBtn) {
|
||||||
|
els.refreshBtn.addEventListener('click', load);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快捷键 Ctrl+R 刷新(不拦截浏览器默认刷新)
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.ctrlKey && e.key === 'r' && !e.defaultPrevented) {
|
||||||
|
// 不阻止默认行为,让浏览器正常刷新
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initDom();
|
||||||
|
bindEvents();
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -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,66 @@
|
|||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "9aa227e2ba294bb1a95c95fde892eb31",
|
||||||
|
"title": "《Totally Reliable Delivery Service》 Standard Edition",
|
||||||
|
"cover": "https://cdn1.epicgames.com/52b90f9a982a404781b189f6a7903226/offer/EGS_TotallyReliableDeliveryService_WereFiveGames_S1-2560x1440-47e6e9562d62705a75ea7b7096d0b8dc.jpg",
|
||||||
|
"original_price": 52,
|
||||||
|
"original_price_desc": "¥52.00",
|
||||||
|
"description": "穿好护腰护具,发动货车,送货的时间到啦!在一个高度互动的沙盒世界中,与最多三位好友一起随意地完成送货。货物已试投,这就是我们靠谱快递(Totally Reliable Delivery Service)的品质保证!",
|
||||||
|
"seller": "Infogrames LLC",
|
||||||
|
"is_free_now": true,
|
||||||
|
"free_start": "2025/08/14 23:00:00",
|
||||||
|
"free_start_at": 1755183600000,
|
||||||
|
"free_end": "2025/08/21 23:00:00",
|
||||||
|
"free_end_at": 1755788400000,
|
||||||
|
"link": "https://store.epicgames.com/store/zh-CN/p/totally-reliable-delivery-service/home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8ea3500dc38e4f429702bf889c172d3d",
|
||||||
|
"title": "Hidden Folks",
|
||||||
|
"cover": "https://cdn1.epicgames.com/spt-assets/7bfd56b0586348dcb139945d9e59f988/hidden-folks-1b7hh.png",
|
||||||
|
"original_price": 47,
|
||||||
|
"original_price_desc": "¥47.00",
|
||||||
|
"description": "Search for hidden folks in hand-drawn, interactive, miniature landscapes. Unfurl tent flaps, cut through bushes, slam doors, and poke some crocodiles! Rooooaaaarrrr!!!!!",
|
||||||
|
"seller": "Adriaan de Jongh",
|
||||||
|
"is_free_now": true,
|
||||||
|
"free_start": "2025/08/14 23:00:00",
|
||||||
|
"free_start_at": 1755183600000,
|
||||||
|
"free_end": "2025/08/21 23:00:00",
|
||||||
|
"free_end_at": 1755788400000,
|
||||||
|
"link": "https://store.epicgames.com/store/zh-CN/p/hidden-folks-239d16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4cbb6c3704d240f19c3dd5f5cb2b0cb4",
|
||||||
|
"title": "Kamaeru",
|
||||||
|
"cover": "https://cdn1.epicgames.com/spt-assets/44313cfbb62b4df5801d0c8d541c2624/kamaeru-40asc.png",
|
||||||
|
"original_price": 62,
|
||||||
|
"original_price_desc": "¥62.00",
|
||||||
|
"description": "Foster a sanctuary for frogs and restore the biodiversity of the wetlands in Kamaeru, a cozy frog collecting game, where you take pictures of frogs, play mini-games and decorate your habitat. Hop right to it!",
|
||||||
|
"seller": "Armor Games Studios",
|
||||||
|
"is_free_now": false,
|
||||||
|
"free_start": "2025/08/21 23:00:00",
|
||||||
|
"free_start_at": 1755788400000,
|
||||||
|
"free_end": "2025/08/28 23:00:00",
|
||||||
|
"free_end_at": 1756393200000,
|
||||||
|
"link": "https://store.epicgames.com/store/zh-CN/p/kamaeru-0c301e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0d9a533f0e684cc18620a8f408e8e72c",
|
||||||
|
"title": "Strange Horticulture",
|
||||||
|
"cover": "https://cdn1.epicgames.com/spt-assets/15e8e3eba65a4763a815d6eae1d763b2/strange-horticulture-offer-2wghv.png",
|
||||||
|
"original_price": 45,
|
||||||
|
"original_price_desc": "¥45.00",
|
||||||
|
"description": "款神秘学解谜游戏,你将扮演当地植物商店的店主,寻找并识别新的植物,悠闲撸猫,与女巫团体交谈,或加入异教。收集各种强大的植物,用它们来影响故事走向,揭开昂德米尔镇的黑暗谜团。",
|
||||||
|
"seller": "Iceberg Interactive",
|
||||||
|
"is_free_now": false,
|
||||||
|
"free_start": "2025/08/21 23:00:00",
|
||||||
|
"free_start_at": 1755788400000,
|
||||||
|
"free_end": "2025/08/28 23:00:00",
|
||||||
|
"free_end_at": 1756393200000,
|
||||||
|
"link": "https://store.epicgames.com/store/zh-CN/p/strange-horticulture-360e80"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/* 玻璃拟态背景相关样式 */
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
#667eea 0%,
|
||||||
|
#764ba2 25%,
|
||||||
|
#f093fb 50%,
|
||||||
|
#f5576c 75%,
|
||||||
|
#4facfe 100%
|
||||||
|
);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradientShift 20s ease infinite;
|
||||||
|
background-attachment: fixed;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientShift {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
25% { background-position: 100% 50%; }
|
||||||
|
50% { background-position: 100% 100%; }
|
||||||
|
75% { background-position: 0% 100%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 玻璃拟态装饰层 */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 80% 80%, rgba(255, 255, 255, 0.08) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 40% 60%, rgba(255, 255, 255, 0.06) 0%, transparent 30%),
|
||||||
|
radial-gradient(circle at 60% 30%, rgba(255, 255, 255, 0.05) 0%, transparent 35%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
animation: glassFloat 25s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 毛玻璃气泡效果 */
|
||||||
|
body::after {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 10% 20%, rgba(255, 255, 255, 0.3) 2px, transparent 2px),
|
||||||
|
radial-gradient(circle at 30% 40%, rgba(255, 255, 255, 0.25) 3px, transparent 3px),
|
||||||
|
radial-gradient(circle at 50% 60%, rgba(255, 255, 255, 0.2) 1.5px, transparent 1.5px),
|
||||||
|
radial-gradient(circle at 70% 80%, rgba(255, 255, 255, 0.3) 2.5px, transparent 2.5px),
|
||||||
|
radial-gradient(circle at 90% 10%, rgba(255, 255, 255, 0.25) 2px, transparent 2px),
|
||||||
|
radial-gradient(circle at 20% 90%, rgba(255, 255, 255, 0.2) 1px, transparent 1px);
|
||||||
|
background-size: 300px 300px, 250px 250px, 400px 400px, 200px 200px, 350px 350px, 150px 150px;
|
||||||
|
animation: bubbleFloat 30s linear infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glassFloat {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0px) rotate(0deg);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-20px) rotate(2deg);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bubbleFloat {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateX(0) translateY(0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translateX(-15px) translateY(-10px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(10px) translateY(-20px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateX(-5px) translateY(-15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
1105
frontend/react-app/public/60sapi/实用功能/农历信息/css/style.css
Normal file
1105
frontend/react-app/public/60sapi/实用功能/农历信息/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
71
frontend/react-app/public/60sapi/实用功能/农历信息/index.html
Normal file
71
frontend/react-app/public/60sapi/实用功能/农历信息/index.html
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<!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/background.css">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-icon">🏮</div>
|
||||||
|
<h1 class="title">🌙 农历信息查询 📅</h1>
|
||||||
|
<p class="subtitle">传统文化 · 时光转换 · 节气查询</p>
|
||||||
|
|
||||||
|
<div class="date-selector">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="dateInput" class="input-label">
|
||||||
|
<span class="label-icon">📅</span>
|
||||||
|
选择日期
|
||||||
|
</label>
|
||||||
|
<input type="date" id="dateInput" class="date-input" />
|
||||||
|
</div>
|
||||||
|
<button id="queryBtn" class="query-btn">
|
||||||
|
<span class="btn-icon">🔍</span>
|
||||||
|
查询农历
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="update-time">
|
||||||
|
<span class="time-icon">⏰</span>
|
||||||
|
<span id="updateTime">等待查询...</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="loading" id="loading" style="display: none;">
|
||||||
|
<div class="loading-content">
|
||||||
|
<div class="glass-spinner"></div>
|
||||||
|
<div class="loading-text">
|
||||||
|
<span class="loading-emoji">🔮</span>
|
||||||
|
<p>正在查询农历信息...</p>
|
||||||
|
<div class="loading-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lunar-info" id="lunarInfo" style="display: none;">
|
||||||
|
<!-- 农历信息将动态生成 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-message" id="errorMessage" style="display: none;">
|
||||||
|
<div class="error-content">
|
||||||
|
<div class="error-icon">😔</div>
|
||||||
|
<h3>查询失败了</h3>
|
||||||
|
<p>无法获取农历信息,请稍后重试</p>
|
||||||
|
<button onclick="queryLunarInfo()" class="retry-btn">
|
||||||
|
<span>🔄</span>
|
||||||
|
重新查询
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
485
frontend/react-app/public/60sapi/实用功能/农历信息/js/script.js
vendored
Normal file
485
frontend/react-app/public/60sapi/实用功能/农历信息/js/script.js
vendored
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
// API接口列表
|
||||||
|
const API_ENDPOINTS = [
|
||||||
|
"https://60s-cf.viki.moe",
|
||||||
|
"https://60s.viki.moe",
|
||||||
|
"https://60s.b23.run",
|
||||||
|
"https://60s.114128.xyz",
|
||||||
|
"https://60s-cf.114128.xyz"
|
||||||
|
];
|
||||||
|
|
||||||
|
// 当前使用的API索引
|
||||||
|
let currentApiIndex = 0;
|
||||||
|
|
||||||
|
// DOM元素
|
||||||
|
const loadingElement = document.getElementById('loading');
|
||||||
|
const lunarInfoElement = document.getElementById('lunarInfo');
|
||||||
|
const errorMessageElement = document.getElementById('errorMessage');
|
||||||
|
const updateTimeElement = document.getElementById('updateTime');
|
||||||
|
const dateInput = document.getElementById('dateInput');
|
||||||
|
const queryBtn = document.getElementById('queryBtn');
|
||||||
|
|
||||||
|
// 页面加载完成后初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initializePage();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化页面
|
||||||
|
function initializePage() {
|
||||||
|
// 设置默认日期为今天
|
||||||
|
const today = new Date();
|
||||||
|
const dateString = today.toISOString().split('T')[0];
|
||||||
|
dateInput.value = dateString;
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
queryBtn.addEventListener('click', queryLunarInfo);
|
||||||
|
dateInput.addEventListener('change', queryLunarInfo);
|
||||||
|
|
||||||
|
// 自动查询当天信息
|
||||||
|
queryLunarInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询农历信息
|
||||||
|
async function queryLunarInfo() {
|
||||||
|
const selectedDate = dateInput.value;
|
||||||
|
if (!selectedDate) {
|
||||||
|
showError('请选择查询日期');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
hideError();
|
||||||
|
hideLunarInfo();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchLunarData(selectedDate);
|
||||||
|
displayLunarInfo(data.data);
|
||||||
|
updateQueryTime();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('查询失败:', error);
|
||||||
|
showError('查询农历信息失败,请稍后重试');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取农历数据
|
||||||
|
async function fetchLunarData(date) {
|
||||||
|
for (let i = 0; i < API_ENDPOINTS.length; i++) {
|
||||||
|
const apiUrl = API_ENDPOINTS[currentApiIndex];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiUrl}/v2/lunar?date=${date}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.code === 200 && data.data) {
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
throw new Error('数据格式错误');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`API ${apiUrl} 请求失败:`, error);
|
||||||
|
currentApiIndex = (currentApiIndex + 1) % API_ENDPOINTS.length;
|
||||||
|
|
||||||
|
if (i === API_ENDPOINTS.length - 1) {
|
||||||
|
throw new Error('所有API接口都无法访问');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示农历信息
|
||||||
|
function displayLunarInfo(lunarData) {
|
||||||
|
lunarInfoElement.innerHTML = `
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon">📅</div>
|
||||||
|
<div class="card-title">公历信息</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🗓️</div>
|
||||||
|
<div class="item-label">公历日期</div>
|
||||||
|
<div class="item-value">${lunarData.solar.year}年${String(lunarData.solar.month).padStart(2, '0')}月${String(lunarData.solar.day).padStart(2, '0')}日</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🌍</div>
|
||||||
|
<div class="item-label">星期</div>
|
||||||
|
<div class="item-value">${lunarData.solar.week_desc}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon"><3E></div>
|
||||||
|
<div class="item-label">季节</div>
|
||||||
|
<div class="item-value">${lunarData.solar.season_name_desc}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">⭐</div>
|
||||||
|
<div class="item-label">星座</div>
|
||||||
|
<div class="item-value">${lunarData.constellation.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon">🌙</div>
|
||||||
|
<div class="card-title">农历信息</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🏮</div>
|
||||||
|
<div class="item-label">农历日期</div>
|
||||||
|
<div class="item-value">${lunarData.lunar.desc_short}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🐲</div>
|
||||||
|
<div class="item-label">生肖年</div>
|
||||||
|
<div class="item-value">${lunarData.zodiac.year}年</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">⚡</div>
|
||||||
|
<div class="item-label">天干地支</div>
|
||||||
|
<div class="item-value">${lunarData.sixty_cycle.year.name}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🌙</div>
|
||||||
|
<div class="item-label">月相</div>
|
||||||
|
<div class="item-value">${lunarData.phase.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon">🌸</div>
|
||||||
|
<div class="card-title">节气节日</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🍃</div>
|
||||||
|
<div class="item-label">当前节气</div>
|
||||||
|
<div class="item-value">${lunarData.term.stage ? lunarData.term.stage.name : '无节气'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🎊</div>
|
||||||
|
<div class="item-label">法定假日</div>
|
||||||
|
<div class="item-value">${lunarData.legal_holiday ? lunarData.legal_holiday.name : '无假日'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon"><3E></div>
|
||||||
|
<div class="item-label">传统节日</div>
|
||||||
|
<div class="item-value">${lunarData.festival.both_desc || '无特殊节日'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🔢</div>
|
||||||
|
<div class="item-label">一年第几天</div>
|
||||||
|
<div class="item-value">第${lunarData.stats.day_of_year}天</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon">⏰</div>
|
||||||
|
<div class="card-title">时辰干支</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🕐</div>
|
||||||
|
<div class="item-label">当前时辰</div>
|
||||||
|
<div class="item-value">${lunarData.lunar.hour_desc}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">⚡</div>
|
||||||
|
<div class="item-label">时辰干支</div>
|
||||||
|
<div class="item-value">${lunarData.sixty_cycle.hour.name}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🐓</div>
|
||||||
|
<div class="item-label">时辰生肖</div>
|
||||||
|
<div class="item-value">${lunarData.zodiac.hour}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🎵</div>
|
||||||
|
<div class="item-label">纳音</div>
|
||||||
|
<div class="item-value">${lunarData.nayin.hour}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon">🔮</div>
|
||||||
|
<div class="card-title">黄历宜忌</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">✅</div>
|
||||||
|
<div class="item-label">宜</div>
|
||||||
|
<div class="item-value">${formatTabooText(lunarData.taboo.day.recommends)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">❌</div>
|
||||||
|
<div class="item-label">忌</div>
|
||||||
|
<div class="item-value">${formatTabooText(lunarData.taboo.day.avoids)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🕐</div>
|
||||||
|
<div class="item-label">时辰宜</div>
|
||||||
|
<div class="item-value">${formatTabooText(lunarData.taboo.hour.recommends)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🚫</div>
|
||||||
|
<div class="item-label">时辰忌</div>
|
||||||
|
<div class="item-value">${formatTabooText(lunarData.taboo.hour.avoids)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon">🌟</div>
|
||||||
|
<div class="card-title">运势财运</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🎯</div>
|
||||||
|
<div class="item-label">今日运势</div>
|
||||||
|
<div class="item-value">${lunarData.fortune.today_luck}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">💼</div>
|
||||||
|
<div class="item-label">事业运</div>
|
||||||
|
<div class="item-value">${lunarData.fortune.career}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">💰</div>
|
||||||
|
<div class="item-label">财运</div>
|
||||||
|
<div class="item-value">${lunarData.fortune.money}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">💕</div>
|
||||||
|
<div class="item-label">感情运</div>
|
||||||
|
<div class="item-value">${lunarData.fortune.love}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon">📊</div>
|
||||||
|
<div class="card-title">年度统计</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">📈</div>
|
||||||
|
<div class="item-label">年度进度</div>
|
||||||
|
<div class="item-value">${lunarData.stats.percents_formatted.year}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">📅</div>
|
||||||
|
<div class="item-label">本月进度</div>
|
||||||
|
<div class="item-value">${lunarData.stats.percents_formatted.month}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">🗓️</div>
|
||||||
|
<div class="item-label">本周第几天</div>
|
||||||
|
<div class="item-value">第${lunarData.stats.week_of_month}周</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="item-icon">⏰</div>
|
||||||
|
<div class="item-label">今日进度</div>
|
||||||
|
<div class="item-value">${lunarData.stats.percents_formatted.day}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${generateHourlyTaboo(lunarData.taboo.hours)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
showLunarInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化宜忌文本
|
||||||
|
function formatTabooText(text) {
|
||||||
|
if (!text) return '无';
|
||||||
|
return text.replace(/\./g, '、');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成十二时辰宜忌
|
||||||
|
function generateHourlyTaboo(hours) {
|
||||||
|
if (!hours || hours.length === 0) return '';
|
||||||
|
|
||||||
|
const hourCards = hours.map(hour => `
|
||||||
|
<div class="hour-item">
|
||||||
|
<div class="hour-name">${hour.hour}</div>
|
||||||
|
<div class="hour-content">
|
||||||
|
<div class="hour-recommends">
|
||||||
|
<span class="hour-label">宜:</span>
|
||||||
|
<span class="hour-text">${formatTabooText(hour.recommends) || '无'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="hour-avoids">
|
||||||
|
<span class="hour-label">忌:</span>
|
||||||
|
<span class="hour-text">${formatTabooText(hour.avoids) || '无'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="info-card hours-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon">⏰</div>
|
||||||
|
<div class="card-title">十二时辰宜忌</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="hours-grid">
|
||||||
|
${hourCards}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新查询时间
|
||||||
|
function updateQueryTime() {
|
||||||
|
const now = new Date();
|
||||||
|
const timeStr = now.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
updateTimeElement.textContent = `查询时间: ${timeStr}`;
|
||||||
|
|
||||||
|
// 添加成功提示
|
||||||
|
showSuccessMessage('🌙 农历信息已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
function showSuccessMessage(message) {
|
||||||
|
// 移除之前的提示
|
||||||
|
const existingToast = document.querySelector('.success-toast');
|
||||||
|
if (existingToast) {
|
||||||
|
existingToast.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'success-toast';
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.2);
|
||||||
|
z-index: 1000;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9em;
|
||||||
|
animation: glassToastSlide 0.5s ease-out;
|
||||||
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
// 3秒后自动移除
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.animation = 'glassToastSlideOut 0.5s ease-in forwards';
|
||||||
|
setTimeout(() => toast.remove(), 500);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
function showLoading() {
|
||||||
|
loadingElement.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏加载状态
|
||||||
|
function hideLoading() {
|
||||||
|
loadingElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示农历信息
|
||||||
|
function showLunarInfo() {
|
||||||
|
lunarInfoElement.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏农历信息
|
||||||
|
function hideLunarInfo() {
|
||||||
|
lunarInfoElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误信息
|
||||||
|
function showError(message = '查询失败,请稍后重试') {
|
||||||
|
errorMessageElement.style.display = 'block';
|
||||||
|
const errorContent = errorMessageElement.querySelector('.error-content p');
|
||||||
|
if (errorContent) {
|
||||||
|
errorContent.textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏错误信息
|
||||||
|
function hideError() {
|
||||||
|
errorMessageElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加CSS动画到页面
|
||||||
|
if (!document.querySelector('#toast-styles')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'toast-styles';
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes glassToastSlide {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100px) scale(0.8);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glassToastSlideOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0) scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100px) scale(0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 键盘快捷键支持
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
queryLunarInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'r' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
queryLunarInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
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"
|
||||||
|
]
|
||||||
647
frontend/react-app/public/60sapi/实用功能/农历信息/返回接口.json
Normal file
647
frontend/react-app/public/60sapi/实用功能/农历信息/返回接口.json
Normal file
@@ -0,0 +1,647 @@
|
|||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||||
|
"data": {
|
||||||
|
"solar": {
|
||||||
|
"year": 2025,
|
||||||
|
"month": 9,
|
||||||
|
"day": 1,
|
||||||
|
"hour": 17,
|
||||||
|
"minute": 58,
|
||||||
|
"second": 47,
|
||||||
|
"full": "2025-09-01",
|
||||||
|
"full_with_time": "2025-09-01 17:58:47",
|
||||||
|
"week": 1,
|
||||||
|
"week_desc": "星期一",
|
||||||
|
"week_desc_short": "一",
|
||||||
|
"season": 3,
|
||||||
|
"season_desc": "三季度",
|
||||||
|
"season_desc_short": "三",
|
||||||
|
"season_name": "秋",
|
||||||
|
"season_name_desc": "秋天",
|
||||||
|
"is_leap_year": false
|
||||||
|
},
|
||||||
|
"lunar": {
|
||||||
|
"year": "乙巳",
|
||||||
|
"month": "七",
|
||||||
|
"day": "初十",
|
||||||
|
"hour": "酉",
|
||||||
|
"full_with_hour": "农历乙巳年七月初十酉时",
|
||||||
|
"desc_short": "农历乙巳年七月初十",
|
||||||
|
"year_desc": "农历乙巳年",
|
||||||
|
"month_desc": "七月",
|
||||||
|
"day_desc": "初十",
|
||||||
|
"hour_desc": "酉时",
|
||||||
|
"is_leap_month": false
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"day_of_year": 244,
|
||||||
|
"week_of_year": 36,
|
||||||
|
"week_of_month": 1,
|
||||||
|
"percents": {
|
||||||
|
"year": 0.665753424657534,
|
||||||
|
"month": 0.0333333333333333,
|
||||||
|
"week": 0.142857142857143,
|
||||||
|
"day": 0.749161909722222
|
||||||
|
},
|
||||||
|
"percents_formatted": {
|
||||||
|
"year": "66.58%",
|
||||||
|
"month": "3.33%",
|
||||||
|
"week": "14.29%",
|
||||||
|
"day": "74.92%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"term": {
|
||||||
|
"today": null,
|
||||||
|
"stage": {
|
||||||
|
"name": "处暑",
|
||||||
|
"position": 10,
|
||||||
|
"is_jie": false,
|
||||||
|
"is_qi": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zodiac": {
|
||||||
|
"year": "蛇",
|
||||||
|
"month": "鸡",
|
||||||
|
"day": "鸡",
|
||||||
|
"hour": "鸡"
|
||||||
|
},
|
||||||
|
"sixty_cycle": {
|
||||||
|
"year": {
|
||||||
|
"heaven_stem": "乙",
|
||||||
|
"earth_branch": "巳",
|
||||||
|
"name": "乙巳年",
|
||||||
|
"name_short": "乙巳"
|
||||||
|
},
|
||||||
|
"month": {
|
||||||
|
"heaven_stem": "乙",
|
||||||
|
"earth_branch": "酉",
|
||||||
|
"name": "乙酉月",
|
||||||
|
"name_short": "乙酉"
|
||||||
|
},
|
||||||
|
"day": {
|
||||||
|
"heaven_stem": "癸",
|
||||||
|
"earth_branch": "酉",
|
||||||
|
"name": "癸酉日",
|
||||||
|
"name_short": "癸酉"
|
||||||
|
},
|
||||||
|
"hour": {
|
||||||
|
"heaven_stem": "辛",
|
||||||
|
"earth_branch": "酉",
|
||||||
|
"name": "辛酉时",
|
||||||
|
"name_short": "辛酉"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"legal_holiday": null,
|
||||||
|
"festival": {
|
||||||
|
"solar": null,
|
||||||
|
"lunar": null,
|
||||||
|
"both_desc": null
|
||||||
|
},
|
||||||
|
"phase": {
|
||||||
|
"name": "宵月",
|
||||||
|
"position": 10
|
||||||
|
},
|
||||||
|
"constellation": {
|
||||||
|
"name": "处女座",
|
||||||
|
"name_short": "处女"
|
||||||
|
},
|
||||||
|
"taboo": {
|
||||||
|
"day": {
|
||||||
|
"recommends": "解除.祭祀.祈福.求嗣.修造.动土.竖柱.上梁.安床.纳畜.盖屋.合脊.起基.入殓.破土.安葬",
|
||||||
|
"avoids": "出火.嫁娶.开光.进人口.出行.词讼.开市.入宅.移徙.赴任"
|
||||||
|
},
|
||||||
|
"hour": {
|
||||||
|
"hour": "酉时",
|
||||||
|
"hour_short": "酉",
|
||||||
|
"avoids": "乘船.造桥",
|
||||||
|
"recommends": "嫁娶.出行.移徙.入宅.开市.赴任.祈福.安床.开仓.盖屋.修造.求财"
|
||||||
|
},
|
||||||
|
"hours": [
|
||||||
|
{
|
||||||
|
"hour": "酉时",
|
||||||
|
"hour_short": "酉",
|
||||||
|
"recommends": "嫁娶.出行.移徙.入宅.开市.赴任.祈福.安床.开仓.盖屋.修造.求财",
|
||||||
|
"avoids": "乘船.造桥"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "戌时",
|
||||||
|
"hour_short": "戌",
|
||||||
|
"recommends": "嫁娶.移徙.安葬.进人口.求财",
|
||||||
|
"avoids": "出行.赴任.祈福.祭祀.开光.斋醮"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "亥时",
|
||||||
|
"hour_short": "亥",
|
||||||
|
"recommends": "嫁娶.移徙.交易.入宅.开市.安葬.求嗣.求财",
|
||||||
|
"avoids": "出行.赴任.动土.祈福.祭祀.修造.开光.斋醮"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "子时",
|
||||||
|
"hour_short": "子",
|
||||||
|
"recommends": "嫁娶.交易.入宅.开市.祈福.安葬.求嗣.求财",
|
||||||
|
"avoids": "出行.移徙.赴任.词讼.修造"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "丑时",
|
||||||
|
"hour_short": "丑",
|
||||||
|
"recommends": "嫁娶.祈福.安葬.祭祀.酬神.求财",
|
||||||
|
"avoids": "出行.赴任.动土.修造"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "寅时",
|
||||||
|
"hour_short": "寅",
|
||||||
|
"recommends": "嫁娶.出行.交易.开市.赴任.祈福.安床.祭祀.求嗣.求财",
|
||||||
|
"avoids": "盖屋.入殓.上梁"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "卯时",
|
||||||
|
"hour_short": "卯",
|
||||||
|
"recommends": "嫁娶.交易.入宅.开市.祈福.安床.安葬.求嗣.求财",
|
||||||
|
"avoids": "出行.赴任.修造"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "辰时",
|
||||||
|
"hour_short": "辰",
|
||||||
|
"recommends": "",
|
||||||
|
"avoids": "诸事不宜"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "巳时",
|
||||||
|
"hour_short": "巳",
|
||||||
|
"recommends": "嫁娶.出行.移徙.入宅.开市.祈福.安床.盖屋.祭祀.作灶",
|
||||||
|
"avoids": "安葬.修造.开光"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "午时",
|
||||||
|
"hour_short": "午",
|
||||||
|
"recommends": "嫁娶.出行.交易.开市.祈福.安床.求嗣.求财",
|
||||||
|
"avoids": "赴任.动土.词讼.修造"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "未时",
|
||||||
|
"hour_short": "未",
|
||||||
|
"recommends": "嫁娶.入宅.祈福.安葬.祭祀.修造.酬神.求财",
|
||||||
|
"avoids": "出行.赴任"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hour": "申时",
|
||||||
|
"hour_short": "申",
|
||||||
|
"recommends": "嫁娶.出行.开市.赴任.安葬.求财",
|
||||||
|
"avoids": "祈福.祭祀.酬神.斋醮"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"julian_day": 2460919.5,
|
||||||
|
"nayin": {
|
||||||
|
"year": "覆灯火",
|
||||||
|
"month": "泉中水",
|
||||||
|
"day": "剑锋金",
|
||||||
|
"hour": "石榴木"
|
||||||
|
},
|
||||||
|
"baizi": {
|
||||||
|
"year_baizi": "性格温和,为人正直诚信。",
|
||||||
|
"day_baizi": "性格温和,为人正直诚信。"
|
||||||
|
},
|
||||||
|
"fortune": {
|
||||||
|
"today_luck": "今日学习运好,适合进修",
|
||||||
|
"career": "领导能力突出,升职有望",
|
||||||
|
"money": "偏财运不错,可小试投资",
|
||||||
|
"love": "感情需要沟通,避免误会"
|
||||||
|
},
|
||||||
|
"constants": {
|
||||||
|
"legal_holiday_list": [
|
||||||
|
{
|
||||||
|
"name": "元旦节",
|
||||||
|
"date": "2025-01-01",
|
||||||
|
"start": "2025-01-01",
|
||||||
|
"end": "2025-01-01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "春节",
|
||||||
|
"date": "2025-01-29",
|
||||||
|
"start": "2025-01-26",
|
||||||
|
"end": "2025-02-08"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "清明节",
|
||||||
|
"date": "2025-04-04",
|
||||||
|
"start": "2025-04-04",
|
||||||
|
"end": "2025-04-06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "劳动节",
|
||||||
|
"date": "2025-05-01",
|
||||||
|
"start": "2025-04-27",
|
||||||
|
"end": "2025-05-05"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "端午节",
|
||||||
|
"date": "2025-05-31",
|
||||||
|
"start": "2025-05-31",
|
||||||
|
"end": "2025-06-02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "国庆中秋",
|
||||||
|
"date": "2025-10-01",
|
||||||
|
"start": "2025-09-28",
|
||||||
|
"end": "2025-10-11"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"phase_list": [
|
||||||
|
{
|
||||||
|
"name": "朔月",
|
||||||
|
"lunar_day": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "既朔月",
|
||||||
|
"lunar_day": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "蛾眉新月",
|
||||||
|
"lunar_day": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "蛾眉新月",
|
||||||
|
"lunar_day": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "蛾眉月",
|
||||||
|
"lunar_day": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "夕月",
|
||||||
|
"lunar_day": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "上弦月",
|
||||||
|
"lunar_day": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "上弦月",
|
||||||
|
"lunar_day": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "九夜月",
|
||||||
|
"lunar_day": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "宵月",
|
||||||
|
"lunar_day": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "宵月",
|
||||||
|
"lunar_day": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "宵月",
|
||||||
|
"lunar_day": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "渐盈凸月",
|
||||||
|
"lunar_day": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "小望月",
|
||||||
|
"lunar_day": 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "望月",
|
||||||
|
"lunar_day": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "既望月",
|
||||||
|
"lunar_day": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "立待月",
|
||||||
|
"lunar_day": 17
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "居待月",
|
||||||
|
"lunar_day": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "寝待月",
|
||||||
|
"lunar_day": 19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "更待月",
|
||||||
|
"lunar_day": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "渐亏凸月",
|
||||||
|
"lunar_day": 21
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "下弦月",
|
||||||
|
"lunar_day": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "下弦月",
|
||||||
|
"lunar_day": 23
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "有明月",
|
||||||
|
"lunar_day": 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "有明月",
|
||||||
|
"lunar_day": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "蛾眉残月",
|
||||||
|
"lunar_day": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "蛾眉残月",
|
||||||
|
"lunar_day": 27
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "残月",
|
||||||
|
"lunar_day": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "晓月",
|
||||||
|
"lunar_day": 29
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "晦月",
|
||||||
|
"lunar_day": 30
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"zodiac_list": [
|
||||||
|
"鼠",
|
||||||
|
"牛",
|
||||||
|
"虎",
|
||||||
|
"兔",
|
||||||
|
"龙",
|
||||||
|
"蛇",
|
||||||
|
"马",
|
||||||
|
"羊",
|
||||||
|
"猴",
|
||||||
|
"鸡",
|
||||||
|
"狗",
|
||||||
|
"猪"
|
||||||
|
],
|
||||||
|
"constellation_list": [
|
||||||
|
{
|
||||||
|
"name": "白羊",
|
||||||
|
"desc": "白羊座",
|
||||||
|
"start": "3月21日",
|
||||||
|
"end": "4月19日",
|
||||||
|
"range": "3月21日~4月19日",
|
||||||
|
"start_month": 3,
|
||||||
|
"start_day": 21,
|
||||||
|
"end_month": 4,
|
||||||
|
"end_day": 19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "金牛",
|
||||||
|
"desc": "金牛座",
|
||||||
|
"start": "4月20日",
|
||||||
|
"end": "5月20日",
|
||||||
|
"range": "4月20日~5月20日",
|
||||||
|
"start_month": 4,
|
||||||
|
"start_day": 20,
|
||||||
|
"end_month": 5,
|
||||||
|
"end_day": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "双子",
|
||||||
|
"desc": "双子座",
|
||||||
|
"start": "5月21日",
|
||||||
|
"end": "6月21日",
|
||||||
|
"range": "5月21日~6月21日",
|
||||||
|
"start_month": 5,
|
||||||
|
"start_day": 21,
|
||||||
|
"end_month": 6,
|
||||||
|
"end_day": 21
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "巨蟹",
|
||||||
|
"desc": "巨蟹座",
|
||||||
|
"start": "6月22日",
|
||||||
|
"end": "7月22日",
|
||||||
|
"range": "6月22日~7月22日",
|
||||||
|
"start_month": 6,
|
||||||
|
"start_day": 22,
|
||||||
|
"end_month": 7,
|
||||||
|
"end_day": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "狮子",
|
||||||
|
"desc": "狮子座",
|
||||||
|
"start": "7月23日",
|
||||||
|
"end": "8月22日",
|
||||||
|
"range": "7月23日~8月22日",
|
||||||
|
"start_month": 7,
|
||||||
|
"start_day": 23,
|
||||||
|
"end_month": 8,
|
||||||
|
"end_day": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "处女",
|
||||||
|
"desc": "处女座",
|
||||||
|
"start": "8月23日",
|
||||||
|
"end": "9月22日",
|
||||||
|
"range": "8月23日~9月22日",
|
||||||
|
"start_month": 8,
|
||||||
|
"start_day": 23,
|
||||||
|
"end_month": 9,
|
||||||
|
"end_day": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "天秤",
|
||||||
|
"desc": "天秤座",
|
||||||
|
"start": "9月23日",
|
||||||
|
"end": "10月23日",
|
||||||
|
"range": "9月23日~10月23日",
|
||||||
|
"start_month": 9,
|
||||||
|
"start_day": 23,
|
||||||
|
"end_month": 10,
|
||||||
|
"end_day": 23
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "天蝎",
|
||||||
|
"desc": "天蝎座",
|
||||||
|
"start": "10月24日",
|
||||||
|
"end": "11月22日",
|
||||||
|
"range": "10月24日~11月22日",
|
||||||
|
"start_month": 10,
|
||||||
|
"start_day": 24,
|
||||||
|
"end_month": 11,
|
||||||
|
"end_day": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "射手",
|
||||||
|
"desc": "射手座",
|
||||||
|
"start": "11月23日",
|
||||||
|
"end": "12月21日",
|
||||||
|
"range": "11月23日~12月21日",
|
||||||
|
"start_month": 11,
|
||||||
|
"start_day": 23,
|
||||||
|
"end_month": 12,
|
||||||
|
"end_day": 21
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "摩羯",
|
||||||
|
"desc": "摩羯座",
|
||||||
|
"start": "12月22日",
|
||||||
|
"end": "1月19日",
|
||||||
|
"range": "12月22日~1月19日",
|
||||||
|
"start_month": 12,
|
||||||
|
"start_day": 22,
|
||||||
|
"end_month": 1,
|
||||||
|
"end_day": 19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "水瓶",
|
||||||
|
"desc": "水瓶座",
|
||||||
|
"start": "1月20日",
|
||||||
|
"end": "2月18日",
|
||||||
|
"range": "1月20日~2月18日",
|
||||||
|
"start_month": 1,
|
||||||
|
"start_day": 20,
|
||||||
|
"end_month": 2,
|
||||||
|
"end_day": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "双鱼",
|
||||||
|
"desc": "双鱼座",
|
||||||
|
"start": "2月19日",
|
||||||
|
"end": "3月20日",
|
||||||
|
"range": "2月19日~3月20日",
|
||||||
|
"start_month": 2,
|
||||||
|
"start_day": 19,
|
||||||
|
"end_month": 3,
|
||||||
|
"end_day": 20
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"heaven_stems": [
|
||||||
|
"甲",
|
||||||
|
"乙",
|
||||||
|
"丙",
|
||||||
|
"丁",
|
||||||
|
"戊",
|
||||||
|
"己",
|
||||||
|
"庚",
|
||||||
|
"辛",
|
||||||
|
"壬",
|
||||||
|
"癸"
|
||||||
|
],
|
||||||
|
"earth_branches": [
|
||||||
|
"子",
|
||||||
|
"丑",
|
||||||
|
"寅",
|
||||||
|
"卯",
|
||||||
|
"辰",
|
||||||
|
"巳",
|
||||||
|
"午",
|
||||||
|
"未",
|
||||||
|
"申",
|
||||||
|
"酉",
|
||||||
|
"戌",
|
||||||
|
"亥"
|
||||||
|
],
|
||||||
|
"solar_terms": [
|
||||||
|
{
|
||||||
|
"name": "立春",
|
||||||
|
"desc": "春季开始"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "雨水",
|
||||||
|
"desc": "降雨增多"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "惊蛰",
|
||||||
|
"desc": "春雷乍响"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "春分",
|
||||||
|
"desc": "昼夜等长"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "清明",
|
||||||
|
"desc": "天清地明"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "谷雨",
|
||||||
|
"desc": "雨生百谷"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "立夏",
|
||||||
|
"desc": "夏季开始"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "小满",
|
||||||
|
"desc": "麦粒渐满"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "芒种",
|
||||||
|
"desc": "麦类收割"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "夏至",
|
||||||
|
"desc": "白昼最长"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "小暑",
|
||||||
|
"desc": "天气渐热"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "大暑",
|
||||||
|
"desc": "一年最热"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "立秋",
|
||||||
|
"desc": "秋季开始"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "处暑",
|
||||||
|
"desc": "暑热结束"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "白露",
|
||||||
|
"desc": "露水增多"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "秋分",
|
||||||
|
"desc": "昼夜等长"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "寒露",
|
||||||
|
"desc": "露水渐凉"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "霜降",
|
||||||
|
"desc": "开始降霜"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "立冬",
|
||||||
|
"desc": "冬季开始"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "小雪",
|
||||||
|
"desc": "开始降雪"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "大雪",
|
||||||
|
"desc": "降雪增多"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "冬至",
|
||||||
|
"desc": "白昼最短"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "小寒",
|
||||||
|
"desc": "天气渐冷"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "大寒",
|
||||||
|
"desc": "一年最冷"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
frontend/react-app/public/60sapi/实用功能/实时天气/css/background.css
Normal file
145
frontend/react-app/public/60sapi/实用功能/实时天气/css/background.css
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/* 背景样式文件 */
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #e8f5e8 0%, #d4f1d4 25%, #c8ecc8 50%, #b8e6b8 75%, #a8d5ba 100%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradientShift 15s ease infinite;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景渐变动画 */
|
||||||
|
@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(168, 213, 186, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 80% 20%, rgba(107, 183, 123, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 40% 40%, rgba(200, 236, 200, 0.1) 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(168, 213, 186, 0.3), transparent),
|
||||||
|
radial-gradient(2px 2px at 40px 70px, rgba(107, 183, 123, 0.2), transparent),
|
||||||
|
radial-gradient(1px 1px at 90px 40px, rgba(200, 236, 200, 0.4), transparent),
|
||||||
|
radial-gradient(1px 1px at 130px 80px, rgba(168, 213, 186, 0.2), transparent),
|
||||||
|
radial-gradient(2px 2px at 160px 30px, rgba(107, 183, 123, 0.3), transparent);
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-size: 200px 100px;
|
||||||
|
animation: float 20s linear infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 云朵装饰效果 */
|
||||||
|
.container::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50px;
|
||||||
|
right: -50px;
|
||||||
|
width: 200px;
|
||||||
|
height: 100px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 50px;
|
||||||
|
box-shadow:
|
||||||
|
-30px 20px 0 rgba(255, 255, 255, 0.08),
|
||||||
|
30px 40px 0 rgba(255, 255, 255, 0.06);
|
||||||
|
animation: cloudFloat 25s ease-in-out infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -30px;
|
||||||
|
left: -30px;
|
||||||
|
width: 150px;
|
||||||
|
height: 80px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 40px;
|
||||||
|
box-shadow:
|
||||||
|
20px 15px 0 rgba(255, 255, 255, 0.06),
|
||||||
|
-20px 25px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
animation: cloudFloat 30s ease-in-out infinite reverse;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cloudFloat {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateX(0px) translateY(0px);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translateX(20px) translateY(-10px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(-10px) translateY(-20px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateX(15px) translateY(-5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式背景调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body::after {
|
||||||
|
background-size: 150px 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container::before,
|
||||||
|
.container::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #e8f5e8 0%, #d4f1d4 50%, #a8d5ba 100%);
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before,
|
||||||
|
body::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
409
frontend/react-app/public/60sapi/实用功能/实时天气/css/style.css
Normal file
409
frontend/react-app/public/60sapi/实用功能/实时天气/css/style.css
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
/* 基础样式重置 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部样式 */
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
color: #2d5a3d;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 300;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索区域 */
|
||||||
|
.search-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cityInput {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid #a8d5ba;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 16px;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#cityInput:focus {
|
||||||
|
border-color: #6bb77b;
|
||||||
|
box-shadow: 0 0 10px rgba(107, 183, 123, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchBtn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: linear-gradient(135deg, #6bb77b, #5a9f6a);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchBtn:hover {
|
||||||
|
background: linear-gradient(135deg, #5a9f6a, #4a8759);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(107, 183, 123, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #e8f5e8;
|
||||||
|
border-top: 4px solid #6bb77b;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 天气容器 */
|
||||||
|
.weather-container {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(168, 213, 186, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 位置信息 */
|
||||||
|
.location-info {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #e8f5e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-info h2 {
|
||||||
|
color: #2d5a3d;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-info p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当前天气 */
|
||||||
|
.current-weather {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-main {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature {
|
||||||
|
font-size: 4rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #2d5a3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #6bb77b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-desc p:first-child {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #2d5a3d;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-desc p:last-child {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 天气详情 */
|
||||||
|
.weather-details {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
background: rgba(168, 213, 186, 0.1);
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid rgba(168, 213, 186, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item .label {
|
||||||
|
display: block;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item span:last-child {
|
||||||
|
color: #2d5a3d;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 生活指数 */
|
||||||
|
.life-index {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.life-index h3 {
|
||||||
|
color: #2d5a3d;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(168, 213, 186, 0.05);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
border: 1px solid rgba(168, 213, 186, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-item:hover {
|
||||||
|
background: rgba(168, 213, 186, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(168, 213, 186, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-right: 15px;
|
||||||
|
width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-content h4 {
|
||||||
|
color: #2d5a3d;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-content p {
|
||||||
|
color: #6bb77b;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-content span {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 更新时间 */
|
||||||
|
.update-time {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e8f5e8;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误信息 */
|
||||||
|
.error-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
background: rgba(255, 107, 107, 0.1);
|
||||||
|
border-radius: 15px;
|
||||||
|
border: 1px solid rgba(255, 107, 107, 0.2);
|
||||||
|
color: #d63031;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板端适配 (768px - 1024px) */
|
||||||
|
@media (min-width: 768px) and (max-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-details {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 电脑端适配 (1024px+) */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-container {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-main {
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-details {
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机端适配 (768px以下) */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchBtn {
|
||||||
|
padding: 14px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-container {
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0 -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-main {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-details {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-item {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
width: 40px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 超小屏幕适配 (480px以下) */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-container {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-details {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
140
frontend/react-app/public/60sapi/实用功能/实时天气/index.html
Normal file
140
frontend/react-app/public/60sapi/实用功能/实时天气/index.html
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<!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>实时天气查询</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="search-section">
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" id="cityInput" placeholder="请输入城市名称(如:北京)" value="北京">
|
||||||
|
<button id="searchBtn">查询天气</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="loading" id="loading" style="display: none;">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>正在获取天气信息...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="weather-container" id="weatherContainer" style="display: none;">
|
||||||
|
<div class="location-info">
|
||||||
|
<h2 id="locationName"></h2>
|
||||||
|
<p id="locationDetail"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="current-weather">
|
||||||
|
<div class="weather-main">
|
||||||
|
<div class="temperature">
|
||||||
|
<span id="temperature"></span>
|
||||||
|
<span class="unit">°C</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-desc">
|
||||||
|
<p id="weatherCondition"></p>
|
||||||
|
<p id="feelsLike"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="weather-details">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">湿度</span>
|
||||||
|
<span id="humidity"></span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">风向</span>
|
||||||
|
<span id="windDirection"></span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">风力</span>
|
||||||
|
<span id="windStrength"></span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">气压</span>
|
||||||
|
<span id="pressure"></span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">能见度</span>
|
||||||
|
<span id="visibility"></span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">空气质量</span>
|
||||||
|
<span id="aqi"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="life-index">
|
||||||
|
<h3>生活指数</h3>
|
||||||
|
<div class="index-grid">
|
||||||
|
<div class="index-item">
|
||||||
|
<div class="index-icon comfort">🌡️</div>
|
||||||
|
<div class="index-content">
|
||||||
|
<h4>舒适度</h4>
|
||||||
|
<p id="comfortLevel"></p>
|
||||||
|
<span id="comfortDesc"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="index-item">
|
||||||
|
<div class="index-icon clothing">👕</div>
|
||||||
|
<div class="index-content">
|
||||||
|
<h4>穿衣指数</h4>
|
||||||
|
<p id="clothingLevel"></p>
|
||||||
|
<span id="clothingDesc"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="index-item">
|
||||||
|
<div class="index-icon umbrella">☂️</div>
|
||||||
|
<div class="index-content">
|
||||||
|
<h4>雨伞指数</h4>
|
||||||
|
<p id="umbrellaLevel"></p>
|
||||||
|
<span id="umbrellaDesc"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="index-item">
|
||||||
|
<div class="index-icon uv">☀️</div>
|
||||||
|
<div class="index-content">
|
||||||
|
<h4>紫外线</h4>
|
||||||
|
<p id="uvLevel"></p>
|
||||||
|
<span id="uvDesc"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="index-item">
|
||||||
|
<div class="index-icon travel">🚗</div>
|
||||||
|
<div class="index-content">
|
||||||
|
<h4>出行指数</h4>
|
||||||
|
<p id="travelLevel"></p>
|
||||||
|
<span id="travelDesc"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="index-item">
|
||||||
|
<div class="index-icon sport">🏃</div>
|
||||||
|
<div class="index-content">
|
||||||
|
<h4>运动指数</h4>
|
||||||
|
<p id="sportLevel"></p>
|
||||||
|
<span id="sportDesc"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="update-time">
|
||||||
|
<p>更新时间:<span id="updateTime"></span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-message" id="errorMessage" style="display: none;">
|
||||||
|
<p>获取天气信息失败,请稍后重试</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
252
frontend/react-app/public/60sapi/实用功能/实时天气/js/script.js
vendored
Normal file
252
frontend/react-app/public/60sapi/实用功能/实时天气/js/script.js
vendored
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
// 天气查询应用
|
||||||
|
class WeatherApp {
|
||||||
|
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.currentEndpointIndex = 0;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.bindEvents();
|
||||||
|
// 页面加载时自动查询北京天气
|
||||||
|
this.searchWeather('北京');
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
const searchBtn = document.getElementById('searchBtn');
|
||||||
|
const cityInput = document.getElementById('cityInput');
|
||||||
|
|
||||||
|
searchBtn.addEventListener('click', () => {
|
||||||
|
const city = cityInput.value.trim();
|
||||||
|
if (city) {
|
||||||
|
this.searchWeather(city);
|
||||||
|
} else {
|
||||||
|
this.showError('请输入城市名称');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cityInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const city = cityInput.value.trim();
|
||||||
|
if (city) {
|
||||||
|
this.searchWeather(city);
|
||||||
|
} else {
|
||||||
|
this.showError('请输入城市名称');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 防止输入框为空时查询
|
||||||
|
cityInput.addEventListener('input', () => {
|
||||||
|
const searchBtn = document.getElementById('searchBtn');
|
||||||
|
searchBtn.disabled = !cityInput.value.trim();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchWeather(city) {
|
||||||
|
this.showLoading();
|
||||||
|
|
||||||
|
for (let i = 0; i < this.apiEndpoints.length; i++) {
|
||||||
|
try {
|
||||||
|
const endpoint = this.apiEndpoints[this.currentEndpointIndex];
|
||||||
|
const response = await this.fetchWeatherData(endpoint, city);
|
||||||
|
|
||||||
|
if (response && response.code === 200) {
|
||||||
|
this.displayWeatherData(response.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`API ${this.apiEndpoints[this.currentEndpointIndex]} 请求失败:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换到下一个API端点
|
||||||
|
this.currentEndpointIndex = (this.currentEndpointIndex + 1) % this.apiEndpoints.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有API都失败了
|
||||||
|
this.showError('获取天气信息失败,请检查网络连接或稍后重试');
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchWeatherData(endpoint, city) {
|
||||||
|
const url = `${endpoint}/v2/weather?query=${encodeURIComponent(city)}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
displayWeatherData(data) {
|
||||||
|
const { location, realtime } = data;
|
||||||
|
|
||||||
|
// 显示位置信息
|
||||||
|
document.getElementById('locationName').textContent = location.formatted;
|
||||||
|
document.getElementById('locationDetail').textContent =
|
||||||
|
`${location.province} ${location.city} | 邮编: ${location.zip_code}`;
|
||||||
|
|
||||||
|
// 显示当前天气
|
||||||
|
document.getElementById('temperature').textContent = realtime.temperature;
|
||||||
|
document.getElementById('weatherCondition').textContent = realtime.weather;
|
||||||
|
|
||||||
|
// 体感温度转换(API返回的是华氏度,需要转换为摄氏度)
|
||||||
|
const feelsLikeCelsius = this.fahrenheitToCelsius(realtime.temperature_feels_like);
|
||||||
|
document.getElementById('feelsLike').textContent =
|
||||||
|
`体感温度 ${feelsLikeCelsius}°C`;
|
||||||
|
|
||||||
|
// 显示天气详情
|
||||||
|
document.getElementById('humidity').textContent = `${realtime.humidity}%`;
|
||||||
|
document.getElementById('windDirection').textContent = realtime.wind_direction;
|
||||||
|
document.getElementById('windStrength').textContent = realtime.wind_strength;
|
||||||
|
document.getElementById('pressure').textContent = `${realtime.pressure} hPa`;
|
||||||
|
document.getElementById('visibility').textContent = realtime.visibility;
|
||||||
|
|
||||||
|
// 空气质量显示
|
||||||
|
const aqiElement = document.getElementById('aqi');
|
||||||
|
aqiElement.textContent = `${realtime.aqi} (PM2.5: ${realtime.pm25})`;
|
||||||
|
aqiElement.className = this.getAQIClass(realtime.aqi);
|
||||||
|
|
||||||
|
// 显示生活指数
|
||||||
|
const lifeIndex = realtime.life_index;
|
||||||
|
this.displayLifeIndex('comfort', lifeIndex.comfort);
|
||||||
|
this.displayLifeIndex('clothing', lifeIndex.clothing);
|
||||||
|
this.displayLifeIndex('umbrella', lifeIndex.umbrella);
|
||||||
|
this.displayLifeIndex('uv', lifeIndex.uv);
|
||||||
|
this.displayLifeIndex('travel', lifeIndex.travel);
|
||||||
|
this.displayLifeIndex('sport', lifeIndex.sport);
|
||||||
|
|
||||||
|
// 显示更新时间
|
||||||
|
document.getElementById('updateTime').textContent =
|
||||||
|
`${realtime.updated} (${realtime.updated_at})`;
|
||||||
|
|
||||||
|
this.showWeatherContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
displayLifeIndex(type, indexData) {
|
||||||
|
const levelElement = document.getElementById(`${type}Level`);
|
||||||
|
const descElement = document.getElementById(`${type}Desc`);
|
||||||
|
|
||||||
|
if (levelElement && descElement && indexData) {
|
||||||
|
levelElement.textContent = indexData.level;
|
||||||
|
descElement.textContent = indexData.desc;
|
||||||
|
|
||||||
|
// 根据指数级别设置颜色
|
||||||
|
levelElement.className = this.getIndexLevelClass(indexData.level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAQIClass(aqi) {
|
||||||
|
if (aqi <= 50) return 'aqi-good';
|
||||||
|
if (aqi <= 100) return 'aqi-moderate';
|
||||||
|
if (aqi <= 150) return 'aqi-unhealthy-sensitive';
|
||||||
|
if (aqi <= 200) return 'aqi-unhealthy';
|
||||||
|
if (aqi <= 300) return 'aqi-very-unhealthy';
|
||||||
|
return 'aqi-hazardous';
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndexLevelClass(level) {
|
||||||
|
const levelMap = {
|
||||||
|
'优': 'level-excellent',
|
||||||
|
'良': 'level-good',
|
||||||
|
'适宜': 'level-suitable',
|
||||||
|
'舒适': 'level-comfortable',
|
||||||
|
'较适宜': 'level-fairly-suitable',
|
||||||
|
'不宜': 'level-unsuitable',
|
||||||
|
'较不宜': 'level-fairly-unsuitable',
|
||||||
|
'带伞': 'level-bring-umbrella',
|
||||||
|
'最弱': 'level-weakest',
|
||||||
|
'弱': 'level-weak',
|
||||||
|
'中等': 'level-moderate',
|
||||||
|
'强': 'level-strong',
|
||||||
|
'很强': 'level-very-strong'
|
||||||
|
};
|
||||||
|
|
||||||
|
return levelMap[level] || 'level-default';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 华氏度转摄氏度
|
||||||
|
fahrenheitToCelsius(fahrenheit) {
|
||||||
|
const celsius = (fahrenheit - 32) * 5 / 9;
|
||||||
|
return Math.round(celsius * 10) / 10; // 保留一位小数
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading() {
|
||||||
|
document.getElementById('loading').style.display = 'block';
|
||||||
|
document.getElementById('weatherContainer').style.display = 'none';
|
||||||
|
document.getElementById('errorMessage').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
showWeatherContainer() {
|
||||||
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
document.getElementById('weatherContainer').style.display = 'block';
|
||||||
|
document.getElementById('errorMessage').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
document.getElementById('weatherContainer').style.display = 'none';
|
||||||
|
const errorElement = document.getElementById('errorMessage');
|
||||||
|
errorElement.style.display = 'block';
|
||||||
|
errorElement.querySelector('p').textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加生活指数级别样式
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.aqi-good { color: #52c41a; }
|
||||||
|
.aqi-moderate { color: #faad14; }
|
||||||
|
.aqi-unhealthy-sensitive { color: #fa8c16; }
|
||||||
|
.aqi-unhealthy { color: #f5222d; }
|
||||||
|
.aqi-very-unhealthy { color: #a0206e; }
|
||||||
|
.aqi-hazardous { color: #722ed1; }
|
||||||
|
|
||||||
|
.level-excellent, .level-suitable, .level-comfortable { color: #52c41a; }
|
||||||
|
.level-good, .level-fairly-suitable { color: #1890ff; }
|
||||||
|
.level-bring-umbrella, .level-moderate { color: #faad14; }
|
||||||
|
.level-unsuitable, .level-fairly-unsuitable { color: #f5222d; }
|
||||||
|
.level-weakest, .level-weak { color: #52c41a; }
|
||||||
|
.level-strong, .level-very-strong { color: #fa8c16; }
|
||||||
|
.level-default { color: #666; }
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// 页面加载完成后初始化应用
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new WeatherApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加页面可见性检测,当页面重新可见时刷新数据
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
const cityInput = document.getElementById('cityInput');
|
||||||
|
const city = cityInput.value.trim() || '北京';
|
||||||
|
// 延迟1秒刷新,避免频繁请求
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.weatherApp) {
|
||||||
|
window.weatherApp.searchWeather(city);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将应用实例暴露到全局,方便调试和其他功能调用
|
||||||
|
window.weatherApp = null;
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.weatherApp = new WeatherApp();
|
||||||
|
});
|
||||||
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"
|
||||||
|
]
|
||||||
68
frontend/react-app/public/60sapi/实用功能/实时天气/返回接口.json
Normal file
68
frontend/react-app/public/60sapi/实用功能/实时天气/返回接口.json
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||||
|
"data": {
|
||||||
|
"location": {
|
||||||
|
"province": "北京",
|
||||||
|
"city": "北京",
|
||||||
|
"town": "北京",
|
||||||
|
"formatted": "北京",
|
||||||
|
"location_id": "101010100",
|
||||||
|
"detail_url": "http://www.weather.com.cn/weather/101010100.shtml",
|
||||||
|
"is_province": true,
|
||||||
|
"is_city": false,
|
||||||
|
"is_town": false,
|
||||||
|
"area_code": "10",
|
||||||
|
"zip_code": "100000"
|
||||||
|
},
|
||||||
|
"realtime": {
|
||||||
|
"weather": "小雨转多云",
|
||||||
|
"weather_desc": "未知",
|
||||||
|
"weather_code": "d7",
|
||||||
|
"temperature": 26,
|
||||||
|
"temperature_feels_like": 81.1,
|
||||||
|
"humidity": 78,
|
||||||
|
"wind_direction": "南风转北风",
|
||||||
|
"wind_strength": "\u003C3级",
|
||||||
|
"wind_speed": "5km/h",
|
||||||
|
"pressure": 1008,
|
||||||
|
"visibility": "8km",
|
||||||
|
"aqi": 37,
|
||||||
|
"pm25": 37,
|
||||||
|
"rainfall": 0,
|
||||||
|
"rainfall_24h": 0,
|
||||||
|
"updated": "2025-08-29 08:00:00",
|
||||||
|
"updated_at": "15:10",
|
||||||
|
"life_index": {
|
||||||
|
"comfort": {
|
||||||
|
"level": "舒适",
|
||||||
|
"desc": "白天温度宜人,风力不大。"
|
||||||
|
},
|
||||||
|
"clothing": {
|
||||||
|
"level": "舒适",
|
||||||
|
"desc": "建议穿长袖衬衫单裤等服装。"
|
||||||
|
},
|
||||||
|
"umbrella": {
|
||||||
|
"level": "带伞",
|
||||||
|
"desc": "有降水,带雨伞,短期外出可收起雨伞。"
|
||||||
|
},
|
||||||
|
"uv": {
|
||||||
|
"level": "最弱",
|
||||||
|
"desc": "辐射弱,涂擦SPF8-12防晒护肤品。"
|
||||||
|
},
|
||||||
|
"car_wash": {
|
||||||
|
"level": "不宜",
|
||||||
|
"desc": "有雨,雨水和泥水会弄脏爱车。"
|
||||||
|
},
|
||||||
|
"travel": {
|
||||||
|
"level": "适宜",
|
||||||
|
"desc": "较弱降水和微风将伴您共赴旅程。"
|
||||||
|
},
|
||||||
|
"sport": {
|
||||||
|
"level": "较不宜",
|
||||||
|
"desc": "有降水,推荐您在室内进行休闲运动。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
frontend/react-app/public/60sapi/实用功能/生成二维码/css/background.css
Normal file
132
frontend/react-app/public/60sapi/实用功能/生成二维码/css/background.css
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/* 背景样式文件 - 独立分离便于迁移 */
|
||||||
|
|
||||||
|
/* 主背景渐变 */
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
#e8f5e8 0%,
|
||||||
|
#f1f8e9 25%,
|
||||||
|
#e8f5e8 50%,
|
||||||
|
#c8e6c9 75%,
|
||||||
|
#e8f5e8 100%);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: backgroundShift 15s ease-in-out infinite;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景动画 */
|
||||||
|
@keyframes backgroundShift {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 50% 100%;
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
background-position: 50% 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景装饰元素 */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 20% 20%, rgba(76, 175, 80, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 80% 80%, rgba(129, 199, 132, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 40% 60%, rgba(165, 214, 167, 0.08) 0%, transparent 50%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
animation: floatingBubbles 20s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes floatingBubbles {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0px) rotate(0deg);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
33% {
|
||||||
|
transform: translateY(-20px) rotate(120deg);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
transform: translateY(10px) rotate(240deg);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景粒子效果 */
|
||||||
|
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;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
animation: particleFloat 25s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes particleFloat {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-100px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式背景调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body::after {
|
||||||
|
background-size: 150px 75px;
|
||||||
|
animation-duration: 20s;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
animation-duration: 15s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
body::after {
|
||||||
|
background-size: 100px 50px;
|
||||||
|
animation-duration: 15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
animation-duration: 12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
animation-duration: 12s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 高性能模式 - 减少动画 */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
body,
|
||||||
|
body::before,
|
||||||
|
body::after {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 50%, #c8e6c9 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
468
frontend/react-app/public/60sapi/实用功能/生成二维码/css/style.css
Normal file
468
frontend/react-app/public/60sapi/实用功能/生成二维码/css/style.css
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
/* 基础样式重置 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #2d5a3d;
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 容器样式 */
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部样式 */
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding: 40px 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 8px 32px rgba(76, 175, 80, 0.1);
|
||||||
|
border: 1px solid rgba(76, 175, 80, 0.2);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, transparent, #4caf50, transparent);
|
||||||
|
animation: headerGlow 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes headerGlow {
|
||||||
|
0% { left: -100%; }
|
||||||
|
50% { left: 100%; }
|
||||||
|
100% { left: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(135deg, #4caf50, #81c784, #4caf50);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
animation: titleGradient 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes titleGradient {
|
||||||
|
0%, 100% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #66bb6a;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主要内容区域 */
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 40px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单容器 */
|
||||||
|
.form-container {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
padding: 35px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 8px 32px rgba(76, 175, 80, 0.1);
|
||||||
|
border: 1px solid rgba(76, 175, 80, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 40px rgba(76, 175, 80, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单样式 */
|
||||||
|
.qr-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2d5a3d;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea, select {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid rgba(76, 175, 80, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus, select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4caf50;
|
||||||
|
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
|
||||||
|
background: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
.generate-btn {
|
||||||
|
padding: 15px 30px;
|
||||||
|
background: linear-gradient(135deg, #4caf50, #66bb6a);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-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 ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn.loading .btn-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn.loading .btn-loading {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-loading {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 结果容器 */
|
||||||
|
.result-container {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
padding: 35px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 8px 32px rgba(76, 175, 80, 0.1);
|
||||||
|
border: 1px solid rgba(76, 175, 80, 0.2);
|
||||||
|
min-height: 400px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 4px solid rgba(76, 175, 80, 0.2);
|
||||||
|
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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误样式 */
|
||||||
|
.error {
|
||||||
|
text-align: center;
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #4caf50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn:hover {
|
||||||
|
background: #45a049;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 结果显示 */
|
||||||
|
.result {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-display {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-display img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(76, 175, 80, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-display img:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 8px 30px rgba(76, 175, 80, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-text {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #666;
|
||||||
|
word-break: break-all;
|
||||||
|
background: rgba(76, 175, 80, 0.1);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn, .copy-btn, .new-btn {
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
background: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
background: #2196f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-btn {
|
||||||
|
background: #ff9800;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn:hover, .copy-btn:hover, .new-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏类 */
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页脚样式 */
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 40px;
|
||||||
|
padding: 25px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
border-radius: 15px;
|
||||||
|
color: #66bb6a;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border: 1px solid rgba(76, 175, 80, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板端适配 (768px - 1024px) */
|
||||||
|
@media (max-width: 1024px) and (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
max-width: 95%;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container, .result-container {
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机端适配 (最大768px) */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding: 25px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container, .result-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn, .copy-btn, .new-btn {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小屏手机适配 (最大480px) */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 20px 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container, .result-container {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-display img {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
98
frontend/react-app/public/60sapi/实用功能/生成二维码/index.html
Normal file
98
frontend/react-app/public/60sapi/实用功能/生成二维码/index.html
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<!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>🔗 二维码生成器</h1>
|
||||||
|
<p>快速生成高质量二维码</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="form-container">
|
||||||
|
<form id="qrForm" class="qr-form">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="text">文本内容</label>
|
||||||
|
<textarea id="text" name="text" placeholder="请输入要生成二维码的文本或URL" required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options-grid">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="size">尺寸大小</label>
|
||||||
|
<select id="size" name="size">
|
||||||
|
<option value="128">128x128</option>
|
||||||
|
<option value="256" selected>256x256</option>
|
||||||
|
<option value="512">512x512</option>
|
||||||
|
<option value="1024">1024x1024</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="level">容错级别</label>
|
||||||
|
<select id="level" name="level">
|
||||||
|
<option value="L">L - 低 (7%)</option>
|
||||||
|
<option value="M" selected>M - 中 (15%)</option>
|
||||||
|
<option value="Q">Q - 高 (25%)</option>
|
||||||
|
<option value="H">H - 最高 (30%)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="encoding">返回格式</label>
|
||||||
|
<select id="encoding" name="encoding">
|
||||||
|
<option value="image" selected>图片</option>
|
||||||
|
<option value="json">JSON</option>
|
||||||
|
<option value="text">文本</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="generate-btn">
|
||||||
|
<span class="btn-text">生成二维码</span>
|
||||||
|
<span class="btn-loading">生成中...</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-container">
|
||||||
|
<div id="loading" class="loading hidden">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>正在生成二维码...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="error" class="error hidden">
|
||||||
|
<div class="error-icon">⚠️</div>
|
||||||
|
<p class="error-message"></p>
|
||||||
|
<button class="retry-btn">重试</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="result" class="result hidden">
|
||||||
|
<div class="qr-display">
|
||||||
|
<img id="qrImage" src="" alt="生成的二维码">
|
||||||
|
</div>
|
||||||
|
<div class="result-info">
|
||||||
|
<p class="result-text"></p>
|
||||||
|
<div class="result-actions">
|
||||||
|
<button class="download-btn">下载二维码</button>
|
||||||
|
<button class="copy-btn">复制链接</button>
|
||||||
|
<button class="new-btn">生成新的</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>© 2024 二维码生成器 - 简单快捷的二维码生成工具</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
417
frontend/react-app/public/60sapi/实用功能/生成二维码/js/script.js
vendored
Normal file
417
frontend/react-app/public/60sapi/实用功能/生成二维码/js/script.js
vendored
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
// 二维码生成器 JavaScript
|
||||||
|
class QRCodeGenerator {
|
||||||
|
constructor() {
|
||||||
|
this.apiEndpoints = [];
|
||||||
|
this.currentApiIndex = 0;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
async init() {
|
||||||
|
await this.loadApiEndpoints();
|
||||||
|
this.bindEvents();
|
||||||
|
this.setupFormValidation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载API接口列表
|
||||||
|
async loadApiEndpoints() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('./接口集合.json');
|
||||||
|
this.apiEndpoints = await response.json();
|
||||||
|
console.log('已加载API接口:', this.apiEndpoints);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载API接口失败:', error);
|
||||||
|
this.showError('加载配置失败,请刷新页面重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
bindEvents() {
|
||||||
|
const form = document.getElementById('qrForm');
|
||||||
|
const retryBtn = document.querySelector('.retry-btn');
|
||||||
|
const downloadBtn = document.querySelector('.download-btn');
|
||||||
|
const copyBtn = document.querySelector('.copy-btn');
|
||||||
|
const newBtn = document.querySelector('.new-btn');
|
||||||
|
|
||||||
|
form.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||||
|
retryBtn.addEventListener('click', () => this.retryGeneration());
|
||||||
|
downloadBtn.addEventListener('click', () => this.downloadQRCode());
|
||||||
|
copyBtn.addEventListener('click', () => this.copyImageLink());
|
||||||
|
newBtn.addEventListener('click', () => this.resetForm());
|
||||||
|
|
||||||
|
// 实时字符计数
|
||||||
|
const textArea = document.getElementById('text');
|
||||||
|
textArea.addEventListener('input', () => this.updateCharCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置表单验证
|
||||||
|
setupFormValidation() {
|
||||||
|
const textArea = document.getElementById('text');
|
||||||
|
const form = document.getElementById('qrForm');
|
||||||
|
|
||||||
|
textArea.addEventListener('blur', () => {
|
||||||
|
if (textArea.value.trim() === '') {
|
||||||
|
this.showFieldError(textArea, '请输入要生成二维码的内容');
|
||||||
|
} else {
|
||||||
|
this.clearFieldError(textArea);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示字段错误
|
||||||
|
showFieldError(field, message) {
|
||||||
|
this.clearFieldError(field);
|
||||||
|
field.style.borderColor = '#d32f2f';
|
||||||
|
const errorDiv = document.createElement('div');
|
||||||
|
errorDiv.className = 'field-error';
|
||||||
|
errorDiv.style.color = '#d32f2f';
|
||||||
|
errorDiv.style.fontSize = '0.8rem';
|
||||||
|
errorDiv.style.marginTop = '5px';
|
||||||
|
errorDiv.textContent = message;
|
||||||
|
field.parentNode.appendChild(errorDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除字段错误
|
||||||
|
clearFieldError(field) {
|
||||||
|
field.style.borderColor = '';
|
||||||
|
const errorDiv = field.parentNode.querySelector('.field-error');
|
||||||
|
if (errorDiv) {
|
||||||
|
errorDiv.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新字符计数
|
||||||
|
updateCharCount() {
|
||||||
|
const textArea = document.getElementById('text');
|
||||||
|
const text = textArea.value;
|
||||||
|
const length = text.length;
|
||||||
|
|
||||||
|
// 移除旧的计数显示
|
||||||
|
const oldCounter = textArea.parentNode.querySelector('.char-counter');
|
||||||
|
if (oldCounter) oldCounter.remove();
|
||||||
|
|
||||||
|
// 添加新的计数显示
|
||||||
|
if (length > 0) {
|
||||||
|
const counter = document.createElement('div');
|
||||||
|
counter.className = 'char-counter';
|
||||||
|
counter.style.fontSize = '0.8rem';
|
||||||
|
counter.style.color = '#666';
|
||||||
|
counter.style.textAlign = 'right';
|
||||||
|
counter.style.marginTop = '5px';
|
||||||
|
counter.textContent = `${length} 个字符`;
|
||||||
|
textArea.parentNode.appendChild(counter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理表单提交
|
||||||
|
async handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
const params = {
|
||||||
|
text: formData.get('text').trim(),
|
||||||
|
size: formData.get('size'),
|
||||||
|
level: formData.get('level'),
|
||||||
|
encoding: formData.get('encoding')
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证输入
|
||||||
|
if (!params.text) {
|
||||||
|
this.showFieldError(document.getElementById('text'), '请输入要生成二维码的内容');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showLoading();
|
||||||
|
await this.generateQRCode(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成二维码
|
||||||
|
async generateQRCode(params) {
|
||||||
|
let success = false;
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
|
// 尝试所有API接口
|
||||||
|
for (let i = 0; i < this.apiEndpoints.length; i++) {
|
||||||
|
const apiIndex = (this.currentApiIndex + i) % this.apiEndpoints.length;
|
||||||
|
const apiUrl = this.apiEndpoints[apiIndex];
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`尝试API ${apiIndex + 1}:`, apiUrl);
|
||||||
|
const result = await this.callAPI(apiUrl, params);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.currentApiIndex = apiIndex; // 记录成功的API
|
||||||
|
this.showResult(result.data, params);
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`API ${apiIndex + 1} 失败:`, error);
|
||||||
|
lastError = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
this.showError(lastError?.message || '所有API接口都无法访问,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API
|
||||||
|
async callAPI(baseUrl, params) {
|
||||||
|
const url = new URL('/v2/qrcode', baseUrl);
|
||||||
|
|
||||||
|
// 添加查询参数
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value) {
|
||||||
|
url.searchParams.append(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('请求URL:', url.toString());
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
method: 'GET',
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json, image/*'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据返回格式处理
|
||||||
|
if (params.encoding === 'image') {
|
||||||
|
const blob = await response.blob();
|
||||||
|
const imageUrl = URL.createObjectURL(blob);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
text: params.text,
|
||||||
|
size: params.size,
|
||||||
|
level: params.level,
|
||||||
|
format: 'image'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const jsonData = await response.json();
|
||||||
|
if (jsonData.code === 0 && jsonData.data) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
imageUrl: jsonData.data.data_uri,
|
||||||
|
text: params.text,
|
||||||
|
size: params.size,
|
||||||
|
level: params.level,
|
||||||
|
format: 'json',
|
||||||
|
base64: jsonData.data.base64,
|
||||||
|
mimeType: jsonData.data.mime_type
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(jsonData.message || '生成失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
throw new Error('请求超时,请重试');
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
showLoading() {
|
||||||
|
this.hideAllStates();
|
||||||
|
document.getElementById('loading').classList.remove('hidden');
|
||||||
|
|
||||||
|
const btn = document.querySelector('.generate-btn');
|
||||||
|
btn.classList.add('loading');
|
||||||
|
btn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误
|
||||||
|
showError(message) {
|
||||||
|
this.hideAllStates();
|
||||||
|
const errorDiv = document.getElementById('error');
|
||||||
|
const errorMessage = errorDiv.querySelector('.error-message');
|
||||||
|
errorMessage.textContent = message;
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
|
||||||
|
this.resetButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示结果
|
||||||
|
showResult(data, params) {
|
||||||
|
this.hideAllStates();
|
||||||
|
|
||||||
|
const resultDiv = document.getElementById('result');
|
||||||
|
const qrImage = document.getElementById('qrImage');
|
||||||
|
const resultText = document.querySelector('.result-text');
|
||||||
|
|
||||||
|
qrImage.src = data.imageUrl;
|
||||||
|
qrImage.alt = `二维码: ${data.text}`;
|
||||||
|
|
||||||
|
resultText.innerHTML = `
|
||||||
|
<strong>内容:</strong> ${this.escapeHtml(data.text)}<br>
|
||||||
|
<strong>尺寸:</strong> ${data.size}x${data.size}<br>
|
||||||
|
<strong>容错级别:</strong> ${data.level}<br>
|
||||||
|
<strong>格式:</strong> ${data.format.toUpperCase()}
|
||||||
|
`;
|
||||||
|
|
||||||
|
resultDiv.classList.remove('hidden');
|
||||||
|
this.resetButton();
|
||||||
|
|
||||||
|
// 保存数据供下载使用
|
||||||
|
this.currentQRData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏所有状态
|
||||||
|
hideAllStates() {
|
||||||
|
document.getElementById('loading').classList.add('hidden');
|
||||||
|
document.getElementById('error').classList.add('hidden');
|
||||||
|
document.getElementById('result').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置按钮状态
|
||||||
|
resetButton() {
|
||||||
|
const btn = document.querySelector('.generate-btn');
|
||||||
|
btn.classList.remove('loading');
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试生成
|
||||||
|
async retryGeneration() {
|
||||||
|
const form = document.getElementById('qrForm');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const params = {
|
||||||
|
text: formData.get('text').trim(),
|
||||||
|
size: formData.get('size'),
|
||||||
|
level: formData.get('level'),
|
||||||
|
encoding: formData.get('encoding')
|
||||||
|
};
|
||||||
|
|
||||||
|
this.showLoading();
|
||||||
|
await this.generateQRCode(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载二维码
|
||||||
|
downloadQRCode() {
|
||||||
|
if (!this.currentQRData) return;
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = this.currentQRData.imageUrl;
|
||||||
|
link.download = `qrcode_${Date.now()}.png`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
this.showToast('二维码已下载');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制图片链接
|
||||||
|
async copyImageLink() {
|
||||||
|
if (!this.currentQRData) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(this.currentQRData.imageUrl);
|
||||||
|
this.showToast('链接已复制到剪贴板');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制失败:', error);
|
||||||
|
this.showToast('复制失败,请手动复制');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
resetForm() {
|
||||||
|
document.getElementById('qrForm').reset();
|
||||||
|
this.hideAllStates();
|
||||||
|
this.currentQRData = null;
|
||||||
|
|
||||||
|
// 清除字符计数
|
||||||
|
const counter = document.querySelector('.char-counter');
|
||||||
|
if (counter) counter.remove();
|
||||||
|
|
||||||
|
// 清除字段错误
|
||||||
|
document.querySelectorAll('input, textarea, select').forEach(field => {
|
||||||
|
this.clearFieldError(field);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 聚焦到文本框
|
||||||
|
document.getElementById('text').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示提示消息
|
||||||
|
showToast(message) {
|
||||||
|
// 移除旧的toast
|
||||||
|
const oldToast = document.querySelector('.toast');
|
||||||
|
if (oldToast) oldToast.remove();
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'toast';
|
||||||
|
toast.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #4caf50;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||||
|
z-index: 1000;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
`;
|
||||||
|
toast.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.animation = 'slideOut 0.3s ease';
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML转义
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加CSS动画
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes slideOut {
|
||||||
|
from { transform: translateX(0); opacity: 1; }
|
||||||
|
to { transform: translateX(100%); opacity: 0; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// 页面加载完成后初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new QRCodeGenerator();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
window.addEventListener('error', (e) => {
|
||||||
|
console.error('全局错误:', e.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('unhandledrejection', (e) => {
|
||||||
|
console.error('未处理的Promise拒绝:', e.reason);
|
||||||
|
});
|
||||||
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"
|
||||||
|
]
|
||||||
10
frontend/react-app/public/60sapi/实用功能/生成二维码/返回接口.json
Normal file
10
frontend/react-app/public/60sapi/实用功能/生成二维码/返回接口.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"base64": "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7Z15fBTV...",
|
||||||
|
"data_uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7Z15fBTV...",
|
||||||
|
"mime_type": "image/png",
|
||||||
|
"text": "https://example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
192
frontend/react-app/public/60sapi/实用功能/百度百科词条/css/background.css
Normal file
192
frontend/react-app/public/60sapi/实用功能/百度百科词条/css/background.css
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
/* 彩虹渐变背景样式 */
|
||||||
|
|
||||||
|
/* 主背景渐变 */
|
||||||
|
body {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 107, 107, 0.3) 0%,
|
||||||
|
rgba(255, 165, 0, 0.3) 14.28%,
|
||||||
|
rgba(255, 255, 0, 0.25) 28.56%,
|
||||||
|
rgba(50, 205, 50, 0.3) 42.84%,
|
||||||
|
rgba(0, 191, 255, 0.3) 57.12%,
|
||||||
|
rgba(65, 105, 225, 0.3) 71.4%,
|
||||||
|
rgba(147, 112, 219, 0.3) 85.68%,
|
||||||
|
rgba(255, 105, 180, 0.3) 100%
|
||||||
|
);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: rainbowShift 20s ease infinite;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 彩虹渐变动画 */
|
||||||
|
@keyframes rainbowShift {
|
||||||
|
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: rgba(255, 255, 255, 0.4);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索按钮彩虹渐变 */
|
||||||
|
.search-btn {
|
||||||
|
background: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgba(255, 107, 107, 0.8),
|
||||||
|
rgba(255, 165, 0, 0.8),
|
||||||
|
rgba(255, 255, 0, 0.7),
|
||||||
|
rgba(50, 205, 50, 0.8),
|
||||||
|
rgba(0, 191, 255, 0.8),
|
||||||
|
rgba(65, 105, 225, 0.8),
|
||||||
|
rgba(147, 112, 219, 0.8)
|
||||||
|
);
|
||||||
|
background-size: 300% 300%;
|
||||||
|
animation: buttonRainbow 12s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes buttonRainbow {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 结果卡片边框彩虹渐变 */
|
||||||
|
.result-card {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
background: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgba(255, 107, 107, 0.4),
|
||||||
|
rgba(255, 165, 0, 0.4),
|
||||||
|
rgba(255, 255, 0, 0.3),
|
||||||
|
rgba(50, 205, 50, 0.4),
|
||||||
|
rgba(0, 191, 255, 0.4),
|
||||||
|
rgba(65, 105, 225, 0.4),
|
||||||
|
rgba(147, 112, 219, 0.4),
|
||||||
|
rgba(255, 107, 107, 0.4)
|
||||||
|
);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: borderRainbow 15s linear infinite;
|
||||||
|
border-radius: inherit;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes borderRainbow {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 400% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画彩虹效果 */
|
||||||
|
.loading-spinner {
|
||||||
|
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top: 4px solid transparent;
|
||||||
|
border-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
#ff6b6b,
|
||||||
|
#ffa500,
|
||||||
|
#ffff00,
|
||||||
|
#32cd32,
|
||||||
|
#00bfff,
|
||||||
|
#4169e1,
|
||||||
|
#9370db
|
||||||
|
) 1;
|
||||||
|
animation: spin 1s linear infinite, colorShift 3s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes colorShift {
|
||||||
|
0%, 100% {
|
||||||
|
filter: hue-rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
filter: hue-rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 链接悬停彩虹效果 */
|
||||||
|
.result-link:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(255, 107, 107, 0.7),
|
||||||
|
rgba(255, 165, 0, 0.7),
|
||||||
|
rgba(255, 255, 0, 0.6),
|
||||||
|
rgba(50, 205, 50, 0.7),
|
||||||
|
rgba(0, 191, 255, 0.7),
|
||||||
|
rgba(65, 105, 225, 0.7),
|
||||||
|
rgba(147, 112, 219, 0.7)
|
||||||
|
);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: linkRainbow 3s ease infinite;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes linkRainbow {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题彩虹文字效果 */
|
||||||
|
.title {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(255, 107, 107, 0.8),
|
||||||
|
rgba(255, 165, 0, 0.8),
|
||||||
|
rgba(255, 255, 0, 0.7),
|
||||||
|
rgba(50, 205, 50, 0.8),
|
||||||
|
rgba(0, 191, 255, 0.8),
|
||||||
|
rgba(65, 105, 225, 0.8),
|
||||||
|
rgba(147, 112, 219, 0.8)
|
||||||
|
);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
animation: titleRainbow 8s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes titleRainbow {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
530
frontend/react-app/public/60sapi/实用功能/百度百科词条/css/style.css
Normal file
530
frontend/react-app/public/60sapi/实用功能/百度百科词条/css/style.css
Normal file
@@ -0,0 +1,530 @@
|
|||||||
|
/* 基础样式重置 */
|
||||||
|
* {
|
||||||
|
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: #333;
|
||||||
|
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: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-weight: 300;
|
||||||
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索区域 */
|
||||||
|
.search-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 8px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container:focus-within {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 15px 25px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
background: transparent;
|
||||||
|
color: #333;
|
||||||
|
border-radius: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: #999;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 15px 25px;
|
||||||
|
border-radius: 50px;
|
||||||
|
color: white;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-width: 120px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 结果区域 */
|
||||||
|
.result-section {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 结果卡片 */
|
||||||
|
.result-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
animation: slideUp 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-description {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
gap: 30px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-image-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-image {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 250px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-image:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-abstract h3 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-abstract p {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #555;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: rgba(0, 123, 255, 0.1);
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-link:hover {
|
||||||
|
background: rgba(0, 123, 255, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-link:hover .link-icon {
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误消息 */
|
||||||
|
.error-message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
text-align: center;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message h3 {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(231, 76, 60, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 欢迎消息 */
|
||||||
|
.welcome-message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 60px 40px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
text-align: center;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message h3 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页脚 */
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
margin-top: 40px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板端适配 (768px - 1024px) */
|
||||||
|
@media (max-width: 1024px) and (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
grid-template-columns: 1fr 1.5fr;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机端适配 (最大768px) */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
padding: 12px 20px;
|
||||||
|
min-width: 100px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-description {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-image {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-abstract h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-abstract p {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message {
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message p {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message h3 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小屏手机适配 (最大480px) */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
border-radius: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
border-radius: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-text {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-image {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
83
frontend/react-app/public/60sapi/实用功能/百度百科词条/index.html
Normal file
83
frontend/react-app/public/60sapi/实用功能/百度百科词条/index.html
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<!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/background.css">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1 class="title">百度百科词条查询</h1>
|
||||||
|
<p class="subtitle">探索知识的彩虹世界</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="search-section">
|
||||||
|
<div class="search-container">
|
||||||
|
<input type="text" id="searchInput" class="search-input" placeholder="请输入要查询的词条..." autocomplete="off">
|
||||||
|
<button id="searchBtn" class="search-btn">
|
||||||
|
<span class="search-icon">🔍</span>
|
||||||
|
<span class="search-text">搜索</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-section" id="resultSection">
|
||||||
|
<div class="loading" id="loading" style="display: none;">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>正在搜索中...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-card" id="resultCard" style="display: none;">
|
||||||
|
<div class="result-header">
|
||||||
|
<h2 class="result-title" id="resultTitle"></h2>
|
||||||
|
<p class="result-description" id="resultDescription"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-content">
|
||||||
|
<div class="result-image-container">
|
||||||
|
<img id="resultImage" class="result-image" alt="词条图片">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-text">
|
||||||
|
<div class="result-abstract">
|
||||||
|
<h3>摘要</h3>
|
||||||
|
<p id="resultAbstract"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-actions">
|
||||||
|
<a id="resultLink" class="result-link" target="_blank">
|
||||||
|
<span>查看完整词条</span>
|
||||||
|
<span class="link-icon">→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-message" id="errorMessage" style="display: none;">
|
||||||
|
<div class="error-icon">⚠️</div>
|
||||||
|
<h3>查询失败</h3>
|
||||||
|
<p id="errorText"></p>
|
||||||
|
<button id="retryBtn" class="retry-btn">重试</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="welcome-message" id="welcomeMessage">
|
||||||
|
<div class="welcome-icon">📚</div>
|
||||||
|
<h3>欢迎使用百度百科词条查询</h3>
|
||||||
|
<p>在上方搜索框中输入您想了解的词条,开始探索知识的海洋</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>数据来源:百度百科</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
324
frontend/react-app/public/60sapi/实用功能/百度百科词条/js/script.js
vendored
Normal file
324
frontend/react-app/public/60sapi/实用功能/百度百科词条/js/script.js
vendored
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
// 百度百科词条查询应用
|
||||||
|
class BaikeApp {
|
||||||
|
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.currentApiIndex = 0;
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
this.initElements();
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化DOM元素
|
||||||
|
initElements() {
|
||||||
|
this.searchInput = document.getElementById('searchInput');
|
||||||
|
this.searchBtn = document.getElementById('searchBtn');
|
||||||
|
this.resultSection = document.getElementById('resultSection');
|
||||||
|
this.loading = document.getElementById('loading');
|
||||||
|
this.resultCard = document.getElementById('resultCard');
|
||||||
|
this.errorMessage = document.getElementById('errorMessage');
|
||||||
|
this.welcomeMessage = document.getElementById('welcomeMessage');
|
||||||
|
this.retryBtn = document.getElementById('retryBtn');
|
||||||
|
|
||||||
|
// 结果显示元素
|
||||||
|
this.resultTitle = document.getElementById('resultTitle');
|
||||||
|
this.resultDescription = document.getElementById('resultDescription');
|
||||||
|
this.resultImage = document.getElementById('resultImage');
|
||||||
|
this.resultAbstract = document.getElementById('resultAbstract');
|
||||||
|
this.resultLink = document.getElementById('resultLink');
|
||||||
|
this.errorText = document.getElementById('errorText');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
bindEvents() {
|
||||||
|
// 搜索按钮点击事件
|
||||||
|
this.searchBtn.addEventListener('click', () => {
|
||||||
|
this.handleSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 输入框回车事件
|
||||||
|
this.searchInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
this.handleSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重试按钮事件
|
||||||
|
this.retryBtn.addEventListener('click', () => {
|
||||||
|
this.handleSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 输入框焦点事件
|
||||||
|
this.searchInput.addEventListener('focus', () => {
|
||||||
|
this.searchInput.select();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
async handleSearch() {
|
||||||
|
const query = this.searchInput.value.trim();
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
this.showError('请输入要查询的词条');
|
||||||
|
this.searchInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.searchBaike(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索百科词条
|
||||||
|
async searchBaike(query) {
|
||||||
|
this.showLoading();
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
// 重置API索引
|
||||||
|
this.currentApiIndex = 0;
|
||||||
|
|
||||||
|
const success = await this.tryApiCall(query);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
this.showError('所有API接口都无法访问,请稍后重试');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试API调用
|
||||||
|
async tryApiCall(query) {
|
||||||
|
for (let i = 0; i < this.apiEndpoints.length; i++) {
|
||||||
|
const endpoint = this.apiEndpoints[this.currentApiIndex];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.callApi(endpoint, query);
|
||||||
|
if (result) {
|
||||||
|
this.showResult(result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`API ${endpoint} 调用失败:`, error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换到下一个API
|
||||||
|
this.currentApiIndex = (this.currentApiIndex + 1) % this.apiEndpoints.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API
|
||||||
|
async callApi(endpoint, query) {
|
||||||
|
const url = `${endpoint}/v2/baike?word=${encodeURIComponent(query)}`;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.code === 200 && data.data) {
|
||||||
|
return data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || '未找到相关词条');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
throw new Error('请求超时');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
showLoading() {
|
||||||
|
this.hideAllSections();
|
||||||
|
this.loading.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示搜索结果
|
||||||
|
showResult(data) {
|
||||||
|
this.hideAllSections();
|
||||||
|
|
||||||
|
// 填充数据
|
||||||
|
this.resultTitle.textContent = data.title || '未知标题';
|
||||||
|
this.resultDescription.textContent = data.description || '暂无描述';
|
||||||
|
this.resultAbstract.textContent = data.abstract || '暂无摘要信息';
|
||||||
|
|
||||||
|
// 处理图片
|
||||||
|
if (data.cover) {
|
||||||
|
this.resultImage.src = data.cover;
|
||||||
|
this.resultImage.style.display = 'block';
|
||||||
|
this.resultImage.onerror = () => {
|
||||||
|
this.resultImage.style.display = 'none';
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.resultImage.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理链接
|
||||||
|
if (data.link) {
|
||||||
|
this.resultLink.href = data.link;
|
||||||
|
this.resultLink.style.display = 'inline-flex';
|
||||||
|
} else {
|
||||||
|
this.resultLink.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resultCard.style.display = 'block';
|
||||||
|
|
||||||
|
// 滚动到结果区域
|
||||||
|
this.resultCard.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误信息
|
||||||
|
showError(message) {
|
||||||
|
this.hideAllSections();
|
||||||
|
this.errorText.textContent = message;
|
||||||
|
this.errorMessage.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏所有区域
|
||||||
|
hideAllSections() {
|
||||||
|
this.loading.style.display = 'none';
|
||||||
|
this.resultCard.style.display = 'none';
|
||||||
|
this.errorMessage.style.display = 'none';
|
||||||
|
this.welcomeMessage.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示欢迎信息
|
||||||
|
showWelcome() {
|
||||||
|
this.hideAllSections();
|
||||||
|
this.welcomeMessage.style.display = 'flex';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
class Utils {
|
||||||
|
// 防抖函数
|
||||||
|
static debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节流函数
|
||||||
|
static 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文本长度
|
||||||
|
static truncateText(text, maxLength) {
|
||||||
|
if (text.length <= maxLength) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return text.substring(0, maxLength) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为移动设备
|
||||||
|
static isMobile() {
|
||||||
|
return window.innerWidth <= 768;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后初始化应用
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// 初始化应用
|
||||||
|
const app = new BaikeApp();
|
||||||
|
|
||||||
|
// 添加页面可见性变化监听
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
// 页面重新可见时,聚焦搜索框
|
||||||
|
if (!app.isLoading) {
|
||||||
|
app.searchInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加窗口大小变化监听
|
||||||
|
window.addEventListener('resize', Utils.throttle(() => {
|
||||||
|
// 响应式调整
|
||||||
|
if (Utils.isMobile()) {
|
||||||
|
// 移动端特殊处理
|
||||||
|
document.body.classList.add('mobile');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('mobile');
|
||||||
|
}
|
||||||
|
}, 250));
|
||||||
|
|
||||||
|
// 初始检查设备类型
|
||||||
|
if (Utils.isMobile()) {
|
||||||
|
document.body.classList.add('mobile');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后聚焦搜索框
|
||||||
|
setTimeout(() => {
|
||||||
|
app.searchInput.focus();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// 添加键盘快捷键支持
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
// Ctrl/Cmd + K 聚焦搜索框
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||||
|
e.preventDefault();
|
||||||
|
app.searchInput.focus();
|
||||||
|
app.searchInput.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESC 清空搜索框
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
app.searchInput.value = '';
|
||||||
|
app.showWelcome();
|
||||||
|
app.searchInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('百度百科词条查询应用已初始化');
|
||||||
|
});
|
||||||
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"
|
||||||
|
]
|
||||||
12
frontend/react-app/public/60sapi/实用功能/百度百科词条/返回接口.json
Normal file
12
frontend/react-app/public/60sapi/实用功能/百度百科词条/返回接口.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||||
|
"data": {
|
||||||
|
"title": "西游记",
|
||||||
|
"description": "明代吴承恩创作的章回体长篇神魔小说",
|
||||||
|
"abstract": "《西游记》又名《西游释厄传》,是中国古代第一部浪漫主义章回体长篇神魔小说。最早的《西游记》版本是明代万历二十年金陵世德堂《新刻出像官板大字西游记》,未署作者姓名。鲁迅、董作宾等人根据《淮安府志》“吴承恩《西游记》”的记载予以最终论定“吴承恩原著”。该小说主要讲述了孙悟空出世,并寻菩提祖师学艺及大闹天宫后,与猪八戒、沙僧和白龙马一同护送唐僧西天取经,于路上历经险阻,降妖除魔,渡过了九九八十一难,成功...",
|
||||||
|
"cover": "https://bkimg.cdn.bcebos.com/pic/b7fd5266d01609248d763e43db0735fae6cd3412?x-bce-process=image/format,f_auto",
|
||||||
|
"has_other": true,
|
||||||
|
"link": "http://baike.baidu.com/subview/2583/5315045.htm"
|
||||||
|
}
|
||||||
|
}
|
||||||
388
frontend/react-app/public/60sapi/日更资讯/历史上的今天/css/style.css
Normal file
388
frontend/react-app/public/60sapi/日更资讯/历史上的今天/css/style.css
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
/* 历史上的今天 - 手机端优先的响应式设计 */
|
||||||
|
|
||||||
|
/* 重置样式 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #2c3e50;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部样式 - 手机端优先 */
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px 15px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日期显示 */
|
||||||
|
.date-info {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-info h2 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-info .date-text {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 15px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border: 3px solid #ecf0f1;
|
||||||
|
border-top: 3px solid #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 历史事件容器 */
|
||||||
|
.events-container {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计信息 */
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #667eea;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 事件列表 */
|
||||||
|
.events-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 15px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 事件类型标签 */
|
||||||
|
.event-type {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-type.birth {
|
||||||
|
background: #e8f5e8;
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-type.death {
|
||||||
|
background: #fdf2e9;
|
||||||
|
color: #e67e22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-type.event {
|
||||||
|
background: #ebf3fd;
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 事件年份 */
|
||||||
|
.event-year {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-year::before {
|
||||||
|
content: "📅";
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 事件标题 */
|
||||||
|
.event-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 事件描述 */
|
||||||
|
.event-description {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 链接按钮 */
|
||||||
|
.event-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
border-radius: 15px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-link:hover {
|
||||||
|
background: rgba(102, 126, 234, 0.2);
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-link::after {
|
||||||
|
content: "🔗";
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误提示 */
|
||||||
|
.error {
|
||||||
|
background: #fed7d7;
|
||||||
|
color: #c53030;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #feb2b2;
|
||||||
|
margin: 15px 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏类 */
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 淡入动画 */
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.6s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板端适配 (768px+) */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
max-width: 750px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 30px 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-info {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-info h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-container {
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-description {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 电脑端适配 (1024px+) */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 40px 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card {
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-description {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 大屏幕优化 (1200px+) */
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(102, 126, 234, 0.5);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(102, 126, 234, 0.7);
|
||||||
|
}
|
||||||
83
frontend/react-app/public/60sapi/日更资讯/历史上的今天/index.html
Normal file
83
frontend/react-app/public/60sapi/日更资讯/历史上的今天/index.html
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="历史上的今天 - 了解今天在历史上发生的重要事件">
|
||||||
|
<meta name="keywords" content="历史上的今天,历史事件,百度百科,今日历史">
|
||||||
|
<title>历史上的今天 - 60s API集合</title>
|
||||||
|
|
||||||
|
<!-- 预加载关键资源 -->
|
||||||
|
<link rel="preconnect" href="https://60s.viki.moe">
|
||||||
|
<link rel="dns-prefetch" href="https://60s.viki.moe">
|
||||||
|
|
||||||
|
<!-- 样式文件 -->
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
|
||||||
|
<!-- 图标 -->
|
||||||
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📚</text></svg>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- 页面容器 -->
|
||||||
|
<div class="container">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<header class="header">
|
||||||
|
<h1 class="title">📚 历史上的今天</h1>
|
||||||
|
<p class="subtitle">探索历史,了解今天在历史上发生的重要事件</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 日期信息 -->
|
||||||
|
<section class="date-section" id="date-info">
|
||||||
|
<div class="date-display">
|
||||||
|
<span class="date-text" id="date-text">加载中...</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div class="loading" id="loading">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>正在加载历史数据...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主要内容 -->
|
||||||
|
<main class="main-content" id="history-content" style="display: none;">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 历史事件列表 -->
|
||||||
|
<section class="events-section">
|
||||||
|
<h2 class="section-title">历史事件</h2>
|
||||||
|
<div class="events-container">
|
||||||
|
<div class="events-list" id="events-list">
|
||||||
|
<!-- 事件卡片将通过 JavaScript 动态生成 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript 文件 -->
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
|
||||||
|
<!-- 页面性能监控 -->
|
||||||
|
<script>
|
||||||
|
// 页面加载性能监控
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
if ('performance' in window) {
|
||||||
|
const loadTime = performance.timing.loadEventEnd - performance.timing.navigationStart;
|
||||||
|
console.log(`页面加载时间: ${loadTime}ms`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 错误监控
|
||||||
|
window.addEventListener('error', function(event) {
|
||||||
|
console.error('页面错误:', event.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 未处理的 Promise 错误
|
||||||
|
window.addEventListener('unhandledrejection', function(event) {
|
||||||
|
console.error('未处理的 Promise 错误:', event.reason);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
295
frontend/react-app/public/60sapi/日更资讯/历史上的今天/js/script.js
vendored
Normal file
295
frontend/react-app/public/60sapi/日更资讯/历史上的今天/js/script.js
vendored
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
// 历史上的今天 - JavaScript 功能实现
|
||||||
|
|
||||||
|
// API 配置
|
||||||
|
const API = {
|
||||||
|
endpoints: [],
|
||||||
|
currentIndex: 0,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
// 初始化API接口列表
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('./接口集合.json');
|
||||||
|
const endpoints = await res.json();
|
||||||
|
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/today_in_history`);
|
||||||
|
} catch (e) {
|
||||||
|
// 如果无法加载接口集合,使用默认接口
|
||||||
|
this.endpoints = ['https://60s.viki.moe/v2/today_in_history'];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 获取当前接口URL
|
||||||
|
getCurrentUrl() {
|
||||||
|
if (this.endpoints.length === 0) return null;
|
||||||
|
const url = new URL(this.endpoints[this.currentIndex]);
|
||||||
|
url.searchParams.append('encoding', this.encoding);
|
||||||
|
return url.toString();
|
||||||
|
},
|
||||||
|
// 切换到下一个接口
|
||||||
|
switchToNext() {
|
||||||
|
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length;
|
||||||
|
return this.currentIndex < this.endpoints.length;
|
||||||
|
},
|
||||||
|
// 重置到第一个接口
|
||||||
|
reset() {
|
||||||
|
this.currentIndex = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 事件类型映射
|
||||||
|
const EVENT_TYPE_MAP = {
|
||||||
|
'birth': { name: '诞生', icon: '🎂', color: '#27ae60' },
|
||||||
|
'death': { name: '逝世', icon: '🕊️', color: '#e67e22' },
|
||||||
|
'event': { name: '事件', icon: '📰', color: '#3498db' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM 元素
|
||||||
|
let elements = {};
|
||||||
|
let currentData = null;
|
||||||
|
|
||||||
|
// 页面加载完成后初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initElements();
|
||||||
|
loadTodayInHistory();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化 DOM 元素
|
||||||
|
function initElements() {
|
||||||
|
elements = {
|
||||||
|
loading: document.getElementById('loading'),
|
||||||
|
content: document.getElementById('history-content'),
|
||||||
|
dateInfo: document.getElementById('date-info'),
|
||||||
|
dateText: document.getElementById('date-text'),
|
||||||
|
totalEvents: document.getElementById('total-events'),
|
||||||
|
birthEvents: document.getElementById('birth-events'),
|
||||||
|
deathEvents: document.getElementById('death-events'),
|
||||||
|
otherEvents: document.getElementById('other-events'),
|
||||||
|
eventsList: document.getElementById('events-list')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载历史上的今天数据
|
||||||
|
async function loadTodayInHistory() {
|
||||||
|
try {
|
||||||
|
showLoading(true);
|
||||||
|
|
||||||
|
// 初始化API接口列表
|
||||||
|
await API.init();
|
||||||
|
|
||||||
|
// 重置API索引到第一个接口
|
||||||
|
API.reset();
|
||||||
|
|
||||||
|
// 尝试所有API接口
|
||||||
|
for (let i = 0; i < API.endpoints.length; i++) {
|
||||||
|
try {
|
||||||
|
const url = API.getCurrentUrl();
|
||||||
|
console.log(`尝试接口 ${i + 1}/${API.endpoints.length}: ${url}`);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
cache: 'no-store',
|
||||||
|
timeout: 10000 // 10秒超时
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('API响应数据:', data);
|
||||||
|
|
||||||
|
if (data.code === 200 && data.data) {
|
||||||
|
console.log(`接口 ${i + 1} 请求成功`);
|
||||||
|
currentData = data.data;
|
||||||
|
displayHistoryData(data.data);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || '获取数据失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`接口 ${i + 1} 失败:`, error.message);
|
||||||
|
|
||||||
|
// 如果不是最后一个接口,切换到下一个
|
||||||
|
if (i < API.endpoints.length - 1) {
|
||||||
|
API.switchToNext();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有接口都失败了,抛出错误
|
||||||
|
throw new Error('所有接口都无法访问');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载历史数据失败:', error);
|
||||||
|
showError(`加载失败: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
showLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示历史数据
|
||||||
|
function displayHistoryData(data) {
|
||||||
|
if (!data || !data.items) {
|
||||||
|
showError('没有获取到历史数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新日期信息
|
||||||
|
updateDateInfo(data);
|
||||||
|
|
||||||
|
// 更新统计信息
|
||||||
|
updateStats(data.items);
|
||||||
|
|
||||||
|
// 显示事件列表
|
||||||
|
renderEventsList(data.items);
|
||||||
|
|
||||||
|
// 显示内容区域
|
||||||
|
if (elements.content) {
|
||||||
|
elements.content.classList.add('fade-in');
|
||||||
|
elements.content.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新日期信息
|
||||||
|
function updateDateInfo(data) {
|
||||||
|
if (elements.dateText && data.date) {
|
||||||
|
const today = new Date();
|
||||||
|
const year = today.getFullYear();
|
||||||
|
elements.dateText.textContent = `${year}年${data.month}月${data.day}日`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新统计信息
|
||||||
|
function updateStats(items) {
|
||||||
|
const stats = {
|
||||||
|
total: items.length,
|
||||||
|
birth: items.filter(item => item.event_type === 'birth').length,
|
||||||
|
death: items.filter(item => item.event_type === 'death').length,
|
||||||
|
event: items.filter(item => item.event_type === 'event').length
|
||||||
|
};
|
||||||
|
|
||||||
|
if (elements.totalEvents) {
|
||||||
|
elements.totalEvents.textContent = stats.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.birthEvents) {
|
||||||
|
elements.birthEvents.textContent = stats.birth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.deathEvents) {
|
||||||
|
elements.deathEvents.textContent = stats.death;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.otherEvents) {
|
||||||
|
elements.otherEvents.textContent = stats.event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染事件列表
|
||||||
|
function renderEventsList(items) {
|
||||||
|
if (!elements.eventsList || !items) return;
|
||||||
|
|
||||||
|
// 按年份排序(从今到古)
|
||||||
|
const sortedItems = [...items].sort((a, b) => {
|
||||||
|
return parseInt(b.year) - parseInt(a.year);
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.eventsList.innerHTML = '';
|
||||||
|
|
||||||
|
sortedItems.forEach(item => {
|
||||||
|
const eventCard = createEventCard(item);
|
||||||
|
elements.eventsList.appendChild(eventCard);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建事件卡片
|
||||||
|
function createEventCard(item) {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'event-card';
|
||||||
|
|
||||||
|
const eventType = EVENT_TYPE_MAP[item.event_type] || EVENT_TYPE_MAP['event'];
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="event-type ${item.event_type}">${eventType.name}</div>
|
||||||
|
<div class="event-year">${formatYear(item.year)}</div>
|
||||||
|
<div class="event-title">${escapeHtml(item.title)}</div>
|
||||||
|
<div class="event-description">${escapeHtml(item.description)}</div>
|
||||||
|
${item.link ? `<a href="${escapeHtml(item.link)}" target="_blank" rel="noopener noreferrer" class="event-link">了解更多</a>` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化年份显示
|
||||||
|
function formatYear(year) {
|
||||||
|
const yearNum = parseInt(year);
|
||||||
|
if (yearNum < 0) {
|
||||||
|
return `公元前${Math.abs(yearNum)}年`;
|
||||||
|
} else if (yearNum < 1000) {
|
||||||
|
return `公元${yearNum}年`;
|
||||||
|
} else {
|
||||||
|
return `${yearNum}年`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
function showLoading(show) {
|
||||||
|
if (elements.loading) {
|
||||||
|
elements.loading.style.display = show ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.content) {
|
||||||
|
elements.content.style.display = show ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误信息
|
||||||
|
function showError(message) {
|
||||||
|
if (elements.content) {
|
||||||
|
elements.content.innerHTML = `
|
||||||
|
<div class="error">
|
||||||
|
<h3>😔 加载失败</h3>
|
||||||
|
<p>${escapeHtml(message)}</p>
|
||||||
|
<button onclick="loadTodayInHistory()" style="
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
">重新加载</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
elements.content.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML 转义
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
window.addEventListener('error', function(event) {
|
||||||
|
console.error('页面错误:', event.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 网络状态监听
|
||||||
|
window.addEventListener('online', function() {
|
||||||
|
console.log('网络已连接');
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('offline', function() {
|
||||||
|
console.log('网络已断开');
|
||||||
|
showError('网络连接已断开,请检查网络设置');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导出全局方法
|
||||||
|
window.TodayInHistory = {
|
||||||
|
loadTodayInHistory,
|
||||||
|
showError,
|
||||||
|
showLoading
|
||||||
|
};
|
||||||
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"
|
||||||
|
]
|
||||||
102
frontend/react-app/public/60sapi/日更资讯/历史上的今天/返回接口.json
Normal file
102
frontend/react-app/public/60sapi/日更资讯/历史上的今天/返回接口.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||||
|
"data": {
|
||||||
|
"date": "8-19",
|
||||||
|
"month": 8,
|
||||||
|
"day": 19,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "罗马帝国开国皇帝奥古斯都逝世",
|
||||||
|
"year": "14",
|
||||||
|
"description": "奥古斯都(拉丁文 Augustus的中译,复数型 Augusti)的原意为“神圣的”、“高贵的”,带有宗教与神学式的意味。",
|
||||||
|
"event_type": "death",
|
||||||
|
"link": "https://baike.baidu.com/item/%E5%A5%A5%E5%8F%A4%E6%96%AF%E9%83%BD/14291"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "近代概率论的奠基者帕斯卡逝世",
|
||||||
|
"year": "1662",
|
||||||
|
"description": "布莱士·帕斯卡(Blaise Pascal ,1623-1662)是法国数学家、物理学家、哲学家、散文家。",
|
||||||
|
"event_type": "death",
|
||||||
|
"link": "https://baike.baidu.com/item/%E5%B8%83%E8%8E%B1%E5%A3%AB%C2%B7%E5%B8%95%E6%96%AF%E5%8D%A1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "瑞典国王古斯塔夫三世发动政变夺取权力",
|
||||||
|
"year": "1772",
|
||||||
|
"description": "古斯塔夫三世(Gustavus III,1746-1792)是瑞典历史上褒贬最多的国王(1771-1792)。阿道夫·弗里德里克国王的儿子和继承者。",
|
||||||
|
"event_type": "event",
|
||||||
|
"link": "https://baike.baidu.com/item/%E5%8F%A4%E6%96%AF%E5%A1%94%E5%A4%AB%E4%B8%89%E4%B8%96"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "美国飞机设计师奥维尔·莱特诞生",
|
||||||
|
"year": "1871",
|
||||||
|
"description": "奥威尔莱特(公元1871~公元1948)。 奥维尔·莱特1871年生于美国俄亥俄州代顿市。上过中学,但实际上未获得毕业文凭。",
|
||||||
|
"event_type": "birth",
|
||||||
|
"link": "https://baike.baidu.com/item/%E5%A5%A5%E7%BB%B4%E5%B0%94%C2%B7%E8%8E%B1%E7%89%B9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "法国著名时装设计师、香奈儿品牌创始人加布里埃·香奈儿出生",
|
||||||
|
"year": "1883",
|
||||||
|
"description": "香奈儿儿时入读修女院学校学得一手针线活。后来她与许多上流社会男士有过交往。1910年,毅然放弃嫁入豪门做阔太太的她在巴黎开设了一家女装帽子店,从此开创了香奈儿时尚帝国。",
|
||||||
|
"event_type": "birth",
|
||||||
|
"link": "https://baike.baidu.com/item/%E5%8A%A0%E5%B8%83%E9%87%8C%E5%9F%83%C2%B7%E9%A6%99%E5%A5%88%E5%84%BF/9480318"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "美国宇航员斯托里·马斯格雷夫出生",
|
||||||
|
"year": "1935",
|
||||||
|
"description": "斯托里·马斯格雷夫(Franklin Story Musgrave,1935年8月19日-),美国宇航员,拥有医学、数学、文学等六个学位,入选美国国家航空航天局(NASA)科学家宇航员。",
|
||||||
|
"event_type": "birth",
|
||||||
|
"link": "https://baike.baidu.com/item/%E6%96%AF%E6%89%98%E9%87%8C%C2%B7%E9%A9%AC%E6%96%AF%E6%A0%BC%E9%9B%B7%E5%A4%AB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "纳粹德国陆军元帅京特·冯·克鲁格畏罪自杀",
|
||||||
|
"year": "1944",
|
||||||
|
"description": "汉斯·京特·冯·克卢格(Günther·von·Kluge, 1882年10月30日-1944年8月19日),纳粹德国陆军元帅(1940.7.19),著名军事家、统帅。",
|
||||||
|
"event_type": "death",
|
||||||
|
"link": "https://baike.baidu.com/item/%E4%BA%AC%E7%89%B9%C2%B7%E5%86%AF%C2%B7%E5%85%8B%E9%B2%81%E6%A0%BC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "美国第42任总统克林顿出生",
|
||||||
|
"year": "1946",
|
||||||
|
"description": "威廉·杰斐逊·克林顿,美国律师、政治家,美国民主党成员,曾任阿肯色州州长和第42任美国总统。克林顿基金会主席 。",
|
||||||
|
"event_type": "birth",
|
||||||
|
"link": "https://baike.baidu.com/item/%E5%A8%81%E5%BB%89%C2%B7%E6%9D%B0%E6%96%90%E9%80%8A%C2%B7%E5%85%8B%E6%9E%97%E9%A1%BF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "美国演员马修·派瑞出生",
|
||||||
|
"year": "1969",
|
||||||
|
"description": "马修·派瑞(Matthew Perry,1969年8月19日—2023年10月28日),出生于美国马萨诸塞州普利茅斯,美国、加拿大籍男演员、编剧。",
|
||||||
|
"event_type": "birth",
|
||||||
|
"link": "https://baike.baidu.com/item/%E9%A9%AC%E4%BF%AE%C2%B7%E6%B4%BE%E7%91%9E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "北回归线标志塔在广州落成",
|
||||||
|
"year": "1985",
|
||||||
|
"description": "北回归线标志塔,是标志地理学上北回归线经过地方的建筑物。",
|
||||||
|
"event_type": "event",
|
||||||
|
"link": "https://baike.baidu.com/item/%E5%8C%97%E5%9B%9E%E5%BD%92%E7%BA%BF%E6%A0%87%E5%BF%97%E5%A1%94"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "“八一九事件”,苏联八月政变",
|
||||||
|
"year": "1991",
|
||||||
|
"description": "八一九事件,又称“苏联政变”、“八月政变”,指1991年8月19日-8月21日在苏联发生的一次政变。",
|
||||||
|
"event_type": "event",
|
||||||
|
"link": "https://baike.baidu.com/item/%E5%85%AB%E4%B8%80%E4%B9%9D%E4%BA%8B%E4%BB%B6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "量子化学家莱纳斯·卡尔·鲍林逝世",
|
||||||
|
"year": "1994",
|
||||||
|
"description": "莱纳斯·卡尔·鲍林(Linus Carl Pauling,1901年2月28日—1994年8月19日),出生于美国俄勒冈州波特兰,化学家、美国国家科学院院士、美国艺术与科学院院士,1954年诺贝尔化学奖获得者。",
|
||||||
|
"event_type": "death",
|
||||||
|
"link": "https://baike.baidu.com/item/%E8%8E%B1%E7%BA%B3%E6%96%AF%C2%B7%E5%8D%A1%E5%B0%94%C2%B7%E9%B2%8D%E6%9E%97"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "中国三江源自然保护区成立",
|
||||||
|
"year": "2000",
|
||||||
|
"description": "青海三江源国家级自然保护区位于青藏高原腹地,青海省南部,地理位置介于东经89°24′~102°23′,北纬31°39′~36°16′之间,青海三江源国家级自然保护区属湿地类型的自然保护区。",
|
||||||
|
"event_type": "event",
|
||||||
|
"link": "https://baike.baidu.com/item/%E4%B8%89%E6%B1%9F%E6%BA%90%E8%87%AA%E7%84%B6%E4%BF%9D%E6%8A%A4%E5%8C%BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
326
frontend/react-app/public/60sapi/日更资讯/必应每日壁纸/css/style.css
Normal file
326
frontend/react-app/public/60sapi/日更资讯/必应每日壁纸/css/style.css
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
/* 必应每日壁纸 - 淡绿色清新风格样式 */
|
||||||
|
|
||||||
|
/* 重置样式 */
|
||||||
|
* {
|
||||||
|
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: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部样式 */
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #2d5016;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #e8f5e8;
|
||||||
|
border-top: 4px solid #81c784;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 壁纸容器 */
|
||||||
|
.wallpaper-container {
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 壁纸信息 */
|
||||||
|
.wallpaper-info {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2d5016;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-date {
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-description {
|
||||||
|
color: #2d5016;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 壁纸图片 */
|
||||||
|
.wallpaper-image {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 10px 30px rgba(45, 80, 22, 0.15);
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-image:hover img {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下载按钮 */
|
||||||
|
.download-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 15px 30px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(129, 199, 132, 0.3);
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(129, 199, 132, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误提示 */
|
||||||
|
.error {
|
||||||
|
background: #fed7d7;
|
||||||
|
color: #c53030;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #feb2b2;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 版权信息 */
|
||||||
|
.copyright {
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
|
||||||
|
/* 平板端 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-description {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机端 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-title {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-description {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
padding: 12px 25px;
|
||||||
|
font-size: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 大屏幕优化 */
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.container {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-container {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-image {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特殊效果 */
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.6s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片加载效果 */
|
||||||
|
.wallpaper-image img {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallpaper-image img.loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(129, 199, 132, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(129, 199, 132, 0.7);
|
||||||
|
}
|
||||||
42
frontend/react-app/public/60sapi/日更资讯/必应每日壁纸/index.html
Normal file
42
frontend/react-app/public/60sapi/日更资讯/必应每日壁纸/index.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="必应每日壁纸 - 每天为您呈现精美的必应壁纸">
|
||||||
|
<meta name="keywords" content="必应壁纸,每日壁纸,高清壁纸,桌面壁纸">
|
||||||
|
<title>必应每日壁纸</title>
|
||||||
|
|
||||||
|
<!-- 引入样式文件 -->
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
|
||||||
|
<!-- 网站图标 -->
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🖼️</text></svg>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<header class="header">
|
||||||
|
<h1>
|
||||||
|
<span>🖼️</span>
|
||||||
|
必应每日壁纸
|
||||||
|
</h1>
|
||||||
|
<p>每天为您呈现精美的必应壁纸,发现世界之美</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div id="loading" class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>正在加载今日壁纸...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 壁纸内容区域 -->
|
||||||
|
<main id="wallpaper-content" class="content" style="display: none;">
|
||||||
|
<!-- 壁纸内容将通过JavaScript动态加载 -->
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 引入脚本文件 -->
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
315
frontend/react-app/public/60sapi/日更资讯/必应每日壁纸/js/script.js
vendored
Normal file
315
frontend/react-app/public/60sapi/日更资讯/必应每日壁纸/js/script.js
vendored
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
// 必应每日壁纸 JavaScript 功能
|
||||||
|
|
||||||
|
// API配置
|
||||||
|
const API = {
|
||||||
|
endpoints: [],
|
||||||
|
currentIndex: 0,
|
||||||
|
params: {
|
||||||
|
encoding: 'json'
|
||||||
|
},
|
||||||
|
// 初始化API接口列表
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('./接口集合.json');
|
||||||
|
const endpoints = await res.json();
|
||||||
|
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/bing`);
|
||||||
|
} catch (e) {
|
||||||
|
// 如果无法加载接口集合,使用默认接口
|
||||||
|
this.endpoints = ['https://60s.viki.moe/v2/bing'];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 获取当前接口URL
|
||||||
|
getCurrentUrl() {
|
||||||
|
if (this.endpoints.length === 0) return null;
|
||||||
|
const url = new URL(this.endpoints[this.currentIndex]);
|
||||||
|
Object.keys(this.params).forEach(key => {
|
||||||
|
url.searchParams.append(key, this.params[key]);
|
||||||
|
});
|
||||||
|
return url.toString();
|
||||||
|
},
|
||||||
|
// 切换到下一个接口
|
||||||
|
switchToNext() {
|
||||||
|
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length;
|
||||||
|
return this.currentIndex < this.endpoints.length;
|
||||||
|
},
|
||||||
|
// 重置到第一个接口
|
||||||
|
reset() {
|
||||||
|
this.currentIndex = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM元素
|
||||||
|
let elements = {};
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initElements();
|
||||||
|
loadWallpaper();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化DOM元素
|
||||||
|
function initElements() {
|
||||||
|
elements = {
|
||||||
|
container: document.getElementById('wallpaper-content'),
|
||||||
|
loading: document.getElementById('loading')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载壁纸数据
|
||||||
|
async function loadWallpaper() {
|
||||||
|
try {
|
||||||
|
showLoading(true);
|
||||||
|
|
||||||
|
// 初始化API接口列表
|
||||||
|
await API.init();
|
||||||
|
|
||||||
|
// 重置API索引到第一个接口
|
||||||
|
API.reset();
|
||||||
|
|
||||||
|
// 尝试所有API接口
|
||||||
|
for (let i = 0; i < API.endpoints.length; i++) {
|
||||||
|
try {
|
||||||
|
const url = API.getCurrentUrl();
|
||||||
|
console.log(`尝试接口 ${i + 1}/${API.endpoints.length}: ${url}`);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
cache: 'no-store',
|
||||||
|
timeout: 10000 // 10秒超时
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('API响应数据:', data);
|
||||||
|
|
||||||
|
// 检查数据有效性
|
||||||
|
if (data && (data.code === 200 || data.data)) {
|
||||||
|
console.log(`接口 ${i + 1} 请求成功`);
|
||||||
|
displayWallpaper(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(data && data.message ? data.message : '接口返回异常');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`接口 ${i + 1} 失败:`, error.message);
|
||||||
|
|
||||||
|
// 如果不是最后一个接口,切换到下一个
|
||||||
|
if (i < API.endpoints.length - 1) {
|
||||||
|
API.switchToNext();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有接口都失败了,抛出错误
|
||||||
|
throw new Error('所有接口都无法访问');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载壁纸失败:', error);
|
||||||
|
showError('加载壁纸失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
showLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示壁纸
|
||||||
|
function displayWallpaper(data) {
|
||||||
|
if (!data) {
|
||||||
|
showError('没有获取到壁纸数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取壁纸信息
|
||||||
|
const wallpaperInfo = extractWallpaperInfo(data);
|
||||||
|
|
||||||
|
if (!wallpaperInfo || !wallpaperInfo.imageUrl) {
|
||||||
|
showError('壁纸图片链接无效');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成HTML内容
|
||||||
|
const html = generateWallpaperHTML(wallpaperInfo);
|
||||||
|
|
||||||
|
// 显示内容
|
||||||
|
elements.container.innerHTML = html;
|
||||||
|
elements.container.classList.add('fade-in');
|
||||||
|
|
||||||
|
// 绑定图片加载事件
|
||||||
|
bindImageEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取壁纸信息
|
||||||
|
function extractWallpaperInfo(data) {
|
||||||
|
// 根据API响应结构提取信息
|
||||||
|
let imageUrl = '';
|
||||||
|
let title = '必应每日壁纸';
|
||||||
|
let description = '';
|
||||||
|
let date = new Date().toLocaleDateString('zh-CN');
|
||||||
|
let copyright = '';
|
||||||
|
|
||||||
|
// 处理新的API响应格式
|
||||||
|
if (data.data) {
|
||||||
|
const wallpaperData = data.data;
|
||||||
|
title = wallpaperData.title || title;
|
||||||
|
description = wallpaperData.description || wallpaperData.main_text || '';
|
||||||
|
copyright = wallpaperData.copyright || '';
|
||||||
|
date = wallpaperData.update_date || date;
|
||||||
|
|
||||||
|
// 提取图片URL,去除反引号
|
||||||
|
if (wallpaperData.cover) {
|
||||||
|
imageUrl = wallpaperData.cover.replace(/`/g, '').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理其他可能的API响应格式
|
||||||
|
else if (data.url) {
|
||||||
|
imageUrl = data.url;
|
||||||
|
} else if (data.image_url) {
|
||||||
|
imageUrl = data.image_url;
|
||||||
|
} else if (data.images && data.images.length > 0) {
|
||||||
|
imageUrl = data.images[0].url || data.images[0].image_url;
|
||||||
|
title = data.images[0].title || title;
|
||||||
|
description = data.images[0].description || data.images[0].copyright || '';
|
||||||
|
copyright = data.images[0].copyright || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是相对路径,转换为完整URL
|
||||||
|
if (imageUrl && imageUrl.startsWith('/')) {
|
||||||
|
imageUrl = 'https://www.bing.com' + imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保图片URL有效
|
||||||
|
if (!imageUrl || imageUrl === '') {
|
||||||
|
console.error('无法提取图片URL,原始数据:', data);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
imageUrl,
|
||||||
|
title,
|
||||||
|
description: description || copyright,
|
||||||
|
date,
|
||||||
|
copyright
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成壁纸HTML
|
||||||
|
function generateWallpaperHTML(info) {
|
||||||
|
return `
|
||||||
|
<div class="wallpaper-container">
|
||||||
|
<div class="wallpaper-info">
|
||||||
|
<h2 class="wallpaper-title">${escapeHtml(info.title)}</h2>
|
||||||
|
<div class="wallpaper-date">${info.date}</div>
|
||||||
|
${info.description ? `<div class="wallpaper-description">${escapeHtml(info.description)}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wallpaper-image">
|
||||||
|
<img src="${info.imageUrl}" alt="${escapeHtml(info.title)}" loading="lazy">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="download-section">
|
||||||
|
<a href="${info.imageUrl}" class="download-btn" download="bing-wallpaper-${info.date}.jpg" target="_blank">
|
||||||
|
<span>📥</span>
|
||||||
|
下载壁纸
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${info.copyright ? `
|
||||||
|
<div class="copyright">
|
||||||
|
<p>${escapeHtml(info.copyright)}</p>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定图片事件
|
||||||
|
function bindImageEvents() {
|
||||||
|
const images = elements.container.querySelectorAll('img');
|
||||||
|
|
||||||
|
images.forEach(img => {
|
||||||
|
img.addEventListener('load', function() {
|
||||||
|
this.classList.add('loaded');
|
||||||
|
});
|
||||||
|
|
||||||
|
img.addEventListener('error', function() {
|
||||||
|
console.error('图片加载失败:', this.src);
|
||||||
|
this.parentElement.innerHTML = `
|
||||||
|
<div class="error">
|
||||||
|
<p>🖼️ 图片加载失败</p>
|
||||||
|
<p>请检查网络连接或稍后重试</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示/隐藏加载状态
|
||||||
|
function showLoading(show) {
|
||||||
|
if (elements.loading) {
|
||||||
|
elements.loading.style.display = show ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
if (elements.container) {
|
||||||
|
elements.container.style.display = show ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误信息
|
||||||
|
function showError(message) {
|
||||||
|
if (elements.container) {
|
||||||
|
elements.container.innerHTML = `
|
||||||
|
<div class="error">
|
||||||
|
<h3>⚠️ 加载失败</h3>
|
||||||
|
<p>${escapeHtml(message)}</p>
|
||||||
|
<p>请检查网络连接或稍后重试</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
elements.container.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML转义
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
function formatDate(dateString) {
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
window.addEventListener('error', function(event) {
|
||||||
|
console.error('页面错误:', event.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 网络状态监听
|
||||||
|
window.addEventListener('online', function() {
|
||||||
|
console.log('网络已连接');
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('offline', function() {
|
||||||
|
console.log('网络已断开');
|
||||||
|
showError('网络连接已断开,请检查网络设置');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导出函数供外部调用
|
||||||
|
window.BingWallpaper = {
|
||||||
|
loadWallpaper,
|
||||||
|
showError,
|
||||||
|
showLoading
|
||||||
|
};
|
||||||
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"
|
||||||
|
]
|
||||||
15
frontend/react-app/public/60sapi/日更资讯/必应每日壁纸/返回接口.json
Normal file
15
frontend/react-app/public/60sapi/日更资讯/必应每日壁纸/返回接口.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||||
|
"data": {
|
||||||
|
"title": "瑟沃格湖,瓦加尔岛,法罗群岛",
|
||||||
|
"headline": "海洋上方的湖泊",
|
||||||
|
"description": "大自然自有其奇妙之处,瑟沃格湖(Sørvágsvatn)便是其中最精彩的之一。世界湖泊日是探索法罗群岛(丹麦王国的一个自治行政区)这片视错觉的绝佳时机。这座位于沃格岛上的湖泊也被称为莱蒂斯湖(Leitisvatn),看似漂浮在海平面之上。实际上,它的海拔不到100英尺。索尔瓦格斯湖是法罗群岛最大的湖泊,面积约1.3平方英里,为Bøsdalafossur瀑布Bøsdalafossur提供水源,瀑布的湖水在那里奔腾而下,最终倾泻而入大海。",
|
||||||
|
"main_text": "该湖位于瓦加尔岛南部,通过Bøsdalafossur瀑布与大西洋相连,形成了壮丽的“悬湖”景观。",
|
||||||
|
"cover": "https://bing.com/th?id=OHR.FaroeLake_ZH-CN3977660997_1920x1080.jpg",
|
||||||
|
"cover_4k": "https://bing.com/th?id=OHR.FaroeLake_ZH-CN3977660997_UHD.jpg",
|
||||||
|
"copyright": "© Anton Petrus/Getty Images",
|
||||||
|
"update_date": "2025-08-27 13:24:37",
|
||||||
|
"update_date_at": 1756301077809
|
||||||
|
}
|
||||||
|
}
|
||||||
327
frontend/react-app/public/60sapi/日更资讯/每天60s读懂世界/css/style.css
Normal file
327
frontend/react-app/public/60sapi/日更资讯/每天60s读懂世界/css/style.css
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
/* 每天60s读懂世界 - 清新风格样式 */
|
||||||
|
|
||||||
|
/* 重置样式 */
|
||||||
|
* {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部样式 */
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #2d5016;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 控制面板 */
|
||||||
|
.controls {
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-selector label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-selector input {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-selector input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #81c784;
|
||||||
|
box-shadow: 0 0 0 3px rgba(129, 199, 132, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(129, 199, 132, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(129, 199, 132, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #e8f5e8;
|
||||||
|
border-top: 4px solid #81c784;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容区域 */
|
||||||
|
.content {
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-date {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lunar-date {
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新闻图片 */
|
||||||
|
.news-image {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin: 20px auto;
|
||||||
|
display: block;
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新闻列表 */
|
||||||
|
.news-list {
|
||||||
|
margin: 25px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-item {
|
||||||
|
background: #f1f8e9;
|
||||||
|
border-left: 4px solid #81c784;
|
||||||
|
padding: 15px 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: 0 10px 10px 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-item:hover {
|
||||||
|
background: #e8f5e8;
|
||||||
|
transform: translateX(5px);
|
||||||
|
box-shadow: 0 4px 15px rgba(45, 80, 22, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-item::before {
|
||||||
|
content: counter(news-counter);
|
||||||
|
counter-increment: news-counter;
|
||||||
|
position: absolute;
|
||||||
|
left: -15px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: #81c784;
|
||||||
|
color: white;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-list {
|
||||||
|
counter-reset: news-counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 每日一句 */
|
||||||
|
.daily-tip {
|
||||||
|
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin: 25px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #744210;
|
||||||
|
box-shadow: 0 5px 20px rgba(252, 182, 159, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误提示 */
|
||||||
|
.error {
|
||||||
|
background: #fed7d7;
|
||||||
|
color: #c53030;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #feb2b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
|
||||||
|
/* 平板端 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-selector {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机端 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-item {
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-item::before {
|
||||||
|
left: -10px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-tip {
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 大屏幕优化 */
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.container {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
frontend/react-app/public/60sapi/日更资讯/每天60s读懂世界/index.html
Normal file
49
frontend/react-app/public/60sapi/日更资讯/每天60s读懂世界/index.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="每天60秒读懂世界 - 获取最新资讯,了解天下大事">
|
||||||
|
<meta name="keywords" content="新闻,资讯,每日新闻,60秒读懂世界">
|
||||||
|
<title>每天60秒读懂世界 | 最新资讯</title>
|
||||||
|
|
||||||
|
<!-- 引入CSS样式 -->
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
|
||||||
|
<!-- 网站图标 -->
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📰</text></svg>">
|
||||||
|
|
||||||
|
<!-- Open Graph 元数据 -->
|
||||||
|
<meta property="og:title" content="每天60秒读懂世界">
|
||||||
|
<meta property="og:description" content="获取最新资讯,了解天下大事">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<header class="header">
|
||||||
|
<h1>📰 每天60秒读懂世界</h1>
|
||||||
|
<p>获取最新资讯,了解天下大事</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<main id="content" class="content">
|
||||||
|
<!-- 内容将通过JavaScript动态加载 -->
|
||||||
|
<div class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>正在加载今日资讯...</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 页面底部 -->
|
||||||
|
<footer style="text-align: center; padding: 20px; color: rgba(255,255,255,0.8); font-size: 0.9rem;">
|
||||||
|
<p>Made with ❤️ | 数据来源:每天60秒读懂世界API</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- 引入JavaScript -->
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
305
frontend/react-app/public/60sapi/日更资讯/每天60s读懂世界/js/script.js
vendored
Normal file
305
frontend/react-app/public/60sapi/日更资讯/每天60s读懂世界/js/script.js
vendored
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
// 每天60s读懂世界 - JavaScript功能实现
|
||||||
|
|
||||||
|
const API = {
|
||||||
|
endpoints: [],
|
||||||
|
currentIndex: 0,
|
||||||
|
params: {
|
||||||
|
encoding: 'json'
|
||||||
|
},
|
||||||
|
localFallback: '返回接口.json',
|
||||||
|
// 初始化API接口列表
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('./接口集合.json');
|
||||||
|
const endpoints = await res.json();
|
||||||
|
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/60s`);
|
||||||
|
} catch (e) {
|
||||||
|
// 如果无法加载接口集合,使用默认接口
|
||||||
|
this.endpoints = ['https://60s.viki.moe/v2/60s'];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 获取当前接口URL
|
||||||
|
getCurrentUrl() {
|
||||||
|
if (this.endpoints.length === 0) return null;
|
||||||
|
const url = new URL(this.endpoints[this.currentIndex]);
|
||||||
|
Object.entries(this.params).forEach(([k, v]) => url.searchParams.append(k, v));
|
||||||
|
return url.toString();
|
||||||
|
},
|
||||||
|
// 切换到下一个接口
|
||||||
|
switchToNext() {
|
||||||
|
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length;
|
||||||
|
return this.currentIndex < this.endpoints.length;
|
||||||
|
},
|
||||||
|
// 重置到第一个接口
|
||||||
|
reset() {
|
||||||
|
this.currentIndex = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class NewsApp {
|
||||||
|
constructor() {
|
||||||
|
this.apiUrl = 'https://60s.viki.moe/v2/60s';
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.bindEvents();
|
||||||
|
this.loadTodayNews();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// 移除了刷新按钮,不需要绑定事件
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading() {
|
||||||
|
const contentDiv = document.getElementById('content');
|
||||||
|
if (contentDiv) {
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<div class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>正在获取最新资讯...</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
const contentDiv = document.getElementById('content');
|
||||||
|
if (contentDiv) {
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<div class="error">
|
||||||
|
<h3>😔 获取失败</h3>
|
||||||
|
<p>${message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadNews() {
|
||||||
|
try {
|
||||||
|
this.showLoading();
|
||||||
|
|
||||||
|
// 尝试从API获取数据
|
||||||
|
let data = await this.fetchFromAPI();
|
||||||
|
|
||||||
|
// 如果API失败,尝试本地数据
|
||||||
|
if (!data) {
|
||||||
|
data = await this.fetchFromLocal();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
throw new Error('无法获取数据,请检查网络连接或稍后重试');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.code !== 200) {
|
||||||
|
throw new Error(data.message || '获取数据失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderNews(data.data);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取新闻失败:', error);
|
||||||
|
this.showError(error.message || '网络连接失败,请检查网络后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchFromAPI() {
|
||||||
|
// 初始化API接口列表
|
||||||
|
await API.init();
|
||||||
|
|
||||||
|
// 重置API索引到第一个接口
|
||||||
|
API.reset();
|
||||||
|
|
||||||
|
// 尝试所有API接口
|
||||||
|
for (let i = 0; i < API.endpoints.length; i++) {
|
||||||
|
try {
|
||||||
|
const url = API.getCurrentUrl();
|
||||||
|
console.log(`尝试接口 ${i + 1}/${API.endpoints.length}: ${url}`);
|
||||||
|
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
cache: 'no-store'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
if (data && data.code === 200) {
|
||||||
|
console.log(`接口 ${i + 1} 请求成功`);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(data && data.message ? data.message : '接口返回异常');
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`接口 ${i + 1} 失败:`, e.message);
|
||||||
|
|
||||||
|
// 如果不是最后一个接口,切换到下一个
|
||||||
|
if (i < API.endpoints.length - 1) {
|
||||||
|
API.switchToNext();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有接口都失败了
|
||||||
|
console.warn('所有远程接口都失败,尝试本地数据');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchFromLocal() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(API.localFallback + `?t=${Date.now()}`);
|
||||||
|
if (!resp.ok) throw new Error(`本地文件HTTP ${resp.status}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('读取本地返回接口.json失败:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTodayNews() {
|
||||||
|
this.loadNews();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNews(newsData) {
|
||||||
|
const contentDiv = document.getElementById('content');
|
||||||
|
if (!contentDiv || !newsData) return;
|
||||||
|
|
||||||
|
const {
|
||||||
|
date,
|
||||||
|
day_of_week,
|
||||||
|
lunar_date,
|
||||||
|
news,
|
||||||
|
tip,
|
||||||
|
link
|
||||||
|
} = newsData;
|
||||||
|
|
||||||
|
let newsListHtml = '';
|
||||||
|
if (news && news.length > 0) {
|
||||||
|
newsListHtml = news.map(item => `
|
||||||
|
<div class="news-item">
|
||||||
|
${this.escapeHtml(item)}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除图片显示功能
|
||||||
|
|
||||||
|
const tipHtml = tip ? `
|
||||||
|
<div class="daily-tip">
|
||||||
|
💡 ${this.escapeHtml(tip)}
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
const linkHtml = link ? `
|
||||||
|
<div style="text-align: center; margin-top: 20px;">
|
||||||
|
<a href="${this.escapeHtml(link)}" target="_blank" class="btn" style="text-decoration: none; display: inline-block;">
|
||||||
|
📖 查看原文
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
contentDiv.innerHTML = `
|
||||||
|
<div class="news-header">
|
||||||
|
<div>
|
||||||
|
<div class="news-date">${this.escapeHtml(date)} ${this.escapeHtml(day_of_week || '')}</div>
|
||||||
|
${lunar_date ? `<div class="lunar-date">${this.escapeHtml(lunar_date)}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${tipHtml}
|
||||||
|
|
||||||
|
<div class="news-list">
|
||||||
|
<h3 style="margin-bottom: 20px; color: #2d5016; font-size: 1.3rem;">📰 今日要闻</h3>
|
||||||
|
${newsListHtml}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${linkHtml}
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 30px; color: #5a7c65; font-size: 0.9rem;">
|
||||||
|
<p>数据来源:每天60秒读懂世界</p>
|
||||||
|
<p>更新时间:${newsData.api_updated || '未知'}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(text) {
|
||||||
|
if (typeof text !== 'string') return text;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后初始化应用
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
window.newsApp = new NewsApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加一些实用功能
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
showToast('已复制到剪贴板');
|
||||||
|
}).catch(() => {
|
||||||
|
showToast('复制失败,请手动复制');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message) {
|
||||||
|
// 创建提示框
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #4a5568;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 1000;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
`;
|
||||||
|
toast.textContent = message;
|
||||||
|
|
||||||
|
// 添加动画样式
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
// 3秒后自动移除
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
style.remove();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加键盘快捷键支持
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
// Ctrl/Cmd + R 刷新数据
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'r') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (window.newsApp) {
|
||||||
|
window.newsApp.loadNews();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
66
frontend/react-app/public/60sapi/日更资讯/每天60s读懂世界/返回接口.json
Normal file
66
frontend/react-app/public/60sapi/日更资讯/每天60s读懂世界/返回接口.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "9aa227e2ba294bb1a95c95fde892eb31",
|
||||||
|
"title": "《Totally Reliable Delivery Service》 Standard Edition",
|
||||||
|
"cover": "https://cdn1.epicgames.com/52b90f9a982a404781b189f6a7903226/offer/EGS_TotallyReliableDeliveryService_WereFiveGames_S1-2560x1440-47e6e9562d62705a75ea7b7096d0b8dc.jpg",
|
||||||
|
"original_price": 52,
|
||||||
|
"original_price_desc": "¥52.00",
|
||||||
|
"description": "穿好护腰护具,发动货车,送货的时间到啦!在一个高度互动的沙盒世界中,与最多三位好友一起随意地完成送货。货物已试投,这就是我们靠谱快递(Totally Reliable Delivery Service)的品质保证!",
|
||||||
|
"seller": "Infogrames LLC",
|
||||||
|
"is_free_now": true,
|
||||||
|
"free_start": "2025/08/14 23:00:00",
|
||||||
|
"free_start_at": 1755183600000,
|
||||||
|
"free_end": "2025/08/21 23:00:00",
|
||||||
|
"free_end_at": 1755788400000,
|
||||||
|
"link": "https://store.epicgames.com/store/zh-CN/p/totally-reliable-delivery-service/home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8ea3500dc38e4f429702bf889c172d3d",
|
||||||
|
"title": "Hidden Folks",
|
||||||
|
"cover": "https://cdn1.epicgames.com/spt-assets/7bfd56b0586348dcb139945d9e59f988/hidden-folks-1b7hh.png",
|
||||||
|
"original_price": 47,
|
||||||
|
"original_price_desc": "¥47.00",
|
||||||
|
"description": "Search for hidden folks in hand-drawn, interactive, miniature landscapes. Unfurl tent flaps, cut through bushes, slam doors, and poke some crocodiles! Rooooaaaarrrr!!!!!",
|
||||||
|
"seller": "Adriaan de Jongh",
|
||||||
|
"is_free_now": true,
|
||||||
|
"free_start": "2025/08/14 23:00:00",
|
||||||
|
"free_start_at": 1755183600000,
|
||||||
|
"free_end": "2025/08/21 23:00:00",
|
||||||
|
"free_end_at": 1755788400000,
|
||||||
|
"link": "https://store.epicgames.com/store/zh-CN/p/hidden-folks-239d16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4cbb6c3704d240f19c3dd5f5cb2b0cb4",
|
||||||
|
"title": "Kamaeru",
|
||||||
|
"cover": "https://cdn1.epicgames.com/spt-assets/44313cfbb62b4df5801d0c8d541c2624/kamaeru-40asc.png",
|
||||||
|
"original_price": 62,
|
||||||
|
"original_price_desc": "¥62.00",
|
||||||
|
"description": "Foster a sanctuary for frogs and restore the biodiversity of the wetlands in Kamaeru, a cozy frog collecting game, where you take pictures of frogs, play mini-games and decorate your habitat. Hop right to it!",
|
||||||
|
"seller": "Armor Games Studios",
|
||||||
|
"is_free_now": false,
|
||||||
|
"free_start": "2025/08/21 23:00:00",
|
||||||
|
"free_start_at": 1755788400000,
|
||||||
|
"free_end": "2025/08/28 23:00:00",
|
||||||
|
"free_end_at": 1756393200000,
|
||||||
|
"link": "https://store.epicgames.com/store/zh-CN/p/kamaeru-0c301e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0d9a533f0e684cc18620a8f408e8e72c",
|
||||||
|
"title": "Strange Horticulture",
|
||||||
|
"cover": "https://cdn1.epicgames.com/spt-assets/15e8e3eba65a4763a815d6eae1d763b2/strange-horticulture-offer-2wghv.png",
|
||||||
|
"original_price": 45,
|
||||||
|
"original_price_desc": "¥45.00",
|
||||||
|
"description": "款神秘学解谜游戏,你将扮演当地植物商店的店主,寻找并识别新的植物,悠闲撸猫,与女巫团体交谈,或加入异教。收集各种强大的植物,用它们来影响故事走向,揭开昂德米尔镇的黑暗谜团。",
|
||||||
|
"seller": "Iceberg Interactive",
|
||||||
|
"is_free_now": false,
|
||||||
|
"free_start": "2025/08/21 23:00:00",
|
||||||
|
"free_start_at": 1755788400000,
|
||||||
|
"free_end": "2025/08/28 23:00:00",
|
||||||
|
"free_end_at": 1756393200000,
|
||||||
|
"link": "https://store.epicgames.com/store/zh-CN/p/strange-horticulture-360e80"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
409
frontend/react-app/public/60sapi/日更资讯/每日国际汇率/css/style.css
Normal file
409
frontend/react-app/public/60sapi/日更资讯/每日国际汇率/css/style.css
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
/* 每日国际汇率 - 淡绿色清新风格样式 */
|
||||||
|
|
||||||
|
/* 重置样式 */
|
||||||
|
* {
|
||||||
|
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: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部样式 */
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #2d5016;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 货币选择器 */
|
||||||
|
.currency-selector {
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-selector label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-selector select {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: 2px solid #c8e6c9;
|
||||||
|
border-radius: 25px;
|
||||||
|
background: white;
|
||||||
|
color: #2d5016;
|
||||||
|
font-size: 1rem;
|
||||||
|
min-width: 200px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-selector select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #81c784;
|
||||||
|
box-shadow: 0 0 0 3px rgba(129, 199, 132, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #e8f5e8;
|
||||||
|
border-top: 4px solid #81c784;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 汇率信息容器 */
|
||||||
|
.exchange-info {
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-currency {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-currency h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #2d5016;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-time {
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 汇率网格 */
|
||||||
|
.rates-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(8, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-card {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 3px 10px rgba(45, 80, 22, 0.06);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid rgba(200, 230, 201, 0.5);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-card:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 15px rgba(45, 80, 22, 0.1);
|
||||||
|
border-color: #81c784;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-code {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2d5016;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-flag {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-rate {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #388e3c;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-name {
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索框 */
|
||||||
|
.search-container {
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: 2px solid #c8e6c9;
|
||||||
|
border-radius: 25px;
|
||||||
|
background: white;
|
||||||
|
color: #2d5016;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #81c784;
|
||||||
|
box-shadow: 0 0 0 3px rgba(129, 199, 132, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: #81c784;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 错误提示 */
|
||||||
|
.error {
|
||||||
|
background: #fed7d7;
|
||||||
|
color: #c53030;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #feb2b2;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计信息 */
|
||||||
|
.stats {
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(129, 199, 132, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2d5016;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
|
||||||
|
/* 平板端 */
|
||||||
|
@media (max-width: 768px) and (min-width: 481px) {
|
||||||
|
.container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rates-grid {
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-card {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-selector select {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机端 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rates-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-card {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-code {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-flag {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-rate {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-name {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-selector {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-selector select {
|
||||||
|
min-width: 100%;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 大屏幕优化 */
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.container {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特殊效果 */
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.6s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(129, 199, 132, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(129, 199, 132, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏类 */
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
86
frontend/react-app/public/60sapi/日更资讯/每日国际汇率/index.html
Normal file
86
frontend/react-app/public/60sapi/日更资讯/每日国际汇率/index.html
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="每日国际汇率 - 实时获取全球货币汇率信息">
|
||||||
|
<meta name="keywords" content="汇率,外汇,货币,汇率查询,实时汇率">
|
||||||
|
<title>每日国际汇率</title>
|
||||||
|
|
||||||
|
<!-- 引入样式文件 -->
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
|
||||||
|
<!-- 网站图标 -->
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>💱</text></svg>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<header class="header">
|
||||||
|
<h1>
|
||||||
|
<span>💱</span>
|
||||||
|
每日国际汇率
|
||||||
|
</h1>
|
||||||
|
<p>实时获取全球货币汇率信息,助您掌握汇率动态</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 货币选择器 -->
|
||||||
|
<div class="currency-selector">
|
||||||
|
<label for="currency-select">选择基础货币:</label>
|
||||||
|
<select id="currency-select">
|
||||||
|
<!-- 货币选项将通过JavaScript动态生成 -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<div class="search-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search-input"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="🔍 搜索货币代码或名称..."
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div id="loading" class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>正在加载汇率数据...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 汇率内容区域 -->
|
||||||
|
<main id="exchange-content" class="content" style="display: none;">
|
||||||
|
<!-- 汇率信息 -->
|
||||||
|
<div class="exchange-info">
|
||||||
|
<div class="base-currency">
|
||||||
|
<h2 id="base-currency">基础货币</h2>
|
||||||
|
<div id="update-time" class="update-time">更新时间: --</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<div class="stats">
|
||||||
|
<h3>汇率统计</h3>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div id="total-currencies" class="stat-number">--</div>
|
||||||
|
<div class="stat-label">货币总数</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div id="last-update" class="stat-number">--</div>
|
||||||
|
<div class="stat-label">最后更新</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 汇率网格 -->
|
||||||
|
<div id="rates-grid" class="rates-grid">
|
||||||
|
<!-- 汇率卡片将通过JavaScript动态生成 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 引入脚本文件 -->
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
520
frontend/react-app/public/60sapi/日更资讯/每日国际汇率/js/script.js
vendored
Normal file
520
frontend/react-app/public/60sapi/日更资讯/每日国际汇率/js/script.js
vendored
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
// 每日国际汇率 JavaScript 功能
|
||||||
|
|
||||||
|
// API配置
|
||||||
|
const API = {
|
||||||
|
endpoints: [],
|
||||||
|
currentIndex: 0,
|
||||||
|
defaultCurrency: 'CNY',
|
||||||
|
localFallback: '返回接口.json',
|
||||||
|
// 初始化API接口列表
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('./接口集合.json');
|
||||||
|
const endpoints = await res.json();
|
||||||
|
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/exchange_rate`);
|
||||||
|
} catch (e) {
|
||||||
|
// 如果无法加载接口集合,使用默认接口
|
||||||
|
this.endpoints = ['https://60s.viki.moe/v2/exchange_rate'];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 获取当前接口URL
|
||||||
|
getCurrentUrl(currency) {
|
||||||
|
if (this.endpoints.length === 0) return null;
|
||||||
|
const url = new URL(this.endpoints[this.currentIndex]);
|
||||||
|
url.searchParams.append('currency', currency);
|
||||||
|
return url.toString();
|
||||||
|
},
|
||||||
|
// 切换到下一个接口
|
||||||
|
switchToNext() {
|
||||||
|
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length;
|
||||||
|
return this.currentIndex < this.endpoints.length;
|
||||||
|
},
|
||||||
|
// 重置到第一个接口
|
||||||
|
reset() {
|
||||||
|
this.currentIndex = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 常用货币列表
|
||||||
|
const POPULAR_CURRENCIES = [
|
||||||
|
{ code: 'CNY', name: '人民币', flag: '🇨🇳' },
|
||||||
|
{ code: 'USD', name: '美元', flag: '🇺🇸' },
|
||||||
|
{ code: 'EUR', name: '欧元', flag: '🇪🇺' },
|
||||||
|
{ code: 'JPY', name: '日元', flag: '🇯🇵' },
|
||||||
|
{ code: 'GBP', name: '英镑', flag: '🇬🇧' },
|
||||||
|
{ code: 'AUD', name: '澳元', flag: '🇦🇺' },
|
||||||
|
{ code: 'CAD', name: '加元', flag: '🇨🇦' },
|
||||||
|
{ code: 'CHF', name: '瑞士法郎', flag: '🇨🇭' },
|
||||||
|
{ code: 'HKD', name: '港币', flag: '🇭🇰' },
|
||||||
|
{ code: 'SGD', name: '新加坡元', flag: '🇸🇬' },
|
||||||
|
{ code: 'KRW', name: '韩元', flag: '🇰🇷' },
|
||||||
|
{ code: 'THB', name: '泰铢', flag: '🇹🇭' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 货币优先级排序 - 经济发达、交易频繁的国家货币优先
|
||||||
|
const CURRENCY_PRIORITY = {
|
||||||
|
// 第一梯队:全球主要储备货币和交易货币
|
||||||
|
'USD': 1, // 美元 - 全球储备货币
|
||||||
|
'EUR': 2, // 欧元 - 欧盟统一货币
|
||||||
|
'JPY': 3, // 日元 - 亚洲主要货币
|
||||||
|
'GBP': 4, // 英镑 - 传统储备货币
|
||||||
|
'CNY': 5, // 人民币 - 中国货币
|
||||||
|
|
||||||
|
// 第二梯队:发达国家货币
|
||||||
|
'CHF': 10, // 瑞士法郎 - 避险货币
|
||||||
|
'CAD': 11, // 加拿大元
|
||||||
|
'AUD': 12, // 澳大利亚元
|
||||||
|
'NZD': 13, // 新西兰元
|
||||||
|
'SEK': 14, // 瑞典克朗
|
||||||
|
'NOK': 15, // 挪威克朗
|
||||||
|
'DKK': 16, // 丹麦克朗
|
||||||
|
|
||||||
|
// 第三梯队:亚洲发达经济体
|
||||||
|
'HKD': 20, // 港币
|
||||||
|
'SGD': 21, // 新加坡元
|
||||||
|
'KRW': 22, // 韩元
|
||||||
|
'TWD': 23, // 新台币
|
||||||
|
|
||||||
|
// 第四梯队:重要新兴市场货币
|
||||||
|
'RUB': 30, // 俄罗斯卢布
|
||||||
|
'INR': 31, // 印度卢比
|
||||||
|
'BRL': 32, // 巴西雷亚尔
|
||||||
|
'MXN': 33, // 墨西哥比索
|
||||||
|
'ZAR': 34, // 南非兰特
|
||||||
|
'TRY': 35, // 土耳其里拉
|
||||||
|
|
||||||
|
// 第五梯队:亚洲重要货币
|
||||||
|
'THB': 40, // 泰铢
|
||||||
|
'MYR': 41, // 马来西亚林吉特
|
||||||
|
'IDR': 42, // 印尼盾
|
||||||
|
'PHP': 43, // 菲律宾比索
|
||||||
|
'VND': 44, // 越南盾
|
||||||
|
|
||||||
|
// 第六梯队:中东石油国家货币
|
||||||
|
'SAR': 50, // 沙特里亚尔
|
||||||
|
'AED': 51, // 阿联酋迪拉姆
|
||||||
|
'QAR': 52, // 卡塔尔里亚尔
|
||||||
|
'KWD': 53, // 科威特第纳尔
|
||||||
|
|
||||||
|
// 第七梯队:欧洲其他货币
|
||||||
|
'PLN': 60, // 波兰兹罗提
|
||||||
|
'CZK': 61, // 捷克克朗
|
||||||
|
'HUF': 62, // 匈牙利福林
|
||||||
|
'RON': 63, // 罗马尼亚列伊
|
||||||
|
'BGN': 64, // 保加利亚列弗
|
||||||
|
'HRK': 65, // 克罗地亚库纳
|
||||||
|
|
||||||
|
// 第八梯队:拉美货币
|
||||||
|
'ARS': 70, // 阿根廷比索
|
||||||
|
'CLP': 71, // 智利比索
|
||||||
|
'COP': 72, // 哥伦比亚比索
|
||||||
|
'PEN': 73, // 秘鲁索尔
|
||||||
|
'UYU': 74, // 乌拉圭比索
|
||||||
|
|
||||||
|
// 其他货币默认优先级为 999
|
||||||
|
};
|
||||||
|
|
||||||
|
// 货币名称映射
|
||||||
|
const CURRENCY_NAMES = {
|
||||||
|
'CNY': '人民币', 'USD': '美元', 'EUR': '欧元', 'JPY': '日元', 'GBP': '英镑',
|
||||||
|
'AUD': '澳元', 'CAD': '加元', 'CHF': '瑞士法郎', 'HKD': '港币', 'SGD': '新加坡元',
|
||||||
|
'KRW': '韩元', 'THB': '泰铢', 'AED': '阿联酋迪拉姆', 'AFN': '阿富汗尼',
|
||||||
|
'ALL': '阿尔巴尼亚列克', 'AMD': '亚美尼亚德拉姆', 'ANG': '荷属安的列斯盾',
|
||||||
|
'AOA': '安哥拉宽扎', 'ARS': '阿根廷比索', 'AWG': '阿鲁巴弗罗林',
|
||||||
|
'AZN': '阿塞拜疆马纳特', 'BAM': '波黑马克', 'BBD': '巴巴多斯元',
|
||||||
|
'BDT': '孟加拉塔卡', 'BGN': '保加利亚列弗', 'BHD': '巴林第纳尔',
|
||||||
|
'BIF': '布隆迪法郎', 'BMD': '百慕大元', 'BND': '文莱元', 'BOB': '玻利维亚诺',
|
||||||
|
'BRL': '巴西雷亚尔', 'BSD': '巴哈马元', 'BTN': '不丹努尔特鲁姆',
|
||||||
|
'BWP': '博茨瓦纳普拉', 'BYN': '白俄罗斯卢布', 'BZD': '伯利兹元',
|
||||||
|
'CDF': '刚果法郎', 'CLP': '智利比索', 'COP': '哥伦比亚比索', 'CRC': '哥斯达黎加科朗',
|
||||||
|
'CUP': '古巴比索', 'CVE': '佛得角埃斯库多', 'CZK': '捷克克朗', 'DJF': '吉布提法郎',
|
||||||
|
'DKK': '丹麦克朗', 'DOP': '多米尼加比索', 'DZD': '阿尔及利亚第纳尔', 'EGP': '埃及镑',
|
||||||
|
'ERN': '厄立特里亚纳克法', 'ETB': '埃塞俄比亚比尔', 'FJD': '斐济元', 'FKP': '福克兰群岛镑',
|
||||||
|
'FOK': '法罗群岛克朗', 'GEL': '格鲁吉亚拉里', 'GGP': '根西岛镑', 'GHS': '加纳塞地',
|
||||||
|
'GIP': '直布罗陀镑', 'GMD': '冈比亚达拉西', 'GNF': '几内亚法郎', 'GTQ': '危地马拉格查尔',
|
||||||
|
'GYD': '圭亚那元', 'HNL': '洪都拉斯伦皮拉', 'HRK': '克罗地亚库纳', 'HTG': '海地古德',
|
||||||
|
'HUF': '匈牙利福林', 'IDR': '印尼盾', 'ILS': '以色列新谢克尔', 'IMP': '马恩岛镑',
|
||||||
|
'INR': '印度卢比', 'IQD': '伊拉克第纳尔', 'IRR': '伊朗里亚尔', 'ISK': '冰岛克朗',
|
||||||
|
'JEP': '泽西岛镑', 'JMD': '牙买加元', 'JOD': '约旦第纳尔', 'KES': '肯尼亚先令',
|
||||||
|
'KGS': '吉尔吉斯斯坦索姆', 'KHR': '柬埔寨瑞尔', 'KID': '基里巴斯元', 'KMF': '科摩罗法郎',
|
||||||
|
'KWD': '科威特第纳尔', 'KYD': '开曼群岛元', 'KZT': '哈萨克斯坦坚戈', 'LAK': '老挝基普',
|
||||||
|
'LBP': '黎巴嫩镑', 'LKR': '斯里兰卡卢比', 'LRD': '利比里亚元', 'LSL': '莱索托洛蒂',
|
||||||
|
'LYD': '利比亚第纳尔', 'MAD': '摩洛哥迪拉姆', 'MDL': '摩尔多瓦列伊', 'MGA': '马达加斯加阿里亚里',
|
||||||
|
'MKD': '北马其顿第纳尔', 'MMK': '缅甸缅元', 'MNT': '蒙古图格里克', 'MOP': '澳门帕塔卡',
|
||||||
|
'MRU': '毛里塔尼亚乌吉亚', 'MUR': '毛里求斯卢比', 'MVR': '马尔代夫拉菲亚', 'MWK': '马拉维克瓦查',
|
||||||
|
'MXN': '墨西哥比索', 'MYR': '马来西亚林吉特', 'MZN': '莫桑比克梅蒂卡尔', 'NAD': '纳米比亚元',
|
||||||
|
'NGN': '尼日利亚奈拉', 'NIO': '尼加拉瓜科多巴', 'NOK': '挪威克朗', 'NPR': '尼泊尔卢比',
|
||||||
|
'NZD': '新西兰元', 'OMR': '阿曼里亚尔', 'PAB': '巴拿马巴波亚', 'PEN': '秘鲁索尔',
|
||||||
|
'PGK': '巴布亚新几内亚基那', 'PHP': '菲律宾比索', 'PKR': '巴基斯坦卢比', 'PLN': '波兰兹罗提',
|
||||||
|
'PYG': '巴拉圭瓜拉尼', 'QAR': '卡塔尔里亚尔', 'RON': '罗马尼亚列伊', 'RSD': '塞尔维亚第纳尔',
|
||||||
|
'RUB': '俄罗斯卢布', 'RWF': '卢旺达法郎', 'SAR': '沙特里亚尔', 'SBD': '所罗门群岛元',
|
||||||
|
'SCR': '塞舌尔卢比', 'SDG': '苏丹镑', 'SEK': '瑞典克朗', 'SHP': '圣赫勒拿镑',
|
||||||
|
'SLE': '塞拉利昂利昂', 'SLL': '塞拉利昂利昂(旧)', 'SOS': '索马里先令', 'SRD': '苏里南元',
|
||||||
|
'SSP': '南苏丹镑', 'STN': '圣多美和普林西比多布拉', 'SYP': '叙利亚镑', 'SZL': '斯威士兰里兰吉尼',
|
||||||
|
'TJS': '塔吉克斯坦索莫尼', 'TMT': '土库曼斯坦马纳特', 'TND': '突尼斯第纳尔', 'TOP': '汤加潘加',
|
||||||
|
'TRY': '土耳其里拉', 'TTD': '特立尼达和多巴哥元', 'TVD': '图瓦卢元', 'TWD': '新台币',
|
||||||
|
'TZS': '坦桑尼亚先令', 'UAH': '乌克兰格里夫纳', 'UGX': '乌干达先令', 'UYU': '乌拉圭比索',
|
||||||
|
'UZS': '乌兹别克斯坦苏姆', 'VES': '委内瑞拉玻利瓦尔', 'VND': '越南盾', 'VUV': '瓦努阿图瓦图',
|
||||||
|
'WST': '萨摩亚塔拉', 'XAF': '中非法郎', 'XCD': '东加勒比元', 'XCG': '加勒比盾',
|
||||||
|
'XDR': '特别提款权', 'XOF': '西非法郎', 'XPF': '太平洋法郎', 'YER': '也门里亚尔',
|
||||||
|
'ZAR': '南非兰特', 'ZMW': '赞比亚克瓦查', 'ZWL': '津巴布韦元'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 货币旗帜映射
|
||||||
|
const CURRENCY_FLAGS = {
|
||||||
|
'CNY': '🇨🇳', 'USD': '🇺🇸', 'EUR': '🇪🇺', 'JPY': '🇯🇵', 'GBP': '🇬🇧',
|
||||||
|
'AUD': '🇦🇺', 'CAD': '🇨🇦', 'CHF': '🇨🇭', 'HKD': '🇭🇰', 'SGD': '🇸🇬',
|
||||||
|
'KRW': '🇰🇷', 'THB': '🇹🇭', 'AED': '🇦🇪', 'AFN': '🇦🇫', 'ALL': '🇦🇱',
|
||||||
|
'AMD': '🇦🇲', 'ANG': '🇳🇱', 'AOA': '🇦🇴', 'ARS': '🇦🇷', 'AWG': '🇦🇼',
|
||||||
|
'AZN': '🇦🇿', 'BAM': '🇧🇦', 'BBD': '🇧🇧', 'BDT': '🇧🇩', 'BGN': '🇧🇬',
|
||||||
|
'BHD': '🇧🇭', 'BIF': '🇧🇮', 'BMD': '🇧🇲', 'BND': '🇧🇳', 'BOB': '🇧🇴',
|
||||||
|
'BRL': '🇧🇷', 'BSD': '🇧🇸', 'BTN': '🇧🇹', 'BWP': '🇧🇼', 'BYN': '🇧🇾',
|
||||||
|
'BZD': '🇧🇿', 'CDF': '🇨🇩', 'CLP': '🇨🇱', 'COP': '🇨🇴', 'CRC': '🇨🇷',
|
||||||
|
'CUP': '🇨🇺', 'CVE': '🇨🇻', 'CZK': '🇨🇿', 'DJF': '🇩🇯', 'DKK': '🇩🇰',
|
||||||
|
'DOP': '🇩🇴', 'DZD': '🇩🇿', 'EGP': '🇪🇬', 'ERN': '🇪🇷', 'ETB': '🇪🇹',
|
||||||
|
'FJD': '🇫🇯', 'FKP': '🇫🇰', 'FOK': '🇫🇴', 'GEL': '🇬🇪', 'GGP': '🇬🇬',
|
||||||
|
'GHS': '🇬🇭', 'GIP': '🇬🇮', 'GMD': '🇬🇲', 'GNF': '🇬🇳', 'GTQ': '🇬🇹',
|
||||||
|
'GYD': '🇬🇾', 'HNL': '🇭🇳', 'HRK': '🇭🇷', 'HTG': '🇭🇹', 'HUF': '🇭🇺',
|
||||||
|
'IDR': '🇮🇩', 'ILS': '🇮🇱', 'IMP': '🇮🇲', 'INR': '🇮🇳', 'IQD': '🇮🇶',
|
||||||
|
'IRR': '🇮🇷', 'ISK': '🇮🇸', 'JEP': '🇯🇪', 'JMD': '🇯🇲', 'JOD': '🇯🇴',
|
||||||
|
'KES': '🇰🇪', 'KGS': '🇰🇬', 'KHR': '🇰🇭', 'KID': '🇰🇮', 'KMF': '🇰🇲',
|
||||||
|
'KWD': '🇰🇼', 'KYD': '🇰🇾', 'KZT': '🇰🇿', 'LAK': '🇱🇦', 'LBP': '🇱🇧',
|
||||||
|
'LKR': '🇱🇰', 'LRD': '🇱🇷', 'LSL': '🇱🇸', 'LYD': '🇱🇾', 'MAD': '🇲🇦',
|
||||||
|
'MDL': '🇲🇩', 'MGA': '🇲🇬', 'MKD': '🇲🇰', 'MMK': '🇲🇲', 'MNT': '🇲🇳',
|
||||||
|
'MOP': '🇲🇴', 'MRU': '🇲🇷', 'MUR': '🇲🇺', 'MVR': '🇲🇻', 'MWK': '🇲🇼',
|
||||||
|
'MXN': '🇲🇽', 'MYR': '🇲🇾', 'MZN': '🇲🇿', 'NAD': '🇳🇦', 'NGN': '🇳🇬',
|
||||||
|
'NIO': '🇳🇮', 'NOK': '🇳🇴', 'NPR': '🇳🇵', 'NZD': '🇳🇿', 'OMR': '🇴🇲',
|
||||||
|
'PAB': '🇵🇦', 'PEN': '🇵🇪', 'PGK': '🇵🇬', 'PHP': '🇵🇭', 'PKR': '🇵🇰',
|
||||||
|
'PLN': '🇵🇱', 'PYG': '🇵🇾', 'QAR': '🇶🇦', 'RON': '🇷🇴', 'RSD': '🇷🇸',
|
||||||
|
'RUB': '🇷🇺', 'RWF': '🇷🇼', 'SAR': '🇸🇦', 'SBD': '🇸🇧', 'SCR': '🇸🇨',
|
||||||
|
'SDG': '🇸🇩', 'SEK': '🇸🇪', 'SHP': '🇸🇭', 'SLE': '🇸🇱', 'SLL': '🇸🇱',
|
||||||
|
'SOS': '🇸🇴', 'SRD': '🇸🇷', 'SSP': '🇸🇸', 'STN': '🇸🇹', 'SYP': '🇸🇾',
|
||||||
|
'SZL': '🇸🇿', 'TJS': '🇹🇯', 'TMT': '🇹🇲', 'TND': '🇹🇳', 'TOP': '🇹🇴',
|
||||||
|
'TRY': '🇹🇷', 'TTD': '🇹🇹', 'TVD': '🇹🇻', 'TWD': '🇹🇼', 'TZS': '🇹🇿',
|
||||||
|
'UAH': '🇺🇦', 'UGX': '🇺🇬', 'UYU': '🇺🇾', 'UZS': '🇺🇿', 'VES': '🇻🇪',
|
||||||
|
'VND': '🇻🇳', 'VUV': '🇻🇺', 'WST': '🇼🇸', 'XAF': '🌍', 'XCD': '🏝️',
|
||||||
|
'XCG': '🏝️', 'XDR': '🌐', 'XOF': '🌍', 'XPF': '🌊', 'YER': '🇾🇪',
|
||||||
|
'ZAR': '🇿🇦', 'ZMW': '🇿🇲', 'ZWL': '🇿🇼'
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM元素
|
||||||
|
let elements = {};
|
||||||
|
let currentRates = [];
|
||||||
|
let filteredRates = [];
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initElements();
|
||||||
|
initCurrencySelector();
|
||||||
|
bindEvents();
|
||||||
|
loadExchangeRates();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化DOM元素
|
||||||
|
function initElements() {
|
||||||
|
elements = {
|
||||||
|
currencySelect: document.getElementById('currency-select'),
|
||||||
|
searchInput: document.getElementById('search-input'),
|
||||||
|
loading: document.getElementById('loading'),
|
||||||
|
content: document.getElementById('exchange-content'),
|
||||||
|
baseCurrency: document.getElementById('base-currency'),
|
||||||
|
updateTime: document.getElementById('update-time'),
|
||||||
|
ratesGrid: document.getElementById('rates-grid'),
|
||||||
|
totalCurrencies: document.getElementById('total-currencies'),
|
||||||
|
lastUpdate: document.getElementById('last-update')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化货币选择器
|
||||||
|
function initCurrencySelector() {
|
||||||
|
if (!elements.currencySelect) return;
|
||||||
|
|
||||||
|
// 添加常用货币选项
|
||||||
|
POPULAR_CURRENCIES.forEach(currency => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = currency.code;
|
||||||
|
option.textContent = `${currency.flag} ${currency.code} - ${currency.name}`;
|
||||||
|
if (currency.code === API.defaultCurrency) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
elements.currencySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
function bindEvents() {
|
||||||
|
// 货币选择变化
|
||||||
|
if (elements.currencySelect) {
|
||||||
|
elements.currencySelect.addEventListener('change', function() {
|
||||||
|
loadExchangeRates(this.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索功能
|
||||||
|
if (elements.searchInput) {
|
||||||
|
elements.searchInput.addEventListener('input', function() {
|
||||||
|
filterRates(this.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载汇率数据
|
||||||
|
async function loadExchangeRates(currency = API.defaultCurrency) {
|
||||||
|
try {
|
||||||
|
showLoading(true);
|
||||||
|
|
||||||
|
// 尝试从API获取数据
|
||||||
|
const data = await fetchFromAPI(currency);
|
||||||
|
|
||||||
|
if (data && data.code === 200 && data.data) {
|
||||||
|
currentRates = data.data.rates || [];
|
||||||
|
displayExchangeRates(data.data);
|
||||||
|
} else {
|
||||||
|
// 尝试从本地获取数据
|
||||||
|
const localData = await fetchFromLocal();
|
||||||
|
if (localData && localData.code === 200 && localData.data) {
|
||||||
|
currentRates = localData.data.rates || [];
|
||||||
|
displayExchangeRates(localData.data);
|
||||||
|
showError('使用本地数据,可能不是最新汇率');
|
||||||
|
} else {
|
||||||
|
throw new Error('无法获取汇率数据');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载汇率失败:', error);
|
||||||
|
showError('加载汇率数据失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
showLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从API获取数据
|
||||||
|
async function fetchFromAPI(currency) {
|
||||||
|
// 初始化API接口列表
|
||||||
|
await API.init();
|
||||||
|
|
||||||
|
// 重置API索引到第一个接口
|
||||||
|
API.reset();
|
||||||
|
|
||||||
|
// 尝试所有API接口
|
||||||
|
for (let i = 0; i < API.endpoints.length; i++) {
|
||||||
|
try {
|
||||||
|
const url = API.getCurrentUrl(currency);
|
||||||
|
console.log(`尝试接口 ${i + 1}/${API.endpoints.length}: ${url}`);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
cache: 'no-store'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data && data.code === 200) {
|
||||||
|
console.log(`接口 ${i + 1} 请求成功`);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(data && data.message ? data.message : '接口返回异常');
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`接口 ${i + 1} 失败:`, e.message);
|
||||||
|
|
||||||
|
// 如果不是最后一个接口,切换到下一个
|
||||||
|
if (i < API.endpoints.length - 1) {
|
||||||
|
API.switchToNext();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有接口都失败了
|
||||||
|
console.warn('所有远程接口都失败,尝试本地数据');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从本地获取数据
|
||||||
|
async function fetchFromLocal() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(API.localFallback + `?t=${Date.now()}`);
|
||||||
|
if (!response.ok) throw new Error(`本地文件HTTP ${response.status}`);
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('读取本地返回接口.json失败:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示汇率数据
|
||||||
|
function displayExchangeRates(data) {
|
||||||
|
if (!data || !data.rates) {
|
||||||
|
showError('没有获取到汇率数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新基础货币信息
|
||||||
|
if (elements.baseCurrency) {
|
||||||
|
const baseCurrencyName = CURRENCY_NAMES[data.base_code] || data.base_code;
|
||||||
|
const baseCurrencyFlag = CURRENCY_FLAGS[data.base_code] || '💱';
|
||||||
|
elements.baseCurrency.textContent = `${baseCurrencyFlag} ${data.base_code} - ${baseCurrencyName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新时间信息
|
||||||
|
if (elements.updateTime && data.updated) {
|
||||||
|
elements.updateTime.textContent = `更新时间: ${data.updated}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新统计信息
|
||||||
|
updateStats(data);
|
||||||
|
|
||||||
|
// 显示汇率列表
|
||||||
|
filteredRates = data.rates;
|
||||||
|
renderRates(filteredRates);
|
||||||
|
|
||||||
|
// 显示内容区域
|
||||||
|
if (elements.content) {
|
||||||
|
elements.content.classList.add('fade-in');
|
||||||
|
elements.content.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新统计信息
|
||||||
|
function updateStats(data) {
|
||||||
|
if (elements.totalCurrencies) {
|
||||||
|
elements.totalCurrencies.textContent = data.rates ? data.rates.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.lastUpdate && data.updated) {
|
||||||
|
elements.lastUpdate.textContent = data.updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染汇率列表
|
||||||
|
function renderRates(rates) {
|
||||||
|
if (!elements.ratesGrid || !rates) return;
|
||||||
|
|
||||||
|
// 按优先级排序货币
|
||||||
|
const sortedRates = [...rates].sort((a, b) => {
|
||||||
|
const priorityA = CURRENCY_PRIORITY[a.currency] || 999;
|
||||||
|
const priorityB = CURRENCY_PRIORITY[b.currency] || 999;
|
||||||
|
|
||||||
|
// 优先级相同时按货币代码字母顺序排序
|
||||||
|
if (priorityA === priorityB) {
|
||||||
|
return a.currency.localeCompare(b.currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
return priorityA - priorityB;
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.ratesGrid.innerHTML = '';
|
||||||
|
|
||||||
|
sortedRates.forEach(rate => {
|
||||||
|
const rateCard = createRateCard(rate);
|
||||||
|
elements.ratesGrid.appendChild(rateCard);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建汇率卡片
|
||||||
|
function createRateCard(rate) {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'rate-card';
|
||||||
|
|
||||||
|
const currencyName = CURRENCY_NAMES[rate.currency] || rate.currency;
|
||||||
|
const currencyFlag = CURRENCY_FLAGS[rate.currency] || '💱';
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="currency-code">
|
||||||
|
<span class="currency-flag">${currencyFlag}</span>
|
||||||
|
${rate.currency}
|
||||||
|
</div>
|
||||||
|
<div class="exchange-rate">${formatRate(rate.rate)}</div>
|
||||||
|
<div class="currency-name">${currencyName}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化汇率
|
||||||
|
function formatRate(rate) {
|
||||||
|
if (rate >= 1) {
|
||||||
|
return rate.toFixed(4);
|
||||||
|
} else if (rate >= 0.01) {
|
||||||
|
return rate.toFixed(6);
|
||||||
|
} else {
|
||||||
|
return rate.toFixed(8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤汇率数据
|
||||||
|
function filterRates(searchTerm) {
|
||||||
|
if (!searchTerm.trim()) {
|
||||||
|
renderRates(currentRates);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = currentRates.filter(rate => {
|
||||||
|
const currencyName = CURRENCY_NAMES[rate.currency] || rate.currency;
|
||||||
|
return rate.currency.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
currencyName.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
renderRates(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示/隐藏加载状态
|
||||||
|
function showLoading(show) {
|
||||||
|
if (elements.loading) {
|
||||||
|
elements.loading.style.display = show ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
if (elements.content) {
|
||||||
|
elements.content.style.display = show ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误信息
|
||||||
|
function showError(message) {
|
||||||
|
if (elements.content) {
|
||||||
|
elements.content.innerHTML = `
|
||||||
|
<div class="error">
|
||||||
|
<h3>⚠️ 加载失败</h3>
|
||||||
|
<p>${escapeHtml(message)}</p>
|
||||||
|
<p>请检查网络连接或稍后重试</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
elements.content.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML转义
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
window.addEventListener('error', function(event) {
|
||||||
|
console.error('页面错误:', event.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 网络状态监听
|
||||||
|
window.addEventListener('online', function() {
|
||||||
|
console.log('网络已连接');
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('offline', function() {
|
||||||
|
console.log('网络已断开');
|
||||||
|
showError('网络连接已断开,请检查网络设置');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导出函数供外部调用
|
||||||
|
window.ExchangeRate = {
|
||||||
|
loadExchangeRates,
|
||||||
|
showError,
|
||||||
|
showLoading
|
||||||
|
};
|
||||||
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"
|
||||||
|
]
|
||||||
1
frontend/react-app/public/60sapi/日更资讯/每日国际汇率/返回接口.json
Normal file
1
frontend/react-app/public/60sapi/日更资讯/每日国际汇率/返回接口.json
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,106 @@
|
|||||||
|
/* 彩虹背景相关样式 */
|
||||||
|
body {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#ff6b6b 0%,
|
||||||
|
#4ecdc4 12.5%,
|
||||||
|
#45b7d1 25%,
|
||||||
|
#96ceb4 37.5%,
|
||||||
|
#feca57 50%,
|
||||||
|
#ff9ff3 62.5%,
|
||||||
|
#54a0ff 75%,
|
||||||
|
#5f27cd 87.5%,
|
||||||
|
#00d2d3 100%
|
||||||
|
);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: rainbowGradient 15s ease infinite;
|
||||||
|
background-attachment: fixed;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rainbowGradient {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 100%;
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
background-position: 0% 100%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 彩虹装饰层 */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 20% 20%, rgba(255, 107, 107, 0.15) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 80% 80%, rgba(78, 205, 196, 0.15) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 40% 80%, rgba(69, 183, 209, 0.12) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 60% 20%, rgba(150, 206, 180, 0.12) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 80% 40%, rgba(254, 202, 87, 0.1) 0%, transparent 35%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
animation: float 20s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 彩虹粒子效果 */
|
||||||
|
body::after {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 10% 10%, rgba(255, 107, 107, 0.8) 2px, transparent 2px),
|
||||||
|
radial-gradient(circle at 30% 20%, rgba(78, 205, 196, 0.8) 1.5px, transparent 1.5px),
|
||||||
|
radial-gradient(circle at 50% 30%, rgba(69, 183, 209, 0.8) 1px, transparent 1px),
|
||||||
|
radial-gradient(circle at 70% 40%, rgba(150, 206, 180, 0.8) 2px, transparent 2px),
|
||||||
|
radial-gradient(circle at 90% 50%, rgba(254, 202, 87, 0.8) 1.5px, transparent 1.5px),
|
||||||
|
radial-gradient(circle at 20% 60%, rgba(255, 159, 243, 0.8) 1px, transparent 1px),
|
||||||
|
radial-gradient(circle at 40% 70%, rgba(84, 160, 255, 0.8) 2px, transparent 2px),
|
||||||
|
radial-gradient(circle at 60% 80%, rgba(95, 39, 205, 0.8) 1.5px, transparent 1.5px),
|
||||||
|
radial-gradient(circle at 80% 90%, rgba(0, 210, 211, 0.8) 1px, transparent 1px);
|
||||||
|
background-size: 200px 200px, 250px 250px, 180px 180px, 300px 300px, 220px 220px, 160px 160px, 280px 280px, 240px 240px, 200px 200px;
|
||||||
|
animation: sparkle 25s linear infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0px) rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-15px) rotate(2deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sparkle {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateX(0) translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translateX(-10px) translateY(-5px) scale(1.1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(10px) translateY(-10px) scale(0.9);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateX(-5px) translateY(-15px) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
1037
frontend/react-app/public/60sapi/热搜榜单/HackerNews榜单/css/style.css
Normal file
1037
frontend/react-app/public/60sapi/热搜榜单/HackerNews榜单/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>🔥 HackerNews 热门榜单</title>
|
||||||
|
<link rel="stylesheet" href="css/background.css">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-icon">🌈</div>
|
||||||
|
<h1 class="title">🔥 HackerNews 热门榜单 💻</h1>
|
||||||
|
<p class="subtitle">全球技术社区 · 实时热门话题</p>
|
||||||
|
|
||||||
|
<div class="tab-container">
|
||||||
|
<button class="tab-btn active" data-type="top">
|
||||||
|
<span class="tab-icon">🏆</span>
|
||||||
|
热门榜
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" data-type="new">
|
||||||
|
<span class="tab-icon">🆕</span>
|
||||||
|
最新榜
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" data-type="best">
|
||||||
|
<span class="tab-icon">⭐</span>
|
||||||
|
最佳榜
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="update-time">
|
||||||
|
<span class="time-icon">⏰</span>
|
||||||
|
<span id="updateTime">加载中...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="refreshBtn" class="refresh-btn">
|
||||||
|
<span class="btn-icon">🔄</span>
|
||||||
|
刷新数据
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="loading" id="loading">
|
||||||
|
<div class="loading-content">
|
||||||
|
<div class="rainbow-spinner"></div>
|
||||||
|
<div class="loading-text">
|
||||||
|
<span class="loading-emoji">🚀</span>
|
||||||
|
<p>正在获取最新榜单...</p>
|
||||||
|
<div class="loading-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="news-list" id="newsList">
|
||||||
|
<!-- 新闻列表将动态生成 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-message" id="errorMessage" style="display: none;">
|
||||||
|
<div class="error-content">
|
||||||
|
<div class="error-icon">💥</div>
|
||||||
|
<h3>加载失败了</h3>
|
||||||
|
<p>网络连接出现问题,请稍后重试</p>
|
||||||
|
<button onclick="loadNewsList()" class="retry-btn">
|
||||||
|
<span>🔄</span>
|
||||||
|
重新加载
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
338
frontend/react-app/public/60sapi/热搜榜单/HackerNews榜单/js/script.js
vendored
Normal file
338
frontend/react-app/public/60sapi/热搜榜单/HackerNews榜单/js/script.js
vendored
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
// API接口列表
|
||||||
|
const API_ENDPOINTS = [
|
||||||
|
"https://60s-cf.viki.moe",
|
||||||
|
"https://60s.viki.moe",
|
||||||
|
"https://60s.b23.run",
|
||||||
|
"https://60s.114128.xyz",
|
||||||
|
"https://60s-cf.114128.xyz"
|
||||||
|
];
|
||||||
|
|
||||||
|
// 当前使用的API索引
|
||||||
|
let currentApiIndex = 0;
|
||||||
|
let currentType = 'top';
|
||||||
|
|
||||||
|
// DOM元素
|
||||||
|
const loadingElement = document.getElementById('loading');
|
||||||
|
const newsListElement = document.getElementById('newsList');
|
||||||
|
const errorMessageElement = document.getElementById('errorMessage');
|
||||||
|
const updateTimeElement = document.getElementById('updateTime');
|
||||||
|
const refreshBtn = document.getElementById('refreshBtn');
|
||||||
|
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||||
|
|
||||||
|
// 页面加载完成后自动加载数据
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadNewsList();
|
||||||
|
initTabEvents();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化标签事件
|
||||||
|
function initTabEvents() {
|
||||||
|
tabBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const type = this.getAttribute('data-type');
|
||||||
|
if (type !== currentType) {
|
||||||
|
currentType = type;
|
||||||
|
updateActiveTab(this);
|
||||||
|
loadNewsList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新活跃标签
|
||||||
|
function updateActiveTab(activeBtn) {
|
||||||
|
tabBtns.forEach(btn => btn.classList.remove('active'));
|
||||||
|
activeBtn.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新按钮点击事件
|
||||||
|
refreshBtn.addEventListener('click', function() {
|
||||||
|
loadNewsList();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载新闻列表
|
||||||
|
async function loadNewsList() {
|
||||||
|
showLoading();
|
||||||
|
hideError();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchData();
|
||||||
|
displayNewsList(data.data);
|
||||||
|
updateRefreshTime();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载失败:', error);
|
||||||
|
showError();
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
async function fetchData() {
|
||||||
|
for (let i = 0; i < API_ENDPOINTS.length; i++) {
|
||||||
|
const apiUrl = API_ENDPOINTS[currentApiIndex];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiUrl}/v2/hacker-news/${currentType}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.code === 200 && data.data) {
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
throw new Error('数据格式错误');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`API ${apiUrl} 请求失败:`, error);
|
||||||
|
currentApiIndex = (currentApiIndex + 1) % API_ENDPOINTS.length;
|
||||||
|
|
||||||
|
if (i === API_ENDPOINTS.length - 1) {
|
||||||
|
throw new Error('所有API接口都无法访问');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示新闻列表
|
||||||
|
function displayNewsList(newsData) {
|
||||||
|
newsListElement.innerHTML = '';
|
||||||
|
|
||||||
|
newsData.forEach((item, index) => {
|
||||||
|
const newsItem = createNewsItem(item, index + 1);
|
||||||
|
newsListElement.appendChild(newsItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新闻项目
|
||||||
|
function createNewsItem(item, rank) {
|
||||||
|
const newsItem = document.createElement('div');
|
||||||
|
newsItem.className = 'news-item';
|
||||||
|
|
||||||
|
const rankClass = rank <= 5 ? 'news-rank top-5' : 'news-rank';
|
||||||
|
const formattedScore = formatScore(item.score);
|
||||||
|
const formattedTime = formatTime(item.created);
|
||||||
|
|
||||||
|
// 根据排名添加特殊标识
|
||||||
|
let rankEmoji = '';
|
||||||
|
if (rank === 1) rankEmoji = '🏆';
|
||||||
|
else if (rank === 2) rankEmoji = '🥈';
|
||||||
|
else if (rank === 3) rankEmoji = '🥉';
|
||||||
|
else if (rank <= 10) rankEmoji = '💎';
|
||||||
|
else rankEmoji = '⭐';
|
||||||
|
|
||||||
|
// 根据评分添加热度指示
|
||||||
|
let heatLevel = '';
|
||||||
|
if (item.score >= 1000) heatLevel = '🔥🔥🔥';
|
||||||
|
else if (item.score >= 500) heatLevel = '🔥🔥';
|
||||||
|
else if (item.score >= 100) heatLevel = '🔥';
|
||||||
|
else heatLevel = '💫';
|
||||||
|
|
||||||
|
newsItem.innerHTML = `
|
||||||
|
<div class="news-header">
|
||||||
|
<div class="${rankClass}">${rank}</div>
|
||||||
|
<div class="news-score">${heatLevel} ${formattedScore}</div>
|
||||||
|
</div>
|
||||||
|
<div class="news-title">${rankEmoji} ${escapeHtml(item.title)}</div>
|
||||||
|
<div class="news-meta">
|
||||||
|
<div class="news-author">👤 ${escapeHtml(item.author)}</div>
|
||||||
|
<div class="news-time">🕒 ${formattedTime}</div>
|
||||||
|
</div>
|
||||||
|
<a href="${item.link}" target="_blank" class="news-link">
|
||||||
|
🚀 阅读全文
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return newsItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化评分
|
||||||
|
function formatScore(score) {
|
||||||
|
if (score >= 1000) {
|
||||||
|
return (score / 1000).toFixed(1) + 'K';
|
||||||
|
} else {
|
||||||
|
return score.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
function formatTime(timeStr) {
|
||||||
|
try {
|
||||||
|
const date = new Date(timeStr);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now - date;
|
||||||
|
const minutes = Math.floor(diff / (1000 * 60));
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days}天前`;
|
||||||
|
} else if (hours > 0) {
|
||||||
|
return `${hours}小时前`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}分钟前`;
|
||||||
|
} else {
|
||||||
|
return '刚刚';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return timeStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML转义
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新刷新时间
|
||||||
|
function updateRefreshTime() {
|
||||||
|
const now = new Date();
|
||||||
|
const timeStr = now.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
updateTimeElement.textContent = `最后更新: ${timeStr}`;
|
||||||
|
|
||||||
|
// 添加成功提示
|
||||||
|
showSuccessMessage('🌈 数据已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
function showSuccessMessage(message) {
|
||||||
|
// 移除之前的提示
|
||||||
|
const existingToast = document.querySelector('.success-toast');
|
||||||
|
if (existingToast) {
|
||||||
|
existingToast.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'success-toast';
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: linear-gradient(135deg, #ff6b6b, #4ecdc4, #45b7d1);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
box-shadow: 0 4px 20px rgba(255, 107, 107, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9em;
|
||||||
|
animation: rainbowToastSlide 0.5s ease-out;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
// 3秒后自动移除
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.animation = 'rainbowToastSlideOut 0.5s ease-in forwards';
|
||||||
|
setTimeout(() => toast.remove(), 500);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
function showLoading() {
|
||||||
|
loadingElement.style.display = 'block';
|
||||||
|
newsListElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏加载状态
|
||||||
|
function hideLoading() {
|
||||||
|
loadingElement.style.display = 'none';
|
||||||
|
newsListElement.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误信息
|
||||||
|
function showError() {
|
||||||
|
errorMessageElement.style.display = 'block';
|
||||||
|
newsListElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏错误信息
|
||||||
|
function hideError() {
|
||||||
|
errorMessageElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加CSS动画到页面
|
||||||
|
if (!document.querySelector('#toast-styles')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'toast-styles';
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes rainbowToastSlide {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100px) scale(0.8);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rainbowToastSlideOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0) scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100px) scale(0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-toast {
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: rainbowToastSlide 0.5s ease-out, toastRainbow 2s ease-in-out infinite !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toastRainbow {
|
||||||
|
0%, 100% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动刷新 (每5分钟)
|
||||||
|
setInterval(function() {
|
||||||
|
loadNewsList();
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
// 键盘快捷键支持
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'r' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
loadNewsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数字键切换标签
|
||||||
|
if (e.key >= '1' && e.key <= '3') {
|
||||||
|
e.preventDefault();
|
||||||
|
const typeMap = { '1': 'top', '2': 'new', '3': 'best' };
|
||||||
|
const targetType = typeMap[e.key];
|
||||||
|
const targetBtn = document.querySelector(`[data-type="${targetType}"]`);
|
||||||
|
if (targetBtn && targetType !== currentType) {
|
||||||
|
currentType = targetType;
|
||||||
|
updateActiveTab(targetBtn);
|
||||||
|
loadNewsList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user