chore: sync local changes (2026-03-12)

This commit is contained in:
2026-03-12 18:58:26 +08:00
parent 04a4cb962a
commit 939442e061
348 changed files with 91638 additions and 92091 deletions

View File

@@ -1,36 +0,0 @@
# Node modules
InfoGenie-frontend/node_modules
InfoGenie-frontend/build
# Python cache
InfoGenie-backend/__pycache__
InfoGenie-backend/**/__pycache__
InfoGenie-backend/*.pyc
InfoGenie-backend/**/*.pyc
# Git
.git
.gitignore
# IDE
.vscode
.idea
*.swp
*.swo
# Logs
*.log
# OS
.DS_Store
Thumbs.db
# Test files
InfoGenie-backend/test
# Documentation
*.md
# Backup files
*.backup
*.bak

5
.gitignore vendored
View File

@@ -1,5 +0,0 @@
#项目自忽略
.vscode
InfoGenie-frontend/node_modules
InfoGenie-frontend/build
InfoGenie-backend/__pycache__

View File

@@ -1,4 +1,4 @@
{ {
"git.ignoreLimitWarning": true, "git.ignoreLimitWarning": true,
"terminal.integrated.defaultProfile.windows": "Command Prompt" "terminal.integrated.defaultProfile.windows": "Command Prompt"
} }

View File

@@ -1,57 +0,0 @@
# InfoGenie 统一 Docker 镜像
# 多阶段构建:前端构建 + 后端 + Nginx
# 阶段1: 前端构建
FROM node:18-alpine AS frontend-builder
WORKDIR /frontend
COPY InfoGenie-frontend/package*.json ./
RUN npm install --legacy-peer-deps
COPY InfoGenie-frontend/ ./
RUN npm run build
# 阶段2: 最终镜像
FROM python:3.10-slim
# 安装 Nginx 和必要的工具
RUN apt-get update && apt-get install -y \
nginx \
supervisor \
&& rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /app
# 复制后端代码
COPY InfoGenie-backend/ ./backend/
# 安装 Python 依赖
RUN pip install --no-cache-dir -r ./backend/requirements.txt gunicorn
# 复制前端构建产物到 Nginx 目录
COPY --from=frontend-builder /frontend/build /usr/share/nginx/html
# 创建持久化数据目录
RUN mkdir -p /app/data/logs
# 复制 Nginx 配置
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/default.conf /etc/nginx/conf.d/default.conf
# 复制 Supervisor 配置
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# 复制启动脚本
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# 暴露端口
EXPOSE 2323
# 设置环境变量
ENV FLASK_APP=app.py
ENV FLASK_ENV=production
ENV PYTHONUNBUFFERED=1
# 使用 supervisor 管理多进程
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,19 @@
# Python cache
__pycache__
# Git
.git
.gitignore
# IDE
.vscode
.idea
*.swp
*.swo
# Test files
InfoGenie-backend/test

3
InfoGenie-backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
#项目自忽略
.vscode
__pycache__

View File

@@ -0,0 +1,30 @@
# InfoGenie 后端 Docker 镜像
# 仅包含后端服务,使用 Gunicorn
FROM python:3.10-slim
# 安装 curl 用于健康检查
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /app
# 复制依赖文件
COPY requirements.txt .
# 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt gunicorn
# 复制后端代码
COPY . .
# 创建持久化数据目录
RUN mkdir -p /app/data/logs
# 暴露端口
EXPOSE 2323
# 使用 Gunicorn 启动应用
CMD ["gunicorn", "--bind", "0.0.0.0:2323", "--workers", "4", "--threads", "2", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "app:app"]

View File

@@ -1,13 +1,13 @@
{ {
"deepseek": { "deepseek": {
"api_key": "sk-832f8e5250464de08a31523c7fd712", "api_key": "sk-832f8e5250464de08a31523c7fd712",
"api_base": "https://api.deepseek.com", "api_base": "https://api.deepseek.com",
"model": ["deepseek-chat","deepseek-reasoner"] "model": ["deepseek-chat","deepseek-reasoner"]
}, },
"kimi": { "kimi": {
"api_key": "sk-zdg9NBpTlhOcDDpoWfaBKu0KNDdGv18SipORnL2utawja", "api_key": "sk-zdg9NBpTlhOcDDpoWfaBKu0KNDdGv18SipORnL2utawja",
"api_base": "https://api.moonshot.cn", "api_base": "https://api.moonshot.cn",
"model": ["kimi-k2-0905-preview","kimi-k2-0711-preview"] "model": ["kimi-k2-0905-preview","kimi-k2-0711-preview"]
} }
} }

View File

@@ -178,7 +178,9 @@ def create_app():
return app return app
# 为 Gunicorn 创建应用实例
app = create_app()
if __name__ == '__main__': if __name__ == '__main__':
app = create_app()
print("🚀 启动 InfoGenie 后端服务...") print("🚀 启动 InfoGenie 后端服务...")
app.run(debug=True, host='0.0.0.0', port=5002) app.run(debug=True, host='0.0.0.0', port=5002)

View File

@@ -1,85 +1,85 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
InfoGenie 配置文件 InfoGenie 配置文件
Created by: 万象口袋 Created by: 万象口袋
Date: 2025-09-02 Date: 2025-09-02
""" """
import os import os
from datetime import timedelta from datetime import timedelta
from dotenv import load_dotenv from dotenv import load_dotenv
# 加载环境变量 # 加载环境变量
load_dotenv() load_dotenv()
class Config: class Config:
"""应用配置类""" """应用配置类"""
# 基础配置 # 基础配置
SECRET_KEY = os.environ.get('SECRET_KEY') or 'infogenie-secret-key-2025' SECRET_KEY = os.environ.get('SECRET_KEY') or 'infogenie-secret-key-2025'
# MongoDB 配置 # MongoDB 配置
MONGO_URI = os.environ.get('MONGO_URI') or 'mongodb://localhost:27017/InfoGenie' MONGO_URI = os.environ.get('MONGO_URI') or 'mongodb://localhost:27017/InfoGenie'
# hwt 配置 # hwt 配置
HWT_LIFETIME = timedelta(days=7) # hwt持续7天 HWT_LIFETIME = timedelta(days=7) # hwt持续7天
HWT_SECURE = False # 开发环境设为False生产环境设为True HWT_SECURE = False # 开发环境设为False生产环境设为True
HWT_HTTPONLY = True HWT_HTTPONLY = True
HWT_SAMESITE = 'Lax' HWT_SAMESITE = 'Lax'
HWT_DOMAIN = None # 开发环境设为None生产环境设为具体域名 HWT_DOMAIN = None # 开发环境设为None生产环境设为具体域名
HWT_PATH = '/' HWT_PATH = '/'
HWT_REFRESH_EACH_REQUEST = True # 每次请求刷新hwt过期时间 HWT_REFRESH_EACH_REQUEST = True # 每次请求刷新hwt过期时间
# 邮件配置 # 邮件配置
MAIL_SERVER = 'smtp.qq.com' MAIL_SERVER = 'smtp.qq.com'
MAIL_PORT = 465 MAIL_PORT = 465
MAIL_USE_SSL = True MAIL_USE_SSL = True
MAIL_USE_TLS = False MAIL_USE_TLS = False
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'your-email@qq.com' MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'your-email@qq.com'
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'your-app-password' 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') MAIL_DEFAULT_SENDER = ('InfoGenie 万象口袋', os.environ.get('MAIL_USERNAME') or 'your-email@qq.com')
# API 配置 # API 配置
API_RATE_LIMIT = '100 per hour' # API调用频率限制 API_RATE_LIMIT = '100 per hour' # API调用频率限制
# 外部API配置 # 外部API配置
EXTERNAL_APIS = { EXTERNAL_APIS = {
'60s': [ '60s': [
'https://60s.api.shumengya.top' 'https://60s.api.shumengya.top'
] ]
} }
# 应用信息 # 应用信息
APP_INFO = { APP_INFO = {
'name': '✨ 万象口袋 ✨', 'name': '✨ 万象口袋 ✨',
'description': '🎨 一个多功能的聚合软件应用 💬', 'description': '🎨 一个多功能的聚合软件应用 💬',
'author': '👨‍💻 by-万象口袋', 'author': '👨‍💻 by-万象口袋',
'version': '1.0.0', 'version': '1.0.0',
'icp': '📄 蜀ICP备2025151694号' 'icp': '📄 蜀ICP备2025151694号'
} }
class DevelopmentConfig(Config): class DevelopmentConfig(Config):
"""开发环境配置""" """开发环境配置"""
DEBUG = True DEBUG = True
TESTING = False TESTING = False
class ProductionConfig(Config): class ProductionConfig(Config):
"""生产环境配置""" """生产环境配置"""
DEBUG = False DEBUG = False
TESTING = False TESTING = False
HWT_SECURE = True HWT_SECURE = True
class TestingConfig(Config): class TestingConfig(Config):
"""测试环境配置""" """测试环境配置"""
DEBUG = True DEBUG = True
TESTING = True TESTING = True
MONGO_URI = 'mongodb://localhost:27017/InfoGenie_Test' MONGO_URI = 'mongodb://localhost:27017/InfoGenie_Test'
# 配置字典 # 配置字典
config = { config = {
'development': DevelopmentConfig, 'development': DevelopmentConfig,
'production': ProductionConfig, 'production': ProductionConfig,
'testing': TestingConfig, 'testing': TestingConfig,
'default': DevelopmentConfig 'default': DevelopmentConfig
} }

View File

@@ -1,36 +1,39 @@
version: '3.8' version: '3.8'
services: services:
infogenie: infogenie-backend:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: infogenie container_name: infogenie-backend
restart: always restart: always
ports: ports:
- "2323:2323" - "2323:2323"
volumes: volumes:
# 持久化数据映射 # 持久化数据映射
- /shumengya/docker/storage/infogenie/logs:/app/data/logs - /shumengya/docker/infogenie-backend/data:/app/data
- /shumengya/docker/storage/infogenie/data:/app/data
environment: environment:
# 从 .env 文件读取环境变量 # 从 .env 文件读取环境变量
- MONGO_URI=${MONGO_URI} - MONGO_URI=${MONGO_URI}
- MAIL_USERNAME=${MAIL_USERNAME} - MAIL_USERNAME=${MAIL_USERNAME}
- MAIL_PASSWORD=${MAIL_PASSWORD} - MAIL_PASSWORD=${MAIL_PASSWORD}
- SECRET_KEY=${SECRET_KEY} - SECRET_KEY=${SECRET_KEY}
- FLASK_ENV=production - FLASK_ENV=production
env_file: # 生产环境配置
- ./InfoGenie-backend/.env - HWT_DOMAIN=.shumengya.top
networks: - HWT_SECURE=False # 如果使用 HTTPS 反向代理,设为 False直接 HTTPS 设为 True
- infogenie-network env_file:
healthcheck: - .env
test: ["CMD", "curl", "-f", "http://localhost:2323/api/health"] networks:
interval: 30s - infogenie-network
timeout: 10s healthcheck:
retries: 3 test: ["CMD-SHELL", "curl -f http://localhost:2323/api/health || exit 1"]
start_period: 40s interval: 30s
timeout: 10s
networks: retries: 3
infogenie-network: start_period: 40s
driver: bridge
networks:
infogenie-network:
driver: bridge

File diff suppressed because it is too large Load Diff

View File

@@ -1,455 +1,455 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
用户认证模块 用户认证模块
Created by: 万象口袋 Created by: 万象口袋
Date: 2025-09-02 Date: 2025-09-02
""" """
from flask import Blueprint, request, jsonify, current_app from flask import Blueprint, request, jsonify, current_app
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
import hashlib import hashlib
import re import re
import jwt import jwt
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import wraps from functools import wraps
from .email_service import send_verification_email, verify_code, is_qq_email, get_qq_avatar_url from .email_service import send_verification_email, verify_code, is_qq_email, get_qq_avatar_url
auth_bp = Blueprint('auth', __name__) auth_bp = Blueprint('auth', __name__)
#生成JWT token #生成JWT token
def generate_token(user_data): def generate_token(user_data):
"""生成JWT token""" """生成JWT token"""
payload = { payload = {
'user_id': user_data['user_id'], 'user_id': user_data['user_id'],
'email': user_data['email'], 'email': user_data['email'],
'username': user_data['username'], 'username': user_data['username'],
'exp': datetime.utcnow() + timedelta(days=7), # 7天过期 'exp': datetime.utcnow() + timedelta(days=7), # 7天过期
'iat': datetime.utcnow() 'iat': datetime.utcnow()
} }
return jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256') return jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
#验证JWT token #验证JWT token
def verify_token(token): def verify_token(token):
"""验证JWT token""" """验证JWT token"""
try: try:
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
return {'success': True, 'data': payload} return {'success': True, 'data': payload}
except jwt.ExpiredSignatureError: except jwt.ExpiredSignatureError:
return {'success': False, 'message': 'Token已过期'} return {'success': False, 'message': 'Token已过期'}
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
return {'success': False, 'message': 'Token无效'} return {'success': False, 'message': 'Token无效'}
#JWT token验证装饰器 #JWT token验证装饰器
def token_required(f): def token_required(f):
"""JWT token验证装饰器""" """JWT token验证装饰器"""
@wraps(f) @wraps(f)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
token = request.headers.get('Authorization') token = request.headers.get('Authorization')
if not token: if not token:
return jsonify({'success': False, 'message': '缺少认证token'}), 401 return jsonify({'success': False, 'message': '缺少认证token'}), 401
if token.startswith('Bearer '): if token.startswith('Bearer '):
token = token[7:] token = token[7:]
result = verify_token(token) result = verify_token(token)
if not result['success']: if not result['success']:
return jsonify({'success': False, 'message': result['message']}), 401 return jsonify({'success': False, 'message': result['message']}), 401
request.current_user = result['data'] request.current_user = result['data']
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated return decorated
#验证QQ邮箱格式 #验证QQ邮箱格式
def validate_qq_email(email): def validate_qq_email(email):
"""验证QQ邮箱格式""" """验证QQ邮箱格式"""
return is_qq_email(email) return is_qq_email(email)
#验证密码格式 #验证密码格式
def validate_password(password): def validate_password(password):
"""验证密码格式6-20位""" """验证密码格式6-20位"""
return 6 <= len(password) <= 20 return 6 <= len(password) <= 20
#==========================对外暴露的HTTP接口========================== #==========================对外暴露的HTTP接口==========================
#发送验证码邮件 #发送验证码邮件
@auth_bp.route('/send-verification', methods=['POST']) @auth_bp.route('/send-verification', methods=['POST'])
def send_verification(): def send_verification():
"""发送验证码邮件""" """发送验证码邮件"""
try: try:
data = request.get_json() data = request.get_json()
email = data.get('email', '').strip() email = data.get('email', '').strip()
verification_type = data.get('type', 'register') # register, login verification_type = data.get('type', 'register') # register, login
# 参数验证 # 参数验证
if not email: if not email:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '邮箱地址不能为空' 'message': '邮箱地址不能为空'
}), 400 }), 400
if not validate_qq_email(email): if not validate_qq_email(email):
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '仅支持QQ邮箱qq.com、vip.qq.com、foxmail.com' 'message': '仅支持QQ邮箱qq.com、vip.qq.com、foxmail.com'
}), 400 }), 400
# 获取数据库集合 # 获取数据库集合
db = current_app.mongo.db db = current_app.mongo.db
users_collection = db.userdata users_collection = db.userdata
# 检查邮箱是否已注册 # 检查邮箱是否已注册
existing_user = users_collection.find_one({'邮箱': email}) existing_user = users_collection.find_one({'邮箱': email})
if verification_type == 'register' and existing_user: if verification_type == 'register' and existing_user:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '该邮箱已被注册' 'message': '该邮箱已被注册'
}), 409 }), 409
if verification_type == 'login' and not existing_user: if verification_type == 'login' and not existing_user:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '该邮箱尚未注册' 'message': '该邮箱尚未注册'
}), 404 }), 404
# 发送验证码 # 发送验证码
result = send_verification_email(email, verification_type) result = send_verification_email(email, verification_type)
if result['success']: if result['success']:
return jsonify(result), 200 return jsonify(result), 200
else: else:
return jsonify(result), 500 return jsonify(result), 500
except Exception as e: except Exception as e:
current_app.logger.error(f"发送验证码失败: {str(e)}") current_app.logger.error(f"发送验证码失败: {str(e)}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '发送失败,请稍后重试' 'message': '发送失败,请稍后重试'
}), 500 }), 500
#验证验证码 #验证验证码
@auth_bp.route('/verify-code', methods=['POST']) @auth_bp.route('/verify-code', methods=['POST'])
def verify_verification_code(): def verify_verification_code():
"""验证验证码""" """验证验证码"""
try: try:
data = request.get_json() data = request.get_json()
email = data.get('email', '').strip() email = data.get('email', '').strip()
code = data.get('code', '').strip() code = data.get('code', '').strip()
# 参数验证 # 参数验证
if not email or not code: if not email or not code:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '邮箱和验证码不能为空' 'message': '邮箱和验证码不能为空'
}), 400 }), 400
# 验证码校验 # 验证码校验
result = verify_code(email, code) result = verify_code(email, code)
if result['success']: if result['success']:
return jsonify(result), 200 return jsonify(result), 200
else: else:
return jsonify(result), 400 return jsonify(result), 400
except Exception as e: except Exception as e:
current_app.logger.error(f"验证码校验失败: {str(e)}") current_app.logger.error(f"验证码校验失败: {str(e)}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '验证失败,请稍后重试' 'message': '验证失败,请稍后重试'
}), 500 }), 500
#用户注册 #用户注册
@auth_bp.route('/register', methods=['POST']) @auth_bp.route('/register', methods=['POST'])
def register(): def register():
"""用户注册(需要先验证邮箱)""" """用户注册(需要先验证邮箱)"""
try: try:
data = request.get_json() data = request.get_json()
email = data.get('email', '').strip() email = data.get('email', '').strip()
username = data.get('username', '').strip() username = data.get('username', '').strip()
password = data.get('password', '').strip() password = data.get('password', '').strip()
code = data.get('code', '').strip() code = data.get('code', '').strip()
# 参数验证 # 参数验证
if not all([email, username, password, code]): if not all([email, username, password, code]):
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '所有字段都不能为空' 'message': '所有字段都不能为空'
}), 400 }), 400
if not validate_qq_email(email): if not validate_qq_email(email):
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '仅支持QQ邮箱注册' 'message': '仅支持QQ邮箱注册'
}), 400 }), 400
if not validate_password(password): if not validate_password(password):
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '密码长度必须在6-20位之间' 'message': '密码长度必须在6-20位之间'
}), 400 }), 400
# 验证验证码 # 验证验证码
verify_result = verify_code(email, code) verify_result = verify_code(email, code)
if not verify_result['success'] or verify_result.get('type') != 'register': if not verify_result['success'] or verify_result.get('type') != 'register':
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '验证码无效或已过期' 'message': '验证码无效或已过期'
}), 400 }), 400
# 获取数据库集合 # 获取数据库集合
db = current_app.mongo.db db = current_app.mongo.db
users_collection = db.userdata users_collection = db.userdata
# 检查邮箱是否已被注册 # 检查邮箱是否已被注册
if users_collection.find_one({'邮箱': email}): if users_collection.find_one({'邮箱': email}):
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '该邮箱已被注册' 'message': '该邮箱已被注册'
}), 409 }), 409
# 检查用户名是否已被使用 # 检查用户名是否已被使用
if users_collection.find_one({'用户名': username}): if users_collection.find_one({'用户名': username}):
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '该用户名已被使用' 'message': '该用户名已被使用'
}), 409 }), 409
# 获取QQ头像 # 获取QQ头像
avatar_url = get_qq_avatar_url(email) avatar_url = get_qq_avatar_url(email)
# 创建新用户 # 创建新用户
password_hash = generate_password_hash(password) password_hash = generate_password_hash(password)
user_data = { user_data = {
'邮箱': email, '邮箱': email,
'用户名': username, '用户名': username,
'密码': password_hash, '密码': password_hash,
'头像': avatar_url, '头像': avatar_url,
'注册时间': datetime.now().isoformat(), '注册时间': datetime.now().isoformat(),
'最后登录': None, '最后登录': None,
'登录次数': 0, '登录次数': 0,
'用户状态': 'active', '用户状态': 'active',
'等级': 0, '等级': 0,
'经验': 0, '经验': 0,
'萌芽币': 0, '萌芽币': 0,
'签到系统': { '签到系统': {
'连续签到天数': 0, '连续签到天数': 0,
'今日是否已签到': False, '今日是否已签到': False,
'签到时间': '2025-01-01' '签到时间': '2025-01-01'
} }
} }
result = users_collection.insert_one(user_data) result = users_collection.insert_one(user_data)
if result.inserted_id: if result.inserted_id:
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '注册成功!', 'message': '注册成功!',
'user': { 'user': {
'email': email, 'email': email,
'username': username, 'username': username,
'avatar': avatar_url 'avatar': avatar_url
} }
}), 201 }), 201
else: else:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '注册失败,请稍后重试' 'message': '注册失败,请稍后重试'
}), 500 }), 500
except Exception as e: except Exception as e:
current_app.logger.error(f"注册失败: {str(e)}") current_app.logger.error(f"注册失败: {str(e)}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '注册失败,请稍后重试' 'message': '注册失败,请稍后重试'
}), 500 }), 500
#用户登录 #用户登录
@auth_bp.route('/login', methods=['POST']) @auth_bp.route('/login', methods=['POST'])
def login(): def login():
"""用户登录(支持邮箱+验证码或邮箱+密码)""" """用户登录(支持邮箱+验证码或邮箱+密码)"""
try: try:
data = request.get_json() data = request.get_json()
email = data.get('email', '').strip() email = data.get('email', '').strip()
password = data.get('password', '').strip() password = data.get('password', '').strip()
code = data.get('code', '').strip() code = data.get('code', '').strip()
# 参数验证 # 参数验证
if not email: if not email:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '邮箱地址不能为空' 'message': '邮箱地址不能为空'
}), 400 }), 400
if not validate_qq_email(email): if not validate_qq_email(email):
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '仅支持QQ邮箱登录' 'message': '仅支持QQ邮箱登录'
}), 400 }), 400
# 获取数据库集合 # 获取数据库集合
db = current_app.mongo.db db = current_app.mongo.db
users_collection = db.userdata users_collection = db.userdata
# 查找用户 # 查找用户
user = users_collection.find_one({'邮箱': email}) user = users_collection.find_one({'邮箱': email})
if not user: if not user:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '该邮箱尚未注册' 'message': '该邮箱尚未注册'
}), 404 }), 404
# 检查用户状态 # 检查用户状态
if user.get('用户状态') != 'active': if user.get('用户状态') != 'active':
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '账号已被禁用,请联系管理员' 'message': '账号已被禁用,请联系管理员'
}), 403 }), 403
# 验证方式:验证码登录或密码登录 # 验证方式:验证码登录或密码登录
if code: if code:
# 验证码登录 # 验证码登录
verify_result = verify_code(email, code) verify_result = verify_code(email, code)
if not verify_result['success'] or verify_result.get('type') != 'login': if not verify_result['success'] or verify_result.get('type') != 'login':
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '验证码无效或已过期' 'message': '验证码无效或已过期'
}), 400 }), 400
elif password: elif password:
# 密码登录 # 密码登录
if not check_password_hash(user['密码'], password): if not check_password_hash(user['密码'], password):
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '密码错误' 'message': '密码错误'
}), 401 }), 401
else: else:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '请输入密码或验证码' 'message': '请输入密码或验证码'
}), 400 }), 400
# 登录成功,更新用户信息 # 登录成功,更新用户信息
users_collection.update_one( users_collection.update_one(
{'邮箱': email}, {'邮箱': email},
{ {
'$set': {'最后登录': datetime.now().isoformat()}, '$set': {'最后登录': datetime.now().isoformat()},
'$inc': {'登录次数': 1} '$inc': {'登录次数': 1}
} }
) )
# 生成JWT token # 生成JWT token
user_data = { user_data = {
'user_id': str(user['_id']), 'user_id': str(user['_id']),
'email': email, 'email': email,
'username': user.get('用户名', '') 'username': user.get('用户名', '')
} }
token = generate_token(user_data) token = generate_token(user_data)
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '登录成功!', 'message': '登录成功!',
'token': token, 'token': token,
'user': { 'user': {
'id': str(user['_id']), 'id': str(user['_id']),
'email': email, 'email': email,
'username': user.get('用户名', ''), 'username': user.get('用户名', ''),
'avatar': user.get('头像', ''), 'avatar': user.get('头像', ''),
'login_count': user.get('登录次数', 0) + 1 'login_count': user.get('登录次数', 0) + 1
} }
}), 200 }), 200
except Exception as e: except Exception as e:
current_app.logger.error(f"登录失败: {str(e)}") current_app.logger.error(f"登录失败: {str(e)}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': '登录失败,请稍后重试' 'message': '登录失败,请稍后重试'
}), 500 }), 500
# 登录成功,创建会话 # 登录成功,创建会话
hwt = getattr(request, 'hwt', {}) hwt = getattr(request, 'hwt', {})
hwt['user_id'] = str(user['_id']) hwt['user_id'] = str(user['_id'])
hwt['account'] = user['账号'] hwt['account'] = user['账号']
hwt['logged_in'] = True hwt['logged_in'] = True
# 更新登录信息 # 更新登录信息
users_collection.update_one( users_collection.update_one(
{'_id': user['_id']}, {'_id': user['_id']},
{ {
'$set': {'最后登录': datetime.now().isoformat()}, '$set': {'最后登录': datetime.now().isoformat()},
'$inc': {'登录次数': 1} '$inc': {'登录次数': 1}
} }
) )
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '登录成功!', 'message': '登录成功!',
'user': { 'user': {
'account': user['账号'], 'account': user['账号'],
'last_login': user.get('最后登录'), 'last_login': user.get('最后登录'),
'login_count': user.get('登录次数', 0) + 1 'login_count': user.get('登录次数', 0) + 1
} }
}), 200 }), 200
except Exception as e: except Exception as e:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': f'服务器错误: {str(e)}' 'message': f'服务器错误: {str(e)}'
}), 500 }), 500
#用户登出 #用户登出
@auth_bp.route('/logout', methods=['POST']) @auth_bp.route('/logout', methods=['POST'])
def logout(): def logout():
"""用户登出""" """用户登出"""
try: try:
# JWT是无状态的客户端删除token即可 # JWT是无状态的客户端删除token即可
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': '已成功登出' 'message': '已成功登出'
}), 200 }), 200
except Exception as e: except Exception as e:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': f'服务器错误: {str(e)}' 'message': f'服务器错误: {str(e)}'
}), 500 }), 500
#检查登录状态 #检查登录状态
@auth_bp.route('/check', methods=['GET']) @auth_bp.route('/check', methods=['GET'])
def check_login(): def check_login():
"""检查登录状态""" """检查登录状态"""
try: try:
token = request.headers.get('Authorization') token = request.headers.get('Authorization')
if not token: if not token:
return jsonify({ return jsonify({
'success': True, 'success': True,
'logged_in': False 'logged_in': False
}), 200 }), 200
if token.startswith('Bearer '): if token.startswith('Bearer '):
token = token[7:] token = token[7:]
result = verify_token(token) result = verify_token(token)
if result['success']: if result['success']:
user_data = result['data'] user_data = result['data']
return jsonify({ return jsonify({
'success': True, 'success': True,
'logged_in': True, 'logged_in': True,
'user': { 'user': {
'id': user_data['user_id'], 'id': user_data['user_id'],
'email': user_data['email'], 'email': user_data['email'],
'username': user_data['username'] 'username': user_data['username']
} }
}), 200 }), 200
else: else:
return jsonify({ return jsonify({
'success': True, 'success': True,
'logged_in': False 'logged_in': False
}), 200 }), 200
except Exception as e: except Exception as e:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': f'服务器错误: {str(e)}' 'message': f'服务器错误: {str(e)}'
}), 500 }), 500
#==========================对外暴露的HTTP接口========================== #==========================对外暴露的HTTP接口==========================

View File

@@ -1,283 +1,283 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
邮件发送模块 邮件发送模块
负责处理用户注册、登录验证邮件 负责处理用户注册、登录验证邮件
""" """
import random import random
import string import string
import smtplib import smtplib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.header import Header from email.header import Header
from flask import current_app from flask import current_app
import logging import logging
import os import os
# 验证码存储生产环境建议使用Redis # 验证码存储生产环境建议使用Redis
verification_codes = {} verification_codes = {}
# 初始化日志 # 初始化日志
def init_mail(app): def init_mail(app):
"""初始化邮件配置""" """初始化邮件配置"""
# 使用smtplib直接发送不需要Flask-Mail # 使用smtplib直接发送不需要Flask-Mail
pass pass
# 生成验证码 # 生成验证码
def generate_verification_code(length=6): def generate_verification_code(length=6):
"""生成验证码""" """生成验证码"""
return ''.join(random.choices(string.digits, k=length)) return ''.join(random.choices(string.digits, k=length))
# 发送验证邮件 # 发送验证邮件
def send_verification_email(email, verification_type='register'): def send_verification_email(email, verification_type='register'):
""" """
发送验证邮件 发送验证邮件
Args: Args:
email: 收件人邮箱 email: 收件人邮箱
verification_type: 验证类型 ('register', 'login', 'reset_password') verification_type: 验证类型 ('register', 'login', 'reset_password')
Returns: Returns:
dict: 发送结果 dict: 发送结果
""" """
try: try:
# 验证QQ邮箱格式 # 验证QQ邮箱格式
if not is_qq_email(email): if not is_qq_email(email):
return { return {
'success': False, 'success': False,
'message': '仅支持QQ邮箱注册登录' 'message': '仅支持QQ邮箱注册登录'
} }
# 生成验证码 # 生成验证码
code = generate_verification_code() code = generate_verification_code()
# 存储验证码5分钟有效期 # 存储验证码5分钟有效期
verification_codes[email] = { verification_codes[email] = {
'code': code, 'code': code,
'type': verification_type, 'type': verification_type,
'expires_at': datetime.now() + timedelta(minutes=5), 'expires_at': datetime.now() + timedelta(minutes=5),
'attempts': 0 'attempts': 0
} }
# 获取邮件配置 - 使用与QQEmailSendAPI相同的配置 # 获取邮件配置 - 使用与QQEmailSendAPI相同的配置
sender_email = os.environ.get('MAIL_USERNAME', '3205788256@qq.com') sender_email = os.environ.get('MAIL_USERNAME', '3205788256@qq.com')
sender_password = os.environ.get('MAIL_PASSWORD', 'szcaxvbftusqddhi') sender_password = os.environ.get('MAIL_PASSWORD', 'szcaxvbftusqddhi')
# 邮件模板 # 邮件模板
if verification_type == 'register': if verification_type == 'register':
subject = '【万象口袋】注册验证码' subject = '【万象口袋】注册验证码'
html_content = f''' html_content = f'''
<html> <html>
<body> <body>
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;"> <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="text-align: center; margin-bottom: 30px;"> <div style="text-align: center; margin-bottom: 30px;">
<h1 style="color: #66bb6a; margin: 0;">万象口袋</h1> <h1 style="color: #66bb6a; margin: 0;">万象口袋</h1>
<p style="color: #666; font-size: 14px; margin: 5px 0;">欢迎注册万象口袋</p> <p style="color: #666; font-size: 14px; margin: 5px 0;">欢迎注册万象口袋</p>
</div> </div>
<div style="background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 100%); padding: 30px; border-radius: 15px; text-align: center;"> <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> <h2 style="color: #2e7d32; margin-bottom: 20px;">验证码</h2>
<div style="background: white; padding: 20px; border-radius: 10px; margin: 20px 0;"> <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> <span style="font-size: 32px; font-weight: bold; color: #66bb6a; letter-spacing: 5px;">{code}</span>
</div> </div>
<p style="color: #4a4a4a; margin: 15px 0;">请在5分钟内输入此验证码完成注册</p> <p style="color: #4a4a4a; margin: 15px 0;">请在5分钟内输入此验证码完成注册</p>
</div> </div>
<div style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 10px;"> <div style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 10px;">
<p style="color: #666; font-size: 12px; margin: 0; text-align: center;"> <p style="color: #666; font-size: 12px; margin: 0; text-align: center;">
如果您没有申请注册,请忽略此邮件<br> 如果您没有申请注册,请忽略此邮件<br>
此验证码5分钟内有效请勿泄露给他人 此验证码5分钟内有效请勿泄露给他人
</p> </p>
</div> </div>
</div> </div>
</body> </body>
</html> </html>
''' '''
else: # login else: # login
subject = '【InfoGenie】登录验证码' subject = '【InfoGenie】登录验证码'
html_content = f''' html_content = f'''
<html> <html>
<body> <body>
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;"> <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="text-align: center; margin-bottom: 30px;"> <div style="text-align: center; margin-bottom: 30px;">
<h1 style="color: #66bb6a; margin: 0;">InfoGenie 万象口袋</h1> <h1 style="color: #66bb6a; margin: 0;">InfoGenie 万象口袋</h1>
<p style="color: #666; font-size: 14px; margin: 5px 0;">安全登录验证</p> <p style="color: #666; font-size: 14px; margin: 5px 0;">安全登录验证</p>
</div> </div>
<div style="background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%); padding: 30px; border-radius: 15px; text-align: center;"> <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> <h2 style="color: white; margin-bottom: 20px;">登录验证码</h2>
<div style="background: white; padding: 20px; border-radius: 10px; margin: 20px 0;"> <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> <span style="font-size: 32px; font-weight: bold; color: #66bb6a; letter-spacing: 5px;">{code}</span>
</div> </div>
<p style="color: white; margin: 15px 0;">请在5分钟内输入此验证码完成登录</p> <p style="color: white; margin: 15px 0;">请在5分钟内输入此验证码完成登录</p>
</div> </div>
<div style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 10px;"> <div style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 10px;">
<p style="color: #666; font-size: 12px; margin: 0; text-align: center;"> <p style="color: #666; font-size: 12px; margin: 0; text-align: center;">
如果不是您本人操作,请检查账户安全<br> 如果不是您本人操作,请检查账户安全<br>
此验证码5分钟内有效请勿泄露给他人 此验证码5分钟内有效请勿泄露给他人
</p> </p>
</div> </div>
</div> </div>
</body> </body>
</html> </html>
''' '''
# 创建邮件 - 使用与QQEmailSendAPI相同的方式 # 创建邮件 - 使用与QQEmailSendAPI相同的方式
message = MIMEText(html_content, 'html', 'utf-8') message = MIMEText(html_content, 'html', 'utf-8')
message['From'] = sender_email # 直接使用邮箱地址不使用Header包装 message['From'] = sender_email # 直接使用邮箱地址不使用Header包装
message['To'] = email message['To'] = email
message['Subject'] = Header(subject, 'utf-8') message['Subject'] = Header(subject, 'utf-8')
# 发送邮件 - 使用SSL端口465 # 发送邮件 - 使用SSL端口465
try: try:
# 使用与QQEmailSendAPI相同的连接方式 # 使用与QQEmailSendAPI相同的连接方式
smtp_obj = smtplib.SMTP_SSL('smtp.qq.com', 465) smtp_obj = smtplib.SMTP_SSL('smtp.qq.com', 465)
smtp_obj.login(sender_email, sender_password) smtp_obj.login(sender_email, sender_password)
smtp_obj.sendmail(sender_email, [email], message.as_string()) smtp_obj.sendmail(sender_email, [email], message.as_string())
smtp_obj.quit() smtp_obj.quit()
print(f"验证码邮件发送成功: {email}") print(f"验证码邮件发送成功: {email}")
return { return {
'success': True, 'success': True,
'message': '验证码已发送到您的邮箱', 'message': '验证码已发送到您的邮箱',
'email': email 'email': email
} }
except smtplib.SMTPAuthenticationError as auth_error: except smtplib.SMTPAuthenticationError as auth_error:
print(f"SMTP认证失败: {str(auth_error)}") print(f"SMTP认证失败: {str(auth_error)}")
return { return {
'success': False, 'success': False,
'message': 'SMTP认证失败请检查邮箱配置' 'message': 'SMTP认证失败请检查邮箱配置'
} }
except smtplib.SMTPConnectError as conn_error: except smtplib.SMTPConnectError as conn_error:
print(f"SMTP连接失败: {str(conn_error)}") print(f"SMTP连接失败: {str(conn_error)}")
return { return {
'success': False, 'success': False,
'message': 'SMTP服务器连接失败' 'message': 'SMTP服务器连接失败'
} }
except Exception as smtp_error: except Exception as smtp_error:
print(f"SMTP发送失败: {str(smtp_error)}") print(f"SMTP发送失败: {str(smtp_error)}")
return { return {
'success': False, 'success': False,
'message': f'邮件发送失败: {str(smtp_error)}' 'message': f'邮件发送失败: {str(smtp_error)}'
} }
except Exception as e: except Exception as e:
print(f"邮件发送失败: {str(e)}") print(f"邮件发送失败: {str(e)}")
return { return {
'success': False, 'success': False,
'message': '邮件发送失败,请稍后重试' 'message': '邮件发送失败,请稍后重试'
} }
# 验证验证码 # 验证验证码
def verify_code(email, code): def verify_code(email, code):
""" """
验证验证码 验证验证码
Args: Args:
email: 邮箱地址 email: 邮箱地址
code: 验证码 code: 验证码
Returns: Returns:
dict: 验证结果 dict: 验证结果
""" """
if email not in verification_codes: if email not in verification_codes:
return { return {
'success': False, 'success': False,
'message': '验证码不存在或已过期' 'message': '验证码不存在或已过期'
} }
stored_info = verification_codes[email] stored_info = verification_codes[email]
# 检查过期时间 # 检查过期时间
if datetime.now() > stored_info['expires_at']: if datetime.now() > stored_info['expires_at']:
del verification_codes[email] del verification_codes[email]
return { return {
'success': False, 'success': False,
'message': '验证码已过期,请重新获取' 'message': '验证码已过期,请重新获取'
} }
# 检查尝试次数 # 检查尝试次数
if stored_info['attempts'] >= 3: if stored_info['attempts'] >= 3:
del verification_codes[email] del verification_codes[email]
return { return {
'success': False, 'success': False,
'message': '验证码输入错误次数过多,请重新获取' 'message': '验证码输入错误次数过多,请重新获取'
} }
# 验证码校验 # 验证码校验
if stored_info['code'] != code: if stored_info['code'] != code:
stored_info['attempts'] += 1 stored_info['attempts'] += 1
return { return {
'success': False, 'success': False,
'message': f'验证码错误,还可尝试{3 - stored_info["attempts"]}' 'message': f'验证码错误,还可尝试{3 - stored_info["attempts"]}'
} }
# 验证成功,删除验证码 # 验证成功,删除验证码
verification_type = stored_info['type'] verification_type = stored_info['type']
del verification_codes[email] del verification_codes[email]
return { return {
'success': True, 'success': True,
'message': '验证码验证成功', 'message': '验证码验证成功',
'type': verification_type 'type': verification_type
} }
# 验证QQ邮箱格式 # 验证QQ邮箱格式
def is_qq_email(email): def is_qq_email(email):
""" """
验证是否为QQ邮箱 验证是否为QQ邮箱
Args: Args:
email: 邮箱地址 email: 邮箱地址
Returns: Returns:
bool: 是否为QQ邮箱 bool: 是否为QQ邮箱
""" """
if not email or '@' not in email: if not email or '@' not in email:
return False return False
domain = email.split('@')[1].lower() domain = email.split('@')[1].lower()
qq_domains = ['qq.com', 'vip.qq.com', 'foxmail.com'] qq_domains = ['qq.com', 'vip.qq.com', 'foxmail.com']
return domain in qq_domains return domain in qq_domains
# 获取QQ头像URL # 获取QQ头像URL
def get_qq_avatar_url(email): def get_qq_avatar_url(email):
""" """
根据QQ邮箱获取QQ头像URL 根据QQ邮箱获取QQ头像URL
Args: Args:
email: QQ邮箱地址 email: QQ邮箱地址
Returns: Returns:
str: QQ头像URL str: QQ头像URL
""" """
if not is_qq_email(email): if not is_qq_email(email):
return None return None
# 提取QQ号码 # 提取QQ号码
qq_number = email.split('@')[0] qq_number = email.split('@')[0]
# 验证是否为纯数字QQ号 # 验证是否为纯数字QQ号
if not qq_number.isdigit(): if not qq_number.isdigit():
return None return None
# 返回QQ头像API URL # 返回QQ头像API URL
return f"http://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100" return f"https://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100"
# 清理过期验证码 # 清理过期验证码
def cleanup_expired_codes(): def cleanup_expired_codes():
"""清理过期的验证码""" """清理过期的验证码"""
current_time = datetime.now() current_time = datetime.now()
expired_emails = [ expired_emails = [
email for email, info in verification_codes.items() email for email, info in verification_codes.items()
if current_time > info['expires_at'] if current_time > info['expires_at']
] ]
for email in expired_emails: for email in expired_emails:
del verification_codes[email] del verification_codes[email]
return len(expired_emails) return len(expired_emails)

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,29 @@
# InfoGenie 后端依赖包 # InfoGenie 后端依赖包
# Web框架 # Web框架
Flask==2.3.3 Flask==2.3.3
Flask-CORS==4.0.0 Flask-CORS==4.0.0
# 数据库 # 数据库
Flask-PyMongo==2.3.0 Flask-PyMongo==2.3.0
pymongo==4.5.0 pymongo==4.5.0
# 密码加密 # 密码加密
Werkzeug==2.3.7 Werkzeug==2.3.7
# JWT认证 # JWT认证
PyJWT==2.8.0 PyJWT==2.8.0
# HTTP请求 # HTTP请求
requests==2.31.0 requests==2.31.0
# 邮件发送 # 邮件发送
Flask-Mail==0.9.1 Flask-Mail==0.9.1
# 数据处理 # 数据处理
python-dateutil==2.8.2 python-dateutil==2.8.2
# 环境变量 # 环境变量
python-dotenv==1.0.0 python-dotenv==1.0.0
# 开发工具 # 开发工具
flask-limiter==3.5.0 # API限流 flask-limiter==3.5.0 # API限流

View File

@@ -1,2 +1,2 @@
@echo off @echo off
python app.py python app.py

View File

@@ -1,2 +1,2 @@
#!/bin/bash #!/bin/bash
python3 app.py python3 app.py

View File

@@ -1,34 +0,0 @@
#!/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()

View File

@@ -1,49 +0,0 @@
#!/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❌ 所有连接尝试都失败了")

View File

@@ -1,100 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
测试为指定账号增加萌芽币接口 (/api/user/add-coins)
"""
import os
import sys
import json
from datetime import datetime
# 加入后端根目录到路径导入create_app
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app import create_app
from modules.auth import generate_token
from werkzeug.security import generate_password_hash
def run_test():
"""运行加币接口测试,打印真实响应并断言结果"""
app = create_app()
with app.app_context():
db = app.mongo.db
users = db.userdata
# 构造一个临时测试用户(真实写库,测试结束删除)
test_email = "infogenie.test.addcoins@foxmail.com"
users.delete_many({'邮箱': test_email})
test_user = {
'邮箱': test_email,
'用户名': '测试用户_加币',
'密码': generate_password_hash('AddCoins123!'),
'头像': None,
'注册时间': datetime.now().isoformat(),
'最后登录': None,
'登录次数': 0,
'用户状态': 'active',
'等级': 0,
'经验': 0,
'萌芽币': 0,
'签到系统': {
'连续签到天数': 0,
'今日是否已签到': False,
'签到时间': datetime.now().strftime('%Y-%m-%d')
}
}
insert_result = users.insert_one(test_user)
test_user_id = str(insert_result.inserted_id)
# 生成有效JWT用于认证
token = generate_token({
'user_id': test_user_id,
'email': test_email,
'username': test_user['用户名']
})
client = app.test_client()
# 第一次加币: +500
resp1 = client.post(
'/api/user/add-coins',
headers={'Authorization': f'Bearer {token}'},
json={'email': test_email, 'amount': 500}
)
print('第一次加币 状态码:', resp1.status_code)
data1 = resp1.get_json()
print('第一次加币 响应:')
print(json.dumps(data1, ensure_ascii=False, indent=2))
assert resp1.status_code == 200
assert data1.get('success') is True
assert data1['data']['before_coins'] == 0
assert data1['data']['added'] == 500
assert data1['data']['new_coins'] == 500
# 第二次加币: +200
resp2 = client.post(
'/api/user/add-coins',
headers={'Authorization': f'Bearer {token}'},
json={'email': test_email, 'amount': 200}
)
print('第二次加币 状态码:', resp2.status_code)
data2 = resp2.get_json()
print('第二次加币 响应:')
print(json.dumps(data2, ensure_ascii=False, indent=2))
assert resp2.status_code == 200
assert data2.get('success') is True
assert data2['data']['before_coins'] == 500
assert data2['data']['added'] == 200
assert data2['data']['new_coins'] == 700
# 清理临时测试用户
users.delete_many({'邮箱': test_email})
if __name__ == '__main__':
print('🔧 开始测试 /api/user/add-coins 接口...')
run_test()
print('✅ 测试完成!')

View File

@@ -1,35 +0,0 @@
#!/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()

View File

@@ -1,81 +0,0 @@
#!/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测试完成!")

View File

@@ -1,70 +0,0 @@
#!/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()

View File

@@ -1,81 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
测试列出所有用户的HTTP接口 (/api/user/list)
"""
import os
import sys
import json
from datetime import datetime
# 将后端根目录加入路径便于导入app
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app import create_app
from modules.auth import generate_token
from werkzeug.security import generate_password_hash
def run_test():
"""运行用户列表接口测试,输出真实数据"""
# 使用.env中的真实Mongo配置不造假
app = create_app()
with app.app_context():
db = app.mongo.db
users = db.userdata
# 插入一个测试用户(真实写入后再删除),确保可验证接口输出
test_email = "infogenie.test.user@foxmail.com"
users.delete_many({'邮箱': test_email})
test_user = {
'邮箱': test_email,
'用户名': '测试用户_列表',
'密码': generate_password_hash('TestPass123!'),
'头像': None,
'注册时间': datetime.now().isoformat(),
'最后登录': None,
'登录次数': 0,
'用户状态': 'active',
'等级': 0,
'经验': 0,
'萌芽币': 0,
'签到系统': {
'连续签到天数': 0,
'今日是否已签到': False,
'签到时间': datetime.now().strftime('%Y-%m-%d')
}
}
insert_result = users.insert_one(test_user)
test_user_id = str(insert_result.inserted_id)
# 生成有效JWT满足认证要求
token = generate_token({
'user_id': test_user_id,
'email': test_email,
'username': test_user['用户名']
})
client = app.test_client()
resp = client.get('/api/user/list', headers={'Authorization': f'Bearer {token}'})
print("状态码:", resp.status_code)
data = resp.get_json()
print("响应内容:")
print(json.dumps(data, ensure_ascii=False, indent=2))
# 基本断言,确保返回真实列表数据且包含刚插入的测试用户
assert resp.status_code == 200
assert data.get('success') is True
assert isinstance(data.get('data'), list)
assert any(u.get('email') == test_email for u in data['data'])
# 清理测试数据
users.delete_many({'邮箱': test_email})
if __name__ == '__main__':
print('🔎 开始测试 /api/user/list 接口...')
run_test()
print('✅ 测试完成!')

View File

@@ -1,396 +1,396 @@
# InfoGenie 后端架构文档 # InfoGenie 后端架构文档
## 项目概述 ## 项目概述
InfoGenie万象口袋是一个基于前后端分离架构的多功能聚合软件应用。后端采用Flask框架提供RESTful API服务前端通过HTTP请求调用后端API实现数据交互和业务逻辑处理。 InfoGenie万象口袋是一个基于前后端分离架构的多功能聚合软件应用。后端采用Flask框架提供RESTful API服务前端通过HTTP请求调用后端API实现数据交互和业务逻辑处理。
## 技术栈 ## 技术栈
### 核心框架 ### 核心框架
- **Web框架**: Flask 2.3.3 - **Web框架**: Flask 2.3.3
- **数据库**: MongoDB (Flask-PyMongo 2.3.0) - **数据库**: MongoDB (Flask-PyMongo 2.3.0)
- **认证**: JWT (PyJWT 2.8.0) - **认证**: JWT (PyJWT 2.8.0)
- **跨域**: Flask-CORS 4.0.0 - **跨域**: Flask-CORS 4.0.0
### 辅助工具 ### 辅助工具
- **邮件服务**: Flask-Mail 0.9.1 - **邮件服务**: Flask-Mail 0.9.1
- **密码加密**: Werkzeug 2.3.7 - **密码加密**: Werkzeug 2.3.7
- **环境配置**: python-dotenv 1.0.0 - **环境配置**: python-dotenv 1.0.0
- **API限流**: Flask-Limiter 3.5.0 - **API限流**: Flask-Limiter 3.5.0
## 架构设计原则 ## 架构设计原则
### 前后端分离 ### 前后端分离
- 后端专注于数据处理和业务逻辑 - 后端专注于数据处理和业务逻辑
- 前端负责用户界面和交互体验 - 前端负责用户界面和交互体验
- 通过RESTful API进行数据交换 - 通过RESTful API进行数据交换
- 完全解耦,便于独立开发和部署 - 完全解耦,便于独立开发和部署
### 模块化设计 ### 模块化设计
- 按功能划分独立模块 - 按功能划分独立模块
- 每个模块职责单一 - 每个模块职责单一
- 便于维护和扩展 - 便于维护和扩展
## 核心模块详解 ## 核心模块详解
### 1. 认证模块 (auth.py) ### 1. 认证模块 (auth.py)
**功能职责**: **功能职责**:
- 用户注册和登录 - 用户注册和登录
- JWT Token生成和管理 - JWT Token生成和管理
- 邮箱验证码验证 - 邮箱验证码验证
- QQ邮箱格式验证 - QQ邮箱格式验证
**API端点**: **API端点**:
``` ```
POST /api/auth/send-verification # 发送验证码 POST /api/auth/send-verification # 发送验证码
POST /api/auth/verify-code # 验证验证码 POST /api/auth/verify-code # 验证验证码
POST /api/auth/register # 用户注册 POST /api/auth/register # 用户注册
POST /api/auth/login # 用户登录 POST /api/auth/login # 用户登录
POST /api/auth/logout # 用户登出 POST /api/auth/logout # 用户登出
GET /api/auth/check # 检查登录状态 GET /api/auth/check # 检查登录状态
``` ```
**数据流程**: **数据流程**:
1. 前端发送注册/登录请求 1. 前端发送注册/登录请求
2. 后端验证邮箱格式仅支持QQ邮箱 2. 后端验证邮箱格式仅支持QQ邮箱
3. 发送验证码邮件到用户邮箱 3. 发送验证码邮件到用户邮箱
4. 用户输入验证码完成验证 4. 用户输入验证码完成验证
5. 验证成功后生成JWT Token返回给前端 5. 验证成功后生成JWT Token返回给前端
**安全特性**: **安全特性**:
- 密码使用Werkzeug进行哈希加密 - 密码使用Werkzeug进行哈希加密
- JWT Token 7天有效期 - JWT Token 7天有效期
- 验证码5分钟有效期限制尝试次数 - 验证码5分钟有效期限制尝试次数
### 2. 用户管理模块 (user_management.py) ### 2. 用户管理模块 (user_management.py)
**功能职责**: **功能职责**:
- 用户资料管理 - 用户资料管理
- 密码修改 - 密码修改
- 每日签到系统 - 每日签到系统
- 用户游戏数据管理 - 用户游戏数据管理
- 账户删除 - 账户删除
**API端点**: **API端点**:
``` ```
GET /api/user/profile # 获取用户资料 GET /api/user/profile # 获取用户资料
POST /api/user/change-password # 修改密码 POST /api/user/change-password # 修改密码
GET /api/user/stats # 获取用户统计 GET /api/user/stats # 获取用户统计
GET /api/user/game-data # 获取游戏数据 GET /api/user/game-data # 获取游戏数据
POST /api/user/checkin # 每日签到 POST /api/user/checkin # 每日签到
POST /api/user/delete # 删除账户 POST /api/user/delete # 删除账户
``` ```
**数据结构**: **数据结构**:
```json ```json
{ {
"邮箱": "user@qq.com", "邮箱": "user@qq.com",
"用户名": "用户名", "用户名": "用户名",
"密码": "哈希密码", "密码": "哈希密码",
"头像": "QQ头像URL", "头像": "QQ头像URL",
"注册时间": "2025-01-01T00:00:00", "注册时间": "2025-01-01T00:00:00",
"最后登录": "2025-01-01T00:00:00", "最后登录": "2025-01-01T00:00:00",
"登录次数": 10, "登录次数": 10,
"用户状态": "active", "用户状态": "active",
"等级": 5, "等级": 5,
"经验": 1200, "经验": 1200,
"萌芽币": 1500, "萌芽币": 1500,
"签到系统": { "签到系统": {
"连续签到天数": 7, "连续签到天数": 7,
"今日是否已签到": true, "今日是否已签到": true,
"签到时间": "2025-01-01" "签到时间": "2025-01-01"
} }
} }
``` ```
**业务逻辑**: **业务逻辑**:
- 签到奖励300萌芽币 + 200经验 - 签到奖励300萌芽币 + 200经验
- 等级升级100 × 1.2^(等级) 经验需求 - 等级升级100 × 1.2^(等级) 经验需求
### 3. 邮件服务模块 (email_service.py) ### 3. 邮件服务模块 (email_service.py)
**功能职责**: **功能职责**:
- 验证码邮件发送 - 验证码邮件发送
- QQ邮箱格式验证 - QQ邮箱格式验证
- QQ头像获取 - QQ头像获取
- 邮件模板管理 - 邮件模板管理
**邮件模板**: **邮件模板**:
- 注册验证码邮件HTML格式 - 注册验证码邮件HTML格式
- 登录验证码邮件HTML格式 - 登录验证码邮件HTML格式
- 支持自定义邮件内容和样式 - 支持自定义邮件内容和样式
**安全考虑**: **安全考虑**:
- 仅支持QQ邮箱qq.com、vip.qq.com、foxmail.com - 仅支持QQ邮箱qq.com、vip.qq.com、foxmail.com
- 使用SSL加密连接 - 使用SSL加密连接
- 验证码存储在内存中生产环境建议使用Redis - 验证码存储在内存中生产环境建议使用Redis
### 4. AI模型应用模块 (aimodelapp.py) ### 4. AI模型应用模块 (aimodelapp.py)
**功能职责**: **功能职责**:
- 集成多种AI服务DeepSeek、Kimi - 集成多种AI服务DeepSeek、Kimi
- 提供AI功能API接口 - 提供AI功能API接口
- 统一AI接口调用 - 统一AI接口调用
- 管理用户萌芽币消费每次调用消耗100萌芽币 - 管理用户萌芽币消费每次调用消耗100萌芽币
**支持的AI功能**: **支持的AI功能**:
1. **AI聊天接口** (`/api/aimodelapp/chat`) 1. **AI聊天接口** (`/api/aimodelapp/chat`)
2. **姓名分析** (`/api/aimodelapp/name-analysis`) 2. **姓名分析** (`/api/aimodelapp/name-analysis`)
3. **变量命名助手** (`/api/aimodelapp/variable-naming`) 3. **变量命名助手** (`/api/aimodelapp/variable-naming`)
4. **AI写诗助手** (`/api/aimodelapp/poetry`) 4. **AI写诗助手** (`/api/aimodelapp/poetry`)
5. **AI语言翻译** (`/api/aimodelapp/translation`) 5. **AI语言翻译** (`/api/aimodelapp/translation`)
6. **现代文转文言文** (`/api/aimodelapp/classical_conversion`) 6. **现代文转文言文** (`/api/aimodelapp/classical_conversion`)
7. **AI表情制作器** (`/api/aimodelapp/expression-maker`) 7. **AI表情制作器** (`/api/aimodelapp/expression-maker`)
8. **Linux命令生成** (`/api/aimodelapp/linux-command`) 8. **Linux命令生成** (`/api/aimodelapp/linux-command`)
9. **获取可用模型** (`/api/aimodelapp/models`) 9. **获取可用模型** (`/api/aimodelapp/models`)
**AI配置**: **AI配置**:
```json ```json
{ {
"deepseek": { "deepseek": {
"api_key": "your-api-key", "api_key": "your-api-key",
"api_base": "https://api.deepseek.com", "api_base": "https://api.deepseek.com",
"model": ["deepseek-chat", "deepseek-reasoner"] "model": ["deepseek-chat", "deepseek-reasoner"]
}, },
"kimi": { "kimi": {
"api_key": "your-api-key", "api_key": "your-api-key",
"api_base": "https://api.moonshot.cn", "api_base": "https://api.moonshot.cn",
"model": ["kimi-k2-0905-preview", "kimi-k2-0711-preview"] "model": ["kimi-k2-0905-preview", "kimi-k2-0711-preview"]
} }
} }
``` ```
**调用流程**: **调用流程**:
1. 前端发送AI请求包含消息、模型提供商等参数 1. 前端发送AI请求包含消息、模型提供商等参数
2. 后端加载AI配置文件 2. 后端加载AI配置文件
3. 调用对应AI API带重试机制 3. 调用对应AI API带重试机制
4. 返回AI响应给前端 4. 返回AI响应给前端
## API设计规范 ## API设计规范
### 请求/响应格式 ### 请求/响应格式
**成功响应**: **成功响应**:
```json ```json
{ {
"success": true, "success": true,
"data": {...}, "data": {...},
"message": "操作成功", "message": "操作成功",
"timestamp": "2025-01-01T00:00:00" "timestamp": "2025-01-01T00:00:00"
} }
``` ```
**错误响应**: **错误响应**:
```json ```json
{ {
"success": false, "success": false,
"message": "错误信息", "message": "错误信息",
"error": "错误详情" "error": "错误详情"
} }
``` ```
### 认证方式 ### 认证方式
**JWT Token认证**: **JWT Token认证**:
``` ```
Authorization: Bearer <token> Authorization: Bearer <token>
``` ```
**支持的认证端点**: **支持的认证端点**:
- 所有 `/api/user/*` 端点需要认证 - 所有 `/api/user/*` 端点需要认证
- 部分 `/api/aimodelapp/*` 端点需要认证 - 部分 `/api/aimodelapp/*` 端点需要认证
### 错误处理 ### 错误处理
**HTTP状态码**: **HTTP状态码**:
- 200: 成功 - 200: 成功
- 400: 请求参数错误 - 400: 请求参数错误
- 401: 未认证/认证失败 - 401: 未认证/认证失败
- 403: 权限不足 - 403: 权限不足
- 404: 资源不存在 - 404: 资源不存在
- 409: 资源冲突 - 409: 资源冲突
- 500: 服务器内部错误 - 500: 服务器内部错误
## 数据库设计 ## 数据库设计
### MongoDB集合 ### MongoDB集合
**主要集合**: `userdata` **主要集合**: `userdata`
- 存储所有用户相关数据 - 存储所有用户相关数据
- 支持动态字段扩展 - 支持动态字段扩展
- 使用ObjectId作为用户唯一标识 - 使用ObjectId作为用户唯一标识
### 数据关系 ### 数据关系
- 用户数据自包含,无复杂关联 - 用户数据自包含,无复杂关联
- 通过用户ID进行数据关联 - 通过用户ID进行数据关联
- 支持水平扩展 - 支持水平扩展
## 部署和配置 ## 部署和配置
### 环境配置 ### 环境配置
**必需环境变量**: **必需环境变量**:
``` ```
SECRET_KEY=your-secret-key SECRET_KEY=your-secret-key
MONGO_URI=mongodb://localhost:27017/InfoGenie MONGO_URI=mongodb://localhost:27017/InfoGenie
MAIL_USERNAME=your-email@qq.com MAIL_USERNAME=your-email@qq.com
MAIL_PASSWORD=your-app-password MAIL_PASSWORD=your-app-password
``` ```
### 启动方式 ### 启动方式
**开发环境**: **开发环境**:
```bash ```bash
python app.py python app.py
``` ```
**生产环境**: **生产环境**:
- 支持Docker部署 - 支持Docker部署
- 提供docker-compose配置 - 提供docker-compose配置
- 支持Gunicorn WSGI服务器 - 支持Gunicorn WSGI服务器
### 静态文件服务 ### 静态文件服务
**支持的前端资源**: **支持的前端资源**:
- `/60sapi/*`: 60秒API相关文件 - `/60sapi/*`: 60秒API相关文件
- `/smallgame/*`: 小游戏相关文件 - `/smallgame/*`: 小游戏相关文件
- `/aimodelapp/*`: AI模型应用相关文件 - `/aimodelapp/*`: AI模型应用相关文件
## 安全考虑 ## 安全考虑
### 数据安全 ### 数据安全
- 密码哈希存储 - 密码哈希存储
- JWT Token安全传输 - JWT Token安全传输
- 输入数据验证和过滤 - 输入数据验证和过滤
### API安全 ### API安全
- CORS配置生产环境限制域名 - CORS配置生产环境限制域名
- API限流保护 - API限流保护
- 请求日志记录 - 请求日志记录
### 部署安全 ### 部署安全
- 环境变量管理敏感信息 - 环境变量管理敏感信息
- HTTPS证书配置 - HTTPS证书配置
- 防火墙和访问控制 - 防火墙和访问控制
## 前后端协作指南 ## 前后端协作指南
### 前端调用示例 ### 前端调用示例
**用户登录**: **用户登录**:
```javascript ```javascript
// 1. 发送验证码 // 1. 发送验证码
fetch('/api/auth/send-verification', { fetch('/api/auth/send-verification', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@qq.com', type: 'login' }) body: JSON.stringify({ email: 'user@qq.com', type: 'login' })
}); });
// 2. 验证验证码并登录 // 2. 验证验证码并登录
fetch('/api/auth/login', { fetch('/api/auth/login', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
email: 'user@qq.com', email: 'user@qq.com',
code: '123456' code: '123456'
}) })
}); });
// 3. 保存token到localStorage // 3. 保存token到localStorage
localStorage.setItem('token', response.token); localStorage.setItem('token', response.token);
``` ```
**调用需要认证的API**: **调用需要认证的API**:
```javascript ```javascript
fetch('/api/user/profile', { fetch('/api/user/profile', {
method: 'GET', method: 'GET',
headers: { headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}` 'Authorization': `Bearer ${localStorage.getItem('token')}`
} }
}); });
``` ```
### 数据约定 ### 数据约定
**前端发送数据格式**: **前端发送数据格式**:
- 所有请求使用JSON格式 - 所有请求使用JSON格式
- 必填字段验证 - 必填字段验证
- 参数命名使用snake_case - 参数命名使用snake_case
**后端返回数据格式**: **后端返回数据格式**:
- 统一响应格式 - 统一响应格式
- 时间戳使用ISO格式 - 时间戳使用ISO格式
- 错误信息清晰明确 - 错误信息清晰明确
### 开发协作流程 ### 开发协作流程
1. **API设计阶段**: 1. **API设计阶段**:
- 后端定义API接口规范 - 后端定义API接口规范
- 前端根据规范开发调用代码 - 前端根据规范开发调用代码
- 约定数据格式和错误处理 - 约定数据格式和错误处理
2. **联调阶段**: 2. **联调阶段**:
- 使用统一的测试数据 - 使用统一的测试数据
- 验证各种边界情况 - 验证各种边界情况
- 确认错误处理逻辑 - 确认错误处理逻辑
3. **部署阶段**: 3. **部署阶段**:
- 后端部署API服务 - 后端部署API服务
- 前端配置API基础URL - 前端配置API基础URL
- 验证跨域和认证配置 - 验证跨域和认证配置
## 新功能添加 ## 新功能添加
### 1. AI功能萌芽币消费系统 ### 1. AI功能萌芽币消费系统
**功能描述**: **功能描述**:
- 用户每次调用AI模型应用aimodelapp需消耗100萌芽币 - 用户每次调用AI模型应用aimodelapp需消耗100萌芽币
- 当用户萌芽币余额不足时无法使用AI功能 - 当用户萌芽币余额不足时无法使用AI功能
- 记录用户的AI使用历史 - 记录用户的AI使用历史
**API端点**: **API端点**:
``` ```
GET /api/aimodelapp/coins # 查询用户萌芽币余额和使用历史 GET /api/aimodelapp/coins # 查询用户萌芽币余额和使用历史
``` ```
**技术实现**: **技术实现**:
- 使用装饰器模式实现请求前验证和扣除萌芽币 - 使用装饰器模式实现请求前验证和扣除萌芽币
- 在MongoDB中记录用户AI使用历史 - 在MongoDB中记录用户AI使用历史
- 通过JWT Token验证用户身份 - 通过JWT Token验证用户身份
**业务逻辑**: **业务逻辑**:
1. 当用户请求AI功能时首先验证JWT Token 1. 当用户请求AI功能时首先验证JWT Token
2. 检查用户萌芽币余额是否≥100 2. 检查用户萌芽币余额是否≥100
3. 如余额充足先扣除萌芽币然后再调用AI服务 3. 如余额充足先扣除萌芽币然后再调用AI服务
4. 记录使用历史包括API类型、时间和消费萌芽币数量 4. 记录使用历史包括API类型、时间和消费萌芽币数量
5. 返回AI服务结果给用户 5. 返回AI服务结果给用户
**响应示例(查询萌芽币余额)**: **响应示例(查询萌芽币余额)**:
```json ```json
{ {
"success": true, "success": true,
"data": { "data": {
"coins": 200, "coins": 200,
"ai_cost": 100, "ai_cost": 100,
"can_use_ai": true, "can_use_ai": true,
"username": "用户名", "username": "用户名",
"usage_count": 1, "usage_count": 1,
"recent_usage": [ "recent_usage": [
{ {
"api_type": "chat", "api_type": "chat",
"cost": 100, "cost": 100,
"timestamp": "2025-09-16T11:15:47.285720" "timestamp": "2025-09-16T11:15:47.285720"
} }
] ]
}, },
"message": "当前萌芽币余额: 200" "message": "当前萌芽币余额: 200"
} }
``` ```
**前端开发注意事项**: **前端开发注意事项**:
- 每个需要调用AI功能的页面应首先检查用户萌芽币余额 - 每个需要调用AI功能的页面应首先检查用户萌芽币余额
- 当萌芽币不足时,向用户提示并引导用户通过签到等方式获取萌芽币 - 当萌芽币不足时,向用户提示并引导用户通过签到等方式获取萌芽币
- 可在UI中展示用户最近的AI使用记录和萌芽币消费情况 - 可在UI中展示用户最近的AI使用记录和萌芽币消费情况
--- ---

View File

@@ -1,13 +1,13 @@
{ {
"账号":"3205788256", "账号":"3205788256",
"邮箱":"3205788256@qq.com", "邮箱":"3205788256@qq.com",
"密码":"0123456789", "密码":"0123456789",
"等级":0, "等级":0,
"经验":0, "经验":0,
"萌芽币":0, "萌芽币":0,
"签到系统":{ "签到系统":{
"连续签到天数":0, "连续签到天数":0,
"今日是否已签到":false, "今日是否已签到":false,
"签到时间":"2025-01-01" "签到时间":"2025-01-01"
} }
} }

View File

@@ -1,4 +1,4 @@
@echo off @echo off
npm run build npm run build
npx serve -s build npx serve -s build
pause pause

View File

@@ -1,5 +1,5 @@
# 生产环境API配置 # 生产环境API配置
REACT_APP_API_URL=https://infogenie.api.shumengya.top REACT_APP_API_URL=https://infogenie.api.shumengya.top
# 生产环境API配置 # 生产环境API配置
REACT_APP_API_URL=http://127.0.0.1:5002 REACT_APP_API_URL=http://127.0.0.1:5002

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +1,55 @@
{ {
"name": "infogenie-frontend", "name": "infogenie-frontend",
"version": "1.0.0", "version": "1.0.0",
"description": "✨ 万象口袋 - 前端React应用", "description": "✨ 万象口袋 - 前端React应用",
"keywords": [ "keywords": [
"react", "react",
"api", "api",
"mobile-first", "mobile-first",
"responsive" "responsive"
], ],
"author": "万象口袋", "author": "万象口袋",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"homepage": "/", "homepage": "/",
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",
"axios": "^1.5.0", "axios": "^1.5.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-icons": "^4.11.0", "react-icons": "^4.11.0",
"react-router-dom": "^6.15.0", "react-router-dom": "^6.15.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"styled-components": "^6.0.7", "styled-components": "^6.0.7",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"dev": "react-scripts start" "dev": "react-scripts start"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app", "react-app",
"react-app/jest" "react-app/jest"
] ]
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
">0.2%", ">0.2%",
"not dead", "not dead",
"not op_mini all" "not op_mini all"
], ],
"development": [ "development": [
"last 1 chrome version", "last 1 chrome version",
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
} }
} }

View File

@@ -1,190 +1,190 @@
/* 背景样式文件 */ /* 背景样式文件 */
/* 主体背景 */ /* 主体背景 */
body { body {
background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 50%, #a5d6a7 100%); background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 50%, #a5d6a7 100%);
background-attachment: fixed; background-attachment: fixed;
background-size: 400% 400%; background-size: 400% 400%;
animation: gradientShift 15s ease infinite; animation: gradientShift 15s ease infinite;
} }
/* 背景动画 */ /* 背景动画 */
@keyframes gradientShift { @keyframes gradientShift {
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
} }
/* 装饰性背景元素 */ /* 装饰性背景元素 */
body::before { body::before {
content: ''; content: '';
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-image: background-image:
radial-gradient(circle at 20% 80%, rgba(76, 175, 80, 0.1) 0%, transparent 50%), 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 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%); radial-gradient(circle at 40% 40%, rgba(165, 214, 167, 0.08) 0%, transparent 50%);
pointer-events: none; pointer-events: none;
z-index: -1; z-index: -1;
} }
/* 浮动装饰圆点 */ /* 浮动装饰圆点 */
body::after { body::after {
content: ''; content: '';
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-image: background-image:
radial-gradient(2px 2px at 20px 30px, rgba(76, 175, 80, 0.3), transparent), 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(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 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(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); radial-gradient(2px 2px at 160px 30px, rgba(129, 199, 132, 0.3), transparent);
background-repeat: repeat; background-repeat: repeat;
background-size: 200px 100px; background-size: 200px 100px;
animation: float 20s linear infinite; animation: float 20s linear infinite;
pointer-events: none; pointer-events: none;
z-index: -1; z-index: -1;
opacity: 0.6; opacity: 0.6;
} }
@keyframes float { @keyframes float {
0% { 0% {
transform: translateY(0px); transform: translateY(0px);
} }
50% { 50% {
transform: translateY(-10px); transform: translateY(-10px);
} }
100% { 100% {
transform: translateY(0px); transform: translateY(0px);
} }
} }
/* 题目容器背景增强 */ /* 题目容器背景增强 */
.question-container { .question-container {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: box-shadow:
0 8px 32px rgba(26, 77, 26, 0.1), 0 8px 32px rgba(26, 77, 26, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.2); inset 0 1px 0 rgba(255, 255, 255, 0.2);
} }
/* 错误容器背景 */ /* 错误容器背景 */
.error-container { .error-container {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
} }
/* 结果容器背景 */ /* 结果容器背景 */
.result-container { .result-container {
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
} }
/* 代码块背景 */ /* 代码块背景 */
.code-block { .code-block {
background: rgba(248, 249, 250, 0.9); background: rgba(248, 249, 250, 0.9);
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
} }
/* 选项背景 */ /* 选项背景 */
.option { .option {
background: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
} }
.option:hover { .option:hover {
background: rgba(76, 175, 80, 0.05); background: rgba(76, 175, 80, 0.05);
} }
.option.selected { .option.selected {
background: linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(76, 175, 80, 0.08)); background: linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(76, 175, 80, 0.08));
} }
/* 按钮背景增强 */ /* 按钮背景增强 */
.submit-btn { .submit-btn {
background: linear-gradient(135deg, #4caf50, #45a049); background: linear-gradient(135deg, #4caf50, #45a049);
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.2); box-shadow: 0 4px 15px rgba(76, 175, 80, 0.2);
} }
.show-answer-btn { .show-answer-btn {
background: linear-gradient(135deg, #2196f3, #1976d2); background: linear-gradient(135deg, #2196f3, #1976d2);
box-shadow: 0 4px 15px rgba(33, 150, 243, 0.2); box-shadow: 0 4px 15px rgba(33, 150, 243, 0.2);
} }
.retry-btn { .retry-btn {
background: linear-gradient(135deg, #ff9800, #f57c00); background: linear-gradient(135deg, #ff9800, #f57c00);
box-shadow: 0 4px 15px rgba(255, 152, 0, 0.2); box-shadow: 0 4px 15px rgba(255, 152, 0, 0.2);
} }
.refresh-btn { .refresh-btn {
background: linear-gradient(135deg, #4caf50, #45a049); background: linear-gradient(135deg, #4caf50, #45a049);
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.2); box-shadow: 0 4px 15px rgba(76, 175, 80, 0.2);
} }
/* 移动端背景优化 */ /* 移动端背景优化 */
@media (max-width: 768px) { @media (max-width: 768px) {
body { body {
background-attachment: scroll; background-attachment: scroll;
} }
body::after { body::after {
opacity: 0.4; opacity: 0.4;
background-size: 150px 75px; background-size: 150px 75px;
} }
.question-container { .question-container {
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
} }
} }
/* 高对比度模式支持 */ /* 高对比度模式支持 */
@media (prefers-contrast: high) { @media (prefers-contrast: high) {
body { body {
background: #f0f8f0; background: #f0f8f0;
} }
body::before, body::before,
body::after { body::after {
display: none; display: none;
} }
.question-container { .question-container {
background: #ffffff; background: #ffffff;
border: 2px solid #4caf50; border: 2px solid #4caf50;
} }
} }
/* 减少动画模式支持 */ /* 减少动画模式支持 */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
body { body {
animation: none; animation: none;
background: #e8f5e8; background: #e8f5e8;
} }
body::after { body::after {
animation: none; animation: none;
} }
.refresh-btn:hover { .refresh-btn:hover {
transform: scale(1.1); transform: scale(1.1);
} }
} }

View File

@@ -1,91 +1,91 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>随机JavaScript趣味题</title> <title>随机JavaScript趣味题</title>
<link rel="preconnect" href="https://cdnjs.cloudflare.com"> <link rel="preconnect" href="https://cdnjs.cloudflare.com">
<link rel="dns-prefetch" href="https://60s.api.shumengya.top"> <link rel="dns-prefetch" href="https://60s.api.shumengya.top">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/background.css"> <link rel="stylesheet" href="css/background.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<header class="header"> <header class="header">
<h1>JavaScript趣味题</h1> <h1>JavaScript趣味题</h1>
<p class="subtitle">测试你的JavaScript知识</p> <p class="subtitle">测试你的JavaScript知识</p>
</header> </header>
<main class="main-content"> <main class="main-content">
<div class="loading" id="loading"> <div class="loading" id="loading">
<div class="spinner"></div> <div class="spinner"></div>
<p>正在加载题目...</p> <p>正在加载题目...</p>
</div> </div>
<div class="question-container" id="questionContainer" style="display: none;"> <div class="question-container" id="questionContainer" style="display: none;">
<div class="question-header"> <div class="question-header">
<span class="question-id" id="questionId">题目 #1</span> <span class="question-id" id="questionId">题目 #1</span>
<button class="refresh-btn" id="refreshBtn" title="获取新题目"> <button class="refresh-btn" id="refreshBtn" title="获取新题目">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></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> <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> </svg>
</button> </button>
</div> </div>
<div class="question-text" id="questionText"> <div class="question-text" id="questionText">
<h2>输出是什么?</h2> <h2>输出是什么?</h2>
</div> </div>
<div class="code-block" id="codeBlock"> <div class="code-block" id="codeBlock">
<pre><code id="codeContent" class="language-javascript"></code></pre> <pre><code id="codeContent" class="language-javascript"></code></pre>
</div> </div>
<div class="options-container" id="optionsContainer"> <div class="options-container" id="optionsContainer">
<!-- 选项将通过JavaScript动态生成 --> <!-- 选项将通过JavaScript动态生成 -->
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<button class="submit-btn" id="submitBtn" disabled>提交答案</button> <button class="submit-btn" id="submitBtn" disabled>提交答案</button>
<button class="show-answer-btn" id="showAnswerBtn">查看答案</button> <button class="show-answer-btn" id="showAnswerBtn">查看答案</button>
<button class="export-btn" id="exportBtn" title="导出为Markdown文件"> <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"> <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> <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> <polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line> <line x1="12" y1="15" x2="12" y2="3"></line>
</svg> </svg>
导出MD 导出MD
</button> </button>
</div> </div>
<div class="result-container" id="resultContainer" style="display: none;"> <div class="result-container" id="resultContainer" style="display: none;">
<div class="result-header"> <div class="result-header">
<span class="result-status" id="resultStatus"></span> <span class="result-status" id="resultStatus"></span>
<span class="correct-answer" id="correctAnswer"></span> <span class="correct-answer" id="correctAnswer"></span>
</div> </div>
<div class="explanation" id="explanation"> <div class="explanation" id="explanation">
<!-- 解析内容 --> <!-- 解析内容 -->
</div> </div>
</div> </div>
</div> </div>
<div class="error-container" id="errorContainer" style="display: none;"> <div class="error-container" id="errorContainer" style="display: none;">
<div class="error-icon">⚠️</div> <div class="error-icon">⚠️</div>
<h3>加载失败</h3> <h3>加载失败</h3>
<p id="errorMessage">网络连接异常,请稍后重试</p> <p id="errorMessage">网络连接异常,请稍后重试</p>
<button class="retry-btn" id="retryBtn">重新加载</button> <button class="retry-btn" id="retryBtn">重新加载</button>
</div> </div>
</main> </main>
<footer class="footer"> <footer class="footer">
<p>JavaScript趣味题集合</p> <p>JavaScript趣味题集合</p>
</footer> </footer>
</div> </div>
<script src="js/script.js"></script> <script src="js/script.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,3 +1,3 @@
[ [
"https://60s.api.shumengya.top" "https://60s.api.shumengya.top"
] ]

View File

@@ -1,17 +1,17 @@
{ {
"code": 200, "code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841", "message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": { "data": {
"id": 11, "id": 11,
"question": "输出是什么?", "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());", "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": [ "options": [
"A: `TypeError`", "A: `TypeError`",
"B: `SyntaxError`", "B: `SyntaxError`",
"C: `Lydia Hallie`", "C: `Lydia Hallie`",
"D: `undefined` `undefined`" "D: `undefined` `undefined`"
], ],
"answer": "A", "answer": "A",
"explanation": "你不能像常规对象那样,给构造函数添加属性。如果你想一次性给所有实例添加特性,你应该使用原型。因此本例中,使用如下方式:\n\n```js\nPerson.prototype.getFullName = function () {\n return `${this.firstName} ${this.lastName}`;\n}\n```\n\n这才会使 `member.getFullName()` 起作用。为什么这么做有益的?假设我们将这个方法添加到构造函数本身里。也许不是每个 `Person` 实例都需要这个方法。这将浪费大量内存空间,因为它们仍然具有该属性,这将占用每个实例的内存空间。相反,如果我们只将它添加到原型中,那么它只存在于内存中的一个位置,但是所有实例都可以访问它!" "explanation": "你不能像常规对象那样,给构造函数添加属性。如果你想一次性给所有实例添加特性,你应该使用原型。因此本例中,使用如下方式:\n\n```js\nPerson.prototype.getFullName = function () {\n return `${this.firstName} ${this.lastName}`;\n}\n```\n\n这才会使 `member.getFullName()` 起作用。为什么这么做有益的?假设我们将这个方法添加到构造函数本身里。也许不是每个 `Person` 实例都需要这个方法。这将浪费大量内存空间,因为它们仍然具有该属性,这将占用每个实例的内存空间。相反,如果我们只将它添加到原型中,那么它只存在于内存中的一个位置,但是所有实例都可以访问它!"
} }
} }

View File

@@ -1,81 +1,81 @@
/* 背景样式文件 */ /* 背景样式文件 */
body { body {
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 25%, #ffd3a5 50%, #a8e6cf 75%, #88d8a3 100%); background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 25%, #ffd3a5 50%, #a8e6cf 75%, #88d8a3 100%);
background-size: 400% 400%; background-size: 400% 400%;
animation: gradientShift 15s ease infinite; animation: gradientShift 15s ease infinite;
position: relative; position: relative;
} }
/* 背景动画 */ /* 背景动画 */
@keyframes gradientShift { @keyframes gradientShift {
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
} }
/* 背景装饰元素 */ /* 背景装饰元素 */
body::before { body::before {
content: ''; content: '';
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-image: background-image:
radial-gradient(circle at 20% 80%, rgba(39, 174, 96, 0.1) 0%, transparent 50%), 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 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%); radial-gradient(circle at 40% 40%, rgba(26, 188, 156, 0.05) 0%, transparent 50%);
pointer-events: none; pointer-events: none;
z-index: -1; z-index: -1;
} }
/* 浮动装饰圆点 */ /* 浮动装饰圆点 */
body::after { body::after {
content: ''; content: '';
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-image: background-image:
radial-gradient(2px 2px at 20px 30px, rgba(39, 174, 96, 0.3), transparent), 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(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 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(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); radial-gradient(2px 2px at 160px 30px, rgba(46, 204, 113, 0.3), transparent);
background-repeat: repeat; background-repeat: repeat;
background-size: 200px 100px; background-size: 200px 100px;
animation: floatDots 20s linear infinite; animation: floatDots 20s linear infinite;
pointer-events: none; pointer-events: none;
z-index: -1; z-index: -1;
} }
@keyframes floatDots { @keyframes floatDots {
0% { 0% {
transform: translateY(0px); transform: translateY(0px);
} }
100% { 100% {
transform: translateY(-100px); transform: translateY(-100px);
} }
} }
/* 响应式背景调整 */ /* 响应式背景调整 */
@media (max-width: 768px) { @media (max-width: 768px) {
body::after { body::after {
background-size: 150px 75px; background-size: 150px 75px;
animation-duration: 25s; animation-duration: 25s;
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
body::after { body::after {
background-size: 100px 50px; background-size: 100px 50px;
animation-duration: 30s; animation-duration: 30s;
} }
} }

View File

@@ -1,339 +1,339 @@
/* 基础样式重置 */ /* 基础样式重置 */
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif; font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6; line-height: 1.6;
color: #2c3e50; color: #2c3e50;
min-height: 100vh; min-height: 100vh;
overflow-x: hidden; overflow-x: hidden;
} }
/* 容器布局 */ /* 容器布局 */
.container { .container {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
} }
/* 头部样式 */ /* 头部样式 */
.header { .header {
text-align: center; text-align: center;
margin-bottom: 40px; margin-bottom: 40px;
padding: 30px 0; padding: 30px 0;
} }
.title { .title {
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 700; font-weight: 700;
color: #27ae60; color: #27ae60;
margin-bottom: 10px; margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(39, 174, 96, 0.2); text-shadow: 0 2px 4px rgba(39, 174, 96, 0.2);
} }
.subtitle { .subtitle {
font-size: 1.1rem; font-size: 1.1rem;
color: #7f8c8d; color: #7f8c8d;
font-weight: 400; font-weight: 400;
} }
/* 主要内容区域 */ /* 主要内容区域 */
.main { .main {
flex: 1; flex: 1;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.content-card { .content-card {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
border-radius: 20px; border-radius: 20px;
padding: 40px; padding: 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border: 1px solid rgba(39, 174, 96, 0.1); border: 1px solid rgba(39, 174, 96, 0.1);
width: 100%; width: 100%;
max-width: 600px; max-width: 600px;
transition: transform 0.3s ease, box-shadow 0.3s ease; transition: transform 0.3s ease, box-shadow 0.3s ease;
} }
.content-card:hover { .content-card:hover {
transform: translateY(-5px); transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15); box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
} }
/* KFC文案内容 */ /* KFC文案内容 */
.kfc-content { .kfc-content {
min-height: 200px; min-height: 200px;
padding: 30px; padding: 30px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 15px; border-radius: 15px;
border-left: 5px solid #27ae60; border-left: 5px solid #27ae60;
margin-bottom: 30px; margin-bottom: 30px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.kfc-content::before { .kfc-content::before {
content: '"'; content: '"';
position: absolute; position: absolute;
top: 10px; top: 10px;
left: 15px; left: 15px;
font-size: 3rem; font-size: 3rem;
color: #27ae60; color: #27ae60;
opacity: 0.3; opacity: 0.3;
font-family: serif; font-family: serif;
} }
.kfc-content p { .kfc-content p {
font-size: 1.1rem; font-size: 1.1rem;
line-height: 1.8; line-height: 1.8;
color: #2c3e50; color: #2c3e50;
margin-left: 20px; margin-left: 20px;
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.loading-text { .loading-text {
text-align: center; text-align: center;
color: #7f8c8d; color: #7f8c8d;
font-style: italic; font-style: italic;
} }
/* 按钮组 */ /* 按钮组 */
.button-group { .button-group {
display: flex; display: flex;
gap: 15px; gap: 15px;
justify-content: center; justify-content: center;
margin-bottom: 20px; margin-bottom: 20px;
} }
.generate-btn, .copy-btn { .generate-btn, .copy-btn {
padding: 15px 30px; padding: 15px 30px;
border: none; border: none;
border-radius: 50px; border-radius: 50px;
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.generate-btn { .generate-btn {
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%); background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
color: white; color: white;
box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3); box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3);
} }
.generate-btn:hover { .generate-btn:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(39, 174, 96, 0.4); box-shadow: 0 6px 20px rgba(39, 174, 96, 0.4);
} }
.generate-btn:active { .generate-btn:active {
transform: translateY(0); transform: translateY(0);
} }
.generate-btn:disabled { .generate-btn:disabled {
opacity: 0.7; opacity: 0.7;
cursor: not-allowed; cursor: not-allowed;
transform: none; transform: none;
} }
.copy-btn { .copy-btn {
background: linear-gradient(135deg, #3498db 0%, #5dade2 100%); background: linear-gradient(135deg, #3498db 0%, #5dade2 100%);
color: white; color: white;
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3); box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
} }
.copy-btn:hover { .copy-btn:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4); box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
} }
/* 编号信息 */ /* 编号信息 */
.index-info { .index-info {
text-align: center; text-align: center;
padding: 10px; padding: 10px;
background: rgba(39, 174, 96, 0.1); background: rgba(39, 174, 96, 0.1);
border-radius: 10px; border-radius: 10px;
border: 1px solid rgba(39, 174, 96, 0.2); border: 1px solid rgba(39, 174, 96, 0.2);
} }
.index-text { .index-text {
color: #27ae60; color: #27ae60;
font-weight: 600; font-weight: 600;
font-size: 0.9rem; font-size: 0.9rem;
} }
#indexNumber { #indexNumber {
color: #2c3e50; color: #2c3e50;
font-weight: 700; font-weight: 700;
} }
/* 底部 */ /* 底部 */
.footer { .footer {
text-align: center; text-align: center;
padding: 20px 0; padding: 20px 0;
color: #7f8c8d; color: #7f8c8d;
font-size: 0.9rem; font-size: 0.9rem;
margin-top: 40px; margin-top: 40px;
} }
/* 提示框 */ /* 提示框 */
.toast { .toast {
position: fixed; position: fixed;
top: 20px; top: 20px;
right: 20px; right: 20px;
background: #27ae60; background: #27ae60;
color: white; color: white;
padding: 15px 25px; padding: 15px 25px;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transform: translateX(400px); transform: translateX(400px);
transition: transform 0.3s ease; transition: transform 0.3s ease;
z-index: 1000; z-index: 1000;
font-weight: 600; font-weight: 600;
} }
.toast.show { .toast.show {
transform: translateX(0); transform: translateX(0);
} }
/* 平板端适配 (768px - 1024px) */ /* 平板端适配 (768px - 1024px) */
@media (max-width: 1024px) and (min-width: 768px) { @media (max-width: 1024px) and (min-width: 768px) {
.container { .container {
padding: 15px; padding: 15px;
} }
.title { .title {
font-size: 2.2rem; font-size: 2.2rem;
} }
.content-card { .content-card {
padding: 35px; padding: 35px;
max-width: 550px; max-width: 550px;
} }
.kfc-content { .kfc-content {
padding: 25px; padding: 25px;
min-height: 180px; min-height: 180px;
} }
.button-group { .button-group {
flex-direction: row; flex-direction: row;
gap: 12px; gap: 12px;
} }
.generate-btn, .copy-btn { .generate-btn, .copy-btn {
padding: 12px 25px; padding: 12px 25px;
font-size: 0.95rem; font-size: 0.95rem;
} }
} }
/* 手机端适配 (最大768px) */ /* 手机端适配 (最大768px) */
@media (max-width: 768px) { @media (max-width: 768px) {
.container { .container {
padding: 10px; padding: 10px;
} }
.header { .header {
margin-bottom: 30px; margin-bottom: 30px;
padding: 20px 0; padding: 20px 0;
} }
.title { .title {
font-size: 1.8rem; font-size: 1.8rem;
margin-bottom: 8px; margin-bottom: 8px;
} }
.subtitle { .subtitle {
font-size: 1rem; font-size: 1rem;
} }
.content-card { .content-card {
padding: 25px; padding: 25px;
margin: 0 5px; margin: 0 5px;
border-radius: 15px; border-radius: 15px;
} }
.kfc-content { .kfc-content {
padding: 20px; padding: 20px;
min-height: 150px; min-height: 150px;
margin-bottom: 25px; margin-bottom: 25px;
} }
.kfc-content::before { .kfc-content::before {
font-size: 2.5rem; font-size: 2.5rem;
top: 5px; top: 5px;
left: 10px; left: 10px;
} }
.kfc-content p { .kfc-content p {
font-size: 1rem; font-size: 1rem;
line-height: 1.7; line-height: 1.7;
margin-left: 15px; margin-left: 15px;
} }
.button-group { .button-group {
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.generate-btn, .copy-btn { .generate-btn, .copy-btn {
padding: 12px 20px; padding: 12px 20px;
font-size: 0.9rem; font-size: 0.9rem;
width: 100%; width: 100%;
} }
.footer { .footer {
font-size: 0.8rem; font-size: 0.8rem;
margin-top: 30px; margin-top: 30px;
} }
.toast { .toast {
right: 10px; right: 10px;
left: 10px; left: 10px;
transform: translateY(-100px); transform: translateY(-100px);
font-size: 0.9rem; font-size: 0.9rem;
} }
.toast.show { .toast.show {
transform: translateY(0); transform: translateY(0);
} }
} }
/* 小屏手机适配 (最大480px) */ /* 小屏手机适配 (最大480px) */
@media (max-width: 480px) { @media (max-width: 480px) {
.title { .title {
font-size: 1.6rem; font-size: 1.6rem;
} }
.content-card { .content-card {
padding: 20px; padding: 20px;
margin: 0; margin: 0;
} }
.kfc-content { .kfc-content {
padding: 15px; padding: 15px;
min-height: 120px; min-height: 120px;
} }
.kfc-content p { .kfc-content p {
font-size: 0.95rem; font-size: 0.95rem;
margin-left: 10px; margin-left: 10px;
} }
.generate-btn, .copy-btn { .generate-btn, .copy-btn {
padding: 10px 15px; padding: 10px 15px;
font-size: 0.85rem; font-size: 0.85rem;
} }
} }

View File

@@ -1,46 +1,46 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>随机KFC文案生成器</title> <title>随机KFC文案生成器</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/background.css"> <link rel="stylesheet" href="css/background.css">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<header class="header"> <header class="header">
<h1 class="title">🍗 随机KFC文案生成器</h1> <h1 class="title">🍗 随机KFC文案生成器</h1>
<p class="subtitle">疯狂星期四,文案来一套!</p> <p class="subtitle">疯狂星期四,文案来一套!</p>
</header> </header>
<main class="main"> <main class="main">
<div class="content-card"> <div class="content-card">
<div class="kfc-content" id="kfcContent"> <div class="kfc-content" id="kfcContent">
<p class="loading-text">点击按钮获取随机KFC文案...</p> <p class="loading-text">点击按钮获取随机KFC文案...</p>
</div> </div>
<div class="button-group"> <div class="button-group">
<button class="generate-btn" id="generateBtn"> <button class="generate-btn" id="generateBtn">
<span class="btn-text">生成文案</span> <span class="btn-text">生成文案</span>
<span class="btn-loading" style="display: none;">生成中...</span> <span class="btn-loading" style="display: none;">生成中...</span>
</button> </button>
<button class="copy-btn" id="copyBtn" style="display: none;">复制文案</button> <button class="copy-btn" id="copyBtn" style="display: none;">复制文案</button>
</div> </div>
<div class="index-info" id="indexInfo" style="display: none;"> <div class="index-info" id="indexInfo" style="display: none;">
<span class="index-text">文案编号: <span id="indexNumber"></span></span> <span class="index-text">文案编号: <span id="indexNumber"></span></span>
</div> </div>
</div> </div>
</main> </main>
<footer class="footer"> <footer class="footer">
<p>© 2024 KFC文案生成器 | 让每个星期四都疯狂起来</p> <p>© 2024 KFC文案生成器 | 让每个星期四都疯狂起来</p>
</footer> </footer>
</div> </div>
<div class="toast" id="toast"></div> <div class="toast" id="toast"></div>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,240 +1,240 @@
// KFC文案生成器主要功能 // KFC文案生成器主要功能
class KFCGenerator { class KFCGenerator {
constructor() { constructor() {
this.apiEndpoints = []; this.apiEndpoints = [];
this.currentApiIndex = 0; this.currentApiIndex = 0;
this.isLoading = false; this.isLoading = false;
this.init(); this.init();
} }
// 初始化 // 初始化
async init() { async init() {
await this.loadApiEndpoints(); await this.loadApiEndpoints();
this.bindEvents(); this.bindEvents();
} }
// 加载API接口列表 // 加载API接口列表
async loadApiEndpoints() { async loadApiEndpoints() {
try { try {
// 直接硬编码API端点避免CORS问题 // 直接硬编码API端点避免CORS问题
this.apiEndpoints = ["https://60s.api.shumengya.top"]; this.apiEndpoints = ["https://60s.api.shumengya.top"];
} catch (error) { } catch (error) {
console.error('加载API接口列表失败:', error); console.error('加载API接口列表失败:', error);
this.showToast('加载接口配置失败', 'error'); this.showToast('加载接口配置失败', 'error');
} }
} }
// 绑定事件 // 绑定事件
bindEvents() { bindEvents() {
const generateBtn = document.getElementById('generateBtn'); const generateBtn = document.getElementById('generateBtn');
const copyBtn = document.getElementById('copyBtn'); const copyBtn = document.getElementById('copyBtn');
generateBtn.addEventListener('click', () => this.generateKFC()); generateBtn.addEventListener('click', () => this.generateKFC());
copyBtn.addEventListener('click', () => this.copyContent()); copyBtn.addEventListener('click', () => this.copyContent());
} }
// 生成KFC文案 // 生成KFC文案
async generateKFC() { async generateKFC() {
if (this.isLoading) return; if (this.isLoading) return;
this.setLoadingState(true); this.setLoadingState(true);
let success = false; let success = false;
let attempts = 0; let attempts = 0;
const maxAttempts = this.apiEndpoints.length; const maxAttempts = this.apiEndpoints.length;
while (!success && attempts < maxAttempts) { while (!success && attempts < maxAttempts) {
try { try {
const apiUrl = this.apiEndpoints[this.currentApiIndex]; const apiUrl = this.apiEndpoints[this.currentApiIndex];
const data = await this.fetchKFCData(apiUrl); const data = await this.fetchKFCData(apiUrl);
if (data && data.code === 200 && data.data && data.data.kfc) { if (data && data.code === 200 && data.data && data.data.kfc) {
this.displayKFC(data.data); this.displayKFC(data.data);
success = true; success = true;
} else { } else {
throw new Error('API返回数据格式错误'); throw new Error('API返回数据格式错误');
} }
} catch (error) { } catch (error) {
console.error(`API ${this.currentApiIndex + 1} 请求失败:`, error); console.error(`API ${this.currentApiIndex + 1} 请求失败:`, error);
this.currentApiIndex = (this.currentApiIndex + 1) % this.apiEndpoints.length; this.currentApiIndex = (this.currentApiIndex + 1) % this.apiEndpoints.length;
attempts++; attempts++;
} }
} }
if (!success) { if (!success) {
this.showError('所有API接口都无法访问请稍后重试'); this.showError('所有API接口都无法访问请稍后重试');
} }
this.setLoadingState(false); this.setLoadingState(false);
} }
// 请求KFC数据 // 请求KFC数据
async fetchKFCData(apiUrl) { async fetchKFCData(apiUrl) {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时 const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
try { try {
const response = await fetch(`${apiUrl}/v2/kfc`, { const response = await fetch(`${apiUrl}/v2/kfc`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
signal: controller.signal signal: controller.signal
}); });
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`); throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} }
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
throw error; throw error;
} }
} }
// 显示KFC文案 // 显示KFC文案
displayKFC(data) { displayKFC(data) {
const contentElement = document.getElementById('kfcContent'); const contentElement = document.getElementById('kfcContent');
const indexElement = document.getElementById('indexNumber'); const indexElement = document.getElementById('indexNumber');
const indexInfo = document.getElementById('indexInfo'); const indexInfo = document.getElementById('indexInfo');
const copyBtn = document.getElementById('copyBtn'); const copyBtn = document.getElementById('copyBtn');
// 显示文案内容 // 显示文案内容
contentElement.innerHTML = `<p>${this.escapeHtml(data.kfc)}</p>`; contentElement.innerHTML = `<p>${this.escapeHtml(data.kfc)}</p>`;
// 显示编号信息 // 显示编号信息
if (data.index) { if (data.index) {
indexElement.textContent = data.index; indexElement.textContent = data.index;
indexInfo.style.display = 'block'; indexInfo.style.display = 'block';
} else { } else {
indexInfo.style.display = 'none'; indexInfo.style.display = 'none';
} }
// 显示复制按钮 // 显示复制按钮
copyBtn.style.display = 'inline-block'; copyBtn.style.display = 'inline-block';
// 添加显示动画 // 添加显示动画
contentElement.style.opacity = '0'; contentElement.style.opacity = '0';
contentElement.style.transform = 'translateY(20px)'; contentElement.style.transform = 'translateY(20px)';
setTimeout(() => { setTimeout(() => {
contentElement.style.transition = 'all 0.5s ease'; contentElement.style.transition = 'all 0.5s ease';
contentElement.style.opacity = '1'; contentElement.style.opacity = '1';
contentElement.style.transform = 'translateY(0)'; contentElement.style.transform = 'translateY(0)';
}, 100); }, 100);
} }
// 显示错误信息 // 显示错误信息
showError(message) { showError(message) {
const contentElement = document.getElementById('kfcContent'); const contentElement = document.getElementById('kfcContent');
contentElement.innerHTML = `<p class="loading-text" style="color: #e74c3c;">${this.escapeHtml(message)}</p>`; contentElement.innerHTML = `<p class="loading-text" style="color: #e74c3c;">${this.escapeHtml(message)}</p>`;
const copyBtn = document.getElementById('copyBtn'); const copyBtn = document.getElementById('copyBtn');
const indexInfo = document.getElementById('indexInfo'); const indexInfo = document.getElementById('indexInfo');
copyBtn.style.display = 'none'; copyBtn.style.display = 'none';
indexInfo.style.display = 'none'; indexInfo.style.display = 'none';
} }
// 复制文案内容 // 复制文案内容
async copyContent() { async copyContent() {
const contentElement = document.getElementById('kfcContent'); const contentElement = document.getElementById('kfcContent');
const textContent = contentElement.querySelector('p')?.textContent; const textContent = contentElement.querySelector('p')?.textContent;
if (!textContent || textContent.includes('点击按钮获取') || textContent.includes('失败')) { if (!textContent || textContent.includes('点击按钮获取') || textContent.includes('失败')) {
this.showToast('没有可复制的内容', 'error'); this.showToast('没有可复制的内容', 'error');
return; return;
} }
try { try {
if (navigator.clipboard && window.isSecureContext) { if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(textContent); await navigator.clipboard.writeText(textContent);
} else { } else {
// 降级方案 // 降级方案
const textArea = document.createElement('textarea'); const textArea = document.createElement('textarea');
textArea.value = textContent; textArea.value = textContent;
textArea.style.position = 'fixed'; textArea.style.position = 'fixed';
textArea.style.left = '-999999px'; textArea.style.left = '-999999px';
textArea.style.top = '-999999px'; textArea.style.top = '-999999px';
document.body.appendChild(textArea); document.body.appendChild(textArea);
textArea.focus(); textArea.focus();
textArea.select(); textArea.select();
document.execCommand('copy'); document.execCommand('copy');
textArea.remove(); textArea.remove();
} }
this.showToast('文案已复制到剪贴板', 'success'); this.showToast('文案已复制到剪贴板', 'success');
} catch (error) { } catch (error) {
console.error('复制失败:', error); console.error('复制失败:', error);
this.showToast('复制失败,请手动选择复制', 'error'); this.showToast('复制失败,请手动选择复制', 'error');
} }
} }
// 设置加载状态 // 设置加载状态
setLoadingState(loading) { setLoadingState(loading) {
this.isLoading = loading; this.isLoading = loading;
const generateBtn = document.getElementById('generateBtn'); const generateBtn = document.getElementById('generateBtn');
const btnText = generateBtn.querySelector('.btn-text'); const btnText = generateBtn.querySelector('.btn-text');
const btnLoading = generateBtn.querySelector('.btn-loading'); const btnLoading = generateBtn.querySelector('.btn-loading');
if (loading) { if (loading) {
generateBtn.disabled = true; generateBtn.disabled = true;
btnText.style.display = 'none'; btnText.style.display = 'none';
btnLoading.style.display = 'inline'; btnLoading.style.display = 'inline';
} else { } else {
generateBtn.disabled = false; generateBtn.disabled = false;
btnText.style.display = 'inline'; btnText.style.display = 'inline';
btnLoading.style.display = 'none'; btnLoading.style.display = 'none';
} }
} }
// 显示提示消息 // 显示提示消息
showToast(message, type = 'success') { showToast(message, type = 'success') {
const toast = document.getElementById('toast'); const toast = document.getElementById('toast');
toast.textContent = message; toast.textContent = message;
toast.className = `toast ${type}`; toast.className = `toast ${type}`;
toast.classList.add('show'); toast.classList.add('show');
setTimeout(() => { setTimeout(() => {
toast.classList.remove('show'); toast.classList.remove('show');
}, 3000); }, 3000);
} }
// HTML转义 // HTML转义
escapeHtml(text) { escapeHtml(text) {
const div = document.createElement('div'); const div = document.createElement('div');
div.textContent = text; div.textContent = text;
return div.innerHTML; return div.innerHTML;
} }
} }
// 页面加载完成后初始化 // 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const generator = new KFCGenerator(); const generator = new KFCGenerator();
// 页面加载完成后自动生成一条文案 // 页面加载完成后自动生成一条文案
setTimeout(() => { setTimeout(() => {
generator.generateKFC(); generator.generateKFC();
}, 1000); }, 1000);
}); });
// 添加键盘快捷键支持 // 添加键盘快捷键支持
document.addEventListener('keydown', (event) => { document.addEventListener('keydown', (event) => {
// 按空格键生成文案 // 按空格键生成文案
if (event.code === 'Space' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') { if (event.code === 'Space' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') {
event.preventDefault(); event.preventDefault();
document.getElementById('generateBtn').click(); document.getElementById('generateBtn').click();
} }
// Ctrl+C 复制文案 // Ctrl+C 复制文案
if (event.ctrlKey && event.key === 'c' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') { if (event.ctrlKey && event.key === 'c' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') {
const copyBtn = document.getElementById('copyBtn'); const copyBtn = document.getElementById('copyBtn');
if (copyBtn.style.display !== 'none') { if (copyBtn.style.display !== 'none') {
event.preventDefault(); event.preventDefault();
copyBtn.click(); copyBtn.click();
} }
} }
}); });

View File

@@ -1,3 +1,3 @@
[ [
"https://60s.api.shumengya.top" "https://60s.api.shumengya.top"
] ]

View File

@@ -1,8 +1,8 @@
{ {
"code": 200, "code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841", "message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": { "data": {
"index": 78, "index": 78,
"kfc": "我叫夯大力 立冬给我准备了糖炒栗子了没有 没准备的自动绝交 再 v 我 50 吃疯狂星期四 然后再给我点杯奶茶 再给我两万块钱 懂吗你们" "kfc": "我叫夯大力 立冬给我准备了糖炒栗子了没有 没准备的自动绝交 再 v 我 50 吃疯狂星期四 然后再给我点杯奶茶 再给我两万块钱 懂吗你们"
} }
} }

View File

@@ -1,167 +1,167 @@
/* 背景样式文件 - 金色光辉主题 */ /* 背景样式文件 - 金色光辉主题 */
/* 主背景 */ /* 主背景 */
body { body {
background: linear-gradient( background: linear-gradient(
135deg, 135deg,
#f1f8e9 0%, #f1f8e9 0%,
#dcedc8 25%, #dcedc8 25%,
#c8e6c8 50%, #c8e6c8 50%,
#a5d6a7 75%, #a5d6a7 75%,
#81c784 100% #81c784 100%
); );
background-size: 400% 400%; background-size: 400% 400%;
animation: gradientShift 15s ease infinite; animation: gradientShift 15s ease infinite;
position: relative; position: relative;
} }
/* 背景装饰层 */ /* 背景装饰层 */
body::before { body::before {
content: ''; content: '';
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: background:
radial-gradient(circle at 20% 80%, rgba(129, 199, 132, 0.1) 0%, transparent 50%), radial-gradient(circle at 20% 80%, rgba(129, 199, 132, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(165, 214, 167, 0.1) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(165, 214, 167, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(102, 187, 106, 0.05) 0%, transparent 50%); radial-gradient(circle at 40% 40%, rgba(102, 187, 106, 0.05) 0%, transparent 50%);
pointer-events: none; pointer-events: none;
z-index: 1; z-index: 1;
} }
/* 动态光点效果 */ /* 动态光点效果 */
body::after { body::after {
content: ''; content: '';
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-image: background-image:
radial-gradient(2px 2px at 20px 30px, rgba(129, 199, 132, 0.3), transparent), radial-gradient(2px 2px at 20px 30px, rgba(129, 199, 132, 0.3), transparent),
radial-gradient(2px 2px at 40px 70px, rgba(165, 214, 167, 0.2), transparent), radial-gradient(2px 2px at 40px 70px, rgba(165, 214, 167, 0.2), transparent),
radial-gradient(1px 1px at 90px 40px, rgba(102, 187, 106, 0.4), transparent), radial-gradient(1px 1px at 90px 40px, rgba(102, 187, 106, 0.4), transparent),
radial-gradient(1px 1px at 130px 80px, rgba(129, 199, 132, 0.2), transparent), radial-gradient(1px 1px at 130px 80px, rgba(129, 199, 132, 0.2), transparent),
radial-gradient(2px 2px at 160px 30px, rgba(165, 214, 167, 0.3), transparent); radial-gradient(2px 2px at 160px 30px, rgba(165, 214, 167, 0.3), transparent);
background-repeat: repeat; background-repeat: repeat;
background-size: 200px 100px; background-size: 200px 100px;
animation: sparkle 20s linear infinite; animation: sparkle 20s linear infinite;
pointer-events: none; pointer-events: none;
z-index: 2; z-index: 2;
} }
/* 背景动画 */ /* 背景动画 */
@keyframes gradientShift { @keyframes gradientShift {
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
} }
@keyframes sparkle { @keyframes sparkle {
0% { 0% {
transform: translateY(0); transform: translateY(0);
opacity: 1; opacity: 1;
} }
100% { 100% {
transform: translateY(-100vh); transform: translateY(-100vh);
opacity: 0; opacity: 0;
} }
} }
/* 响应式背景调整 */ /* 响应式背景调整 */
/* 平板端背景 */ /* 平板端背景 */
@media (min-width: 768px) and (max-width: 1024px) { @media (min-width: 768px) and (max-width: 1024px) {
body::after { body::after {
background-size: 250px 120px; background-size: 250px 120px;
} }
} }
/* 电脑端背景 */ /* 电脑端背景 */
@media (min-width: 1024px) { @media (min-width: 1024px) {
body { body {
background-size: 300% 300%; background-size: 300% 300%;
} }
body::after { body::after {
background-size: 300px 150px; background-size: 300px 150px;
animation-duration: 25s; animation-duration: 25s;
} }
} }
/* 手机端背景优化 */ /* 手机端背景优化 */
@media (max-width: 767px) { @media (max-width: 767px) {
body { body {
background-size: 200% 200%; background-size: 200% 200%;
animation-duration: 10s; animation-duration: 10s;
} }
body::before { body::before {
background: background:
radial-gradient(circle at 30% 70%, rgba(129, 199, 132, 0.08) 0%, transparent 40%), radial-gradient(circle at 30% 70%, rgba(129, 199, 132, 0.08) 0%, transparent 40%),
radial-gradient(circle at 70% 30%, rgba(165, 214, 167, 0.08) 0%, transparent 40%); radial-gradient(circle at 70% 30%, rgba(165, 214, 167, 0.08) 0%, transparent 40%);
} }
body::after { body::after {
background-size: 150px 80px; background-size: 150px 80px;
animation-duration: 15s; animation-duration: 15s;
} }
} }
/* 超小屏幕背景 */ /* 超小屏幕背景 */
@media (max-width: 479px) { @media (max-width: 479px) {
body { body {
background: linear-gradient( background: linear-gradient(
135deg, 135deg,
#f1f8e9 0%, #f1f8e9 0%,
#dcedc8 50%, #dcedc8 50%,
#c8e6c8 100% #c8e6c8 100%
); );
background-size: 150% 150%; background-size: 150% 150%;
} }
body::after { body::after {
background-size: 120px 60px; background-size: 120px 60px;
} }
} }
/* 深色模式支持 */ /* 深色模式支持 */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
body { body {
background: linear-gradient( background: linear-gradient(
135deg, 135deg,
#1b2e1b 0%, #1b2e1b 0%,
#2e4a2e 25%, #2e4a2e 25%,
#3e5e3e 50%, #3e5e3e 50%,
#4e6e4e 75%, #4e6e4e 75%,
#5e7e5e 100% #5e7e5e 100%
); );
} }
body::before { body::before {
background: background:
radial-gradient(circle at 20% 80%, rgba(129, 199, 132, 0.05) 0%, transparent 50%), radial-gradient(circle at 20% 80%, rgba(129, 199, 132, 0.05) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(165, 214, 167, 0.05) 0%, transparent 50%); radial-gradient(circle at 80% 20%, rgba(165, 214, 167, 0.05) 0%, transparent 50%);
} }
} }
/* 减少动画效果(用户偏好) */ /* 减少动画效果(用户偏好) */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
body, body,
body::before, body::before,
body::after { body::after {
animation: none; animation: none;
} }
body { body {
background: linear-gradient(135deg, #f1f8e9 0%, #dcedc8 50%, #c8e6c8 100%); background: linear-gradient(135deg, #f1f8e9 0%, #dcedc8 50%, #c8e6c8 100%);
} }
} }

View File

@@ -1,357 +1,357 @@
/* 基础样式重置 */ /* 基础样式重置 */
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif; font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6; line-height: 1.6;
color: #2e7d32; color: #2e7d32;
overflow-x: hidden; overflow-x: hidden;
} }
/* 容器布局 */ /* 容器布局 */
.container { .container {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 20px; padding: 20px;
position: relative; position: relative;
} }
/* 头部样式 */ /* 头部样式 */
.header { .header {
text-align: center; text-align: center;
margin-bottom: 40px; margin-bottom: 40px;
z-index: 10; z-index: 10;
} }
.title { .title {
font-size: 3rem; font-size: 3rem;
font-weight: 700; font-weight: 700;
color: #2e7d32; color: #2e7d32;
text-shadow: text-shadow:
0 0 10px rgba(129, 199, 132, 0.8), 0 0 10px rgba(129, 199, 132, 0.8),
0 0 20px rgba(129, 199, 132, 0.6), 0 0 20px rgba(129, 199, 132, 0.6),
0 0 30px rgba(129, 199, 132, 0.4); 0 0 30px rgba(129, 199, 132, 0.4);
margin-bottom: 10px; margin-bottom: 10px;
animation: titleGlow 3s ease-in-out infinite alternate; animation: titleGlow 3s ease-in-out infinite alternate;
} }
.subtitle { .subtitle {
font-size: 1.2rem; font-size: 1.2rem;
color: #388e3c; color: #388e3c;
opacity: 0.9; opacity: 0.9;
text-shadow: 0 0 5px rgba(102, 187, 106, 0.5); text-shadow: 0 0 5px rgba(102, 187, 106, 0.5);
} }
/* 主内容区域 */ /* 主内容区域 */
.main-content { .main-content {
width: 100%; width: 100%;
max-width: 800px; max-width: 800px;
z-index: 10; z-index: 10;
} }
/* 一言容器 */ /* 一言容器 */
.quote-container { .quote-container {
background: linear-gradient(135deg, rgba(129, 199, 132, 0.1), rgba(165, 214, 167, 0.05)); background: linear-gradient(135deg, rgba(129, 199, 132, 0.1), rgba(165, 214, 167, 0.05));
border: 2px solid rgba(102, 187, 106, 0.3); border: 2px solid rgba(102, 187, 106, 0.3);
border-radius: 20px; border-radius: 20px;
padding: 40px; padding: 40px;
margin-bottom: 30px; margin-bottom: 30px;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
box-shadow: box-shadow:
0 8px 32px rgba(102, 187, 106, 0.2), 0 8px 32px rgba(102, 187, 106, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.1); inset 0 1px 0 rgba(255, 255, 255, 0.1);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.quote-container::before { .quote-container::before {
content: ''; content: '';
position: absolute; position: absolute;
top: -2px; top: -2px;
left: -2px; left: -2px;
right: -2px; right: -2px;
bottom: -2px; bottom: -2px;
background: linear-gradient(45deg, #81c784, #a5d6a7, #81c784, #a5d6a7); background: linear-gradient(45deg, #81c784, #a5d6a7, #81c784, #a5d6a7);
border-radius: 22px; border-radius: 22px;
z-index: -1; z-index: -1;
animation: borderGlow 4s linear infinite; animation: borderGlow 4s linear infinite;
} }
/* 加载状态 */ /* 加载状态 */
.loading { .loading {
display: none; display: none;
text-align: center; text-align: center;
color: #2e7d32; color: #2e7d32;
} }
.loading.show { .loading.show {
display: block; display: block;
} }
.loading-spinner { .loading-spinner {
width: 40px; width: 40px;
height: 40px; height: 40px;
border: 4px solid rgba(102, 187, 106, 0.3); border: 4px solid rgba(102, 187, 106, 0.3);
border-top: 4px solid #2e7d32; border-top: 4px solid #2e7d32;
border-radius: 50%; border-radius: 50%;
margin: 0 auto 15px; margin: 0 auto 15px;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
/* 一言显示 */ /* 一言显示 */
.quote-display { .quote-display {
display: block; display: block;
text-align: center; text-align: center;
} }
.quote-display.hide { .quote-display.hide {
display: none; display: none;
} }
.quote-text { .quote-text {
font-size: 1.8rem; font-size: 1.8rem;
line-height: 1.8; line-height: 1.8;
color: #2e7d32; color: #2e7d32;
margin-bottom: 20px; margin-bottom: 20px;
text-shadow: 0 1px 2px rgba(102, 187, 106, 0.1); text-shadow: 0 1px 2px rgba(102, 187, 106, 0.1);
font-weight: 500; font-weight: 500;
} }
.quote-index { .quote-index {
font-size: 0.9rem; font-size: 0.9rem;
color: #388e3c; color: #388e3c;
opacity: 0.8; opacity: 0.8;
} }
/* 错误信息 */ /* 错误信息 */
.error-message { .error-message {
display: none; display: none;
text-align: center; text-align: center;
color: #66bb6a; color: #66bb6a;
} }
.error-message.show { .error-message.show {
display: block; display: block;
} }
.error-icon { .error-icon {
font-size: 2rem; font-size: 2rem;
margin-bottom: 10px; margin-bottom: 10px;
} }
.error-text { .error-text {
font-size: 1.1rem; font-size: 1.1rem;
line-height: 1.5; line-height: 1.5;
} }
/* 控制按钮 */ /* 控制按钮 */
.controls { .controls {
text-align: center; text-align: center;
} }
.refresh-btn { .refresh-btn {
background: linear-gradient(135deg, #81c784, #a5d6a7); background: linear-gradient(135deg, #81c784, #a5d6a7);
border: none; border: none;
border-radius: 50px; border-radius: 50px;
padding: 15px 30px; padding: 15px 30px;
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 600; font-weight: 600;
color: #2e7d32; color: #2e7d32;
cursor: pointer; cursor: pointer;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
transition: all 0.3s ease; transition: all 0.3s ease;
box-shadow: box-shadow:
0 4px 15px rgba(102, 187, 106, 0.3), 0 4px 15px rgba(102, 187, 106, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.3); inset 0 1px 0 rgba(255, 255, 255, 0.3);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.refresh-btn:hover { .refresh-btn:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: box-shadow:
0 6px 20px rgba(102, 187, 106, 0.4), 0 6px 20px rgba(102, 187, 106, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.3); inset 0 1px 0 rgba(255, 255, 255, 0.3);
} }
.refresh-btn:active { .refresh-btn:active {
transform: translateY(0); transform: translateY(0);
} }
.refresh-btn:disabled { .refresh-btn:disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
transform: none; transform: none;
} }
.btn-icon { .btn-icon {
font-size: 1.2rem; font-size: 1.2rem;
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
.refresh-btn:hover .btn-icon { .refresh-btn:hover .btn-icon {
transform: rotate(180deg); transform: rotate(180deg);
} }
/* 底部 */ /* 底部 */
.footer { .footer {
margin-top: 40px; margin-top: 40px;
text-align: center; text-align: center;
color: #388e3c; color: #388e3c;
opacity: 0.8; opacity: 0.8;
font-size: 0.9rem; font-size: 0.9rem;
} }
/* 动画效果 */ /* 动画效果 */
@keyframes titleGlow { @keyframes titleGlow {
0% { 0% {
text-shadow: text-shadow:
0 0 10px rgba(129, 199, 132, 0.8), 0 0 10px rgba(129, 199, 132, 0.8),
0 0 20px rgba(129, 199, 132, 0.6), 0 0 20px rgba(129, 199, 132, 0.6),
0 0 30px rgba(129, 199, 132, 0.4); 0 0 30px rgba(129, 199, 132, 0.4);
} }
100% { 100% {
text-shadow: text-shadow:
0 0 15px rgba(129, 199, 132, 1), 0 0 15px rgba(129, 199, 132, 1),
0 0 25px rgba(129, 199, 132, 0.8), 0 0 25px rgba(129, 199, 132, 0.8),
0 0 35px rgba(129, 199, 132, 0.6); 0 0 35px rgba(129, 199, 132, 0.6);
} }
} }
@keyframes borderGlow { @keyframes borderGlow {
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
/* 平板端适配 (768px - 1024px) */ /* 平板端适配 (768px - 1024px) */
@media (min-width: 768px) and (max-width: 1024px) { @media (min-width: 768px) and (max-width: 1024px) {
.container { .container {
padding: 30px; padding: 30px;
} }
.title { .title {
font-size: 3.5rem; font-size: 3.5rem;
} }
.subtitle { .subtitle {
font-size: 1.3rem; font-size: 1.3rem;
} }
.quote-container { .quote-container {
padding: 50px; padding: 50px;
} }
.quote-text { .quote-text {
font-size: 2rem; font-size: 2rem;
} }
} }
/* 电脑端适配 (1024px+) */ /* 电脑端适配 (1024px+) */
@media (min-width: 1024px) { @media (min-width: 1024px) {
.container { .container {
padding: 40px; padding: 40px;
} }
.title { .title {
font-size: 4rem; font-size: 4rem;
} }
.subtitle { .subtitle {
font-size: 1.4rem; font-size: 1.4rem;
} }
.quote-container { .quote-container {
padding: 60px; padding: 60px;
max-width: 900px; max-width: 900px;
} }
.quote-text { .quote-text {
font-size: 2.2rem; font-size: 2.2rem;
line-height: 1.9; line-height: 1.9;
} }
.refresh-btn { .refresh-btn {
padding: 18px 36px; padding: 18px 36px;
font-size: 1.2rem; font-size: 1.2rem;
} }
} }
/* 手机端适配 (小于768px) */ /* 手机端适配 (小于768px) */
@media (max-width: 767px) { @media (max-width: 767px) {
.container { .container {
padding: 15px; padding: 15px;
} }
.header { .header {
margin-bottom: 30px; margin-bottom: 30px;
} }
.title { .title {
font-size: 2.5rem; font-size: 2.5rem;
} }
.subtitle { .subtitle {
font-size: 1rem; font-size: 1rem;
} }
.quote-container { .quote-container {
padding: 25px; padding: 25px;
border-radius: 15px; border-radius: 15px;
} }
.quote-text { .quote-text {
font-size: 1.4rem; font-size: 1.4rem;
line-height: 1.6; line-height: 1.6;
} }
.refresh-btn { .refresh-btn {
padding: 12px 24px; padding: 12px 24px;
font-size: 1rem; font-size: 1rem;
} }
.footer { .footer {
margin-top: 30px; margin-top: 30px;
font-size: 0.8rem; font-size: 0.8rem;
} }
} }
/* 超小屏幕适配 (小于480px) */ /* 超小屏幕适配 (小于480px) */
@media (max-width: 479px) { @media (max-width: 479px) {
.title { .title {
font-size: 2rem; font-size: 2rem;
} }
.quote-container { .quote-container {
padding: 20px; padding: 20px;
} }
.quote-text { .quote-text {
font-size: 1.2rem; font-size: 1.2rem;
} }
.refresh-btn { .refresh-btn {
padding: 10px 20px; padding: 10px 20px;
font-size: 0.9rem; font-size: 0.9rem;
} }
} }

View File

@@ -1,52 +1,52 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>随机一言 - 金色光辉</title> <title>随机一言 - 金色光辉</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/background.css"> <link rel="stylesheet" href="css/background.css">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<header class="header"> <header class="header">
<h1 class="title">随机一言</h1> <h1 class="title">随机一言</h1>
<p class="subtitle">每一句话都是心灵的光芒</p> <p class="subtitle">每一句话都是心灵的光芒</p>
</header> </header>
<main class="main-content"> <main class="main-content">
<div class="quote-container"> <div class="quote-container">
<div class="loading" id="loading"> <div class="loading" id="loading">
<div class="loading-spinner"></div> <div class="loading-spinner"></div>
<p>正在获取一言...</p> <p>正在获取一言...</p>
</div> </div>
<div class="quote-display" id="quoteDisplay"> <div class="quote-display" id="quoteDisplay">
<div class="quote-text" id="quoteText"> <div class="quote-text" id="quoteText">
点击下方按钮获取一言 点击下方按钮获取一言
</div> </div>
<div class="quote-index" id="quoteIndex"></div> <div class="quote-index" id="quoteIndex"></div>
</div> </div>
<div class="error-message" id="errorMessage"> <div class="error-message" id="errorMessage">
<div class="error-icon">⚠️</div> <div class="error-icon">⚠️</div>
<div class="error-text" id="errorText"></div> <div class="error-text" id="errorText"></div>
</div> </div>
</div> </div>
<div class="controls"> <div class="controls">
<button class="refresh-btn" id="refreshBtn"> <button class="refresh-btn" id="refreshBtn">
<span class="btn-icon">🔄</span> <span class="btn-icon">🔄</span>
<span class="btn-text">获取新一言</span> <span class="btn-text">获取新一言</span>
</button> </button>
</div> </div>
</main> </main>
<footer class="footer"> <footer class="footer">
<p>愿每一句话都能温暖你的心</p> <p>愿每一句话都能温暖你的心</p>
</footer> </footer>
</div> </div>
<script src="js/script.js"></script> <script src="js/script.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,222 +1,222 @@
// 随机一言 JavaScript 功能实现 // 随机一言 JavaScript 功能实现
class HitokotoApp { class HitokotoApp {
constructor() { constructor() {
// API接口列表 // API接口列表
this.apiEndpoints = [ this.apiEndpoints = [
"https://60s.api.shumengya.top" "https://60s.api.shumengya.top"
]; ];
this.currentEndpointIndex = 0; this.currentEndpointIndex = 0;
this.isLoading = false; this.isLoading = false;
// DOM 元素 // DOM 元素
this.elements = { this.elements = {
loading: document.getElementById('loading'), loading: document.getElementById('loading'),
quoteDisplay: document.getElementById('quoteDisplay'), quoteDisplay: document.getElementById('quoteDisplay'),
quoteText: document.getElementById('quoteText'), quoteText: document.getElementById('quoteText'),
quoteIndex: document.getElementById('quoteIndex'), quoteIndex: document.getElementById('quoteIndex'),
errorMessage: document.getElementById('errorMessage'), errorMessage: document.getElementById('errorMessage'),
errorText: document.getElementById('errorText'), errorText: document.getElementById('errorText'),
refreshBtn: document.getElementById('refreshBtn') refreshBtn: document.getElementById('refreshBtn')
}; };
this.init(); this.init();
} }
// 初始化应用 // 初始化应用
init() { init() {
this.bindEvents(); this.bindEvents();
this.hideAllStates(); this.hideAllStates();
this.showQuoteDisplay(); this.showQuoteDisplay();
} }
// 绑定事件 // 绑定事件
bindEvents() { bindEvents() {
this.elements.refreshBtn.addEventListener('click', () => { this.elements.refreshBtn.addEventListener('click', () => {
this.fetchHitokoto(); this.fetchHitokoto();
}); });
// 键盘快捷键支持 // 键盘快捷键支持
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.code === 'Space' && !this.isLoading) { if (e.code === 'Space' && !this.isLoading) {
e.preventDefault(); e.preventDefault();
this.fetchHitokoto(); this.fetchHitokoto();
} }
}); });
} }
// 隐藏所有状态 // 隐藏所有状态
hideAllStates() { hideAllStates() {
this.elements.loading.classList.remove('show'); this.elements.loading.classList.remove('show');
this.elements.quoteDisplay.classList.remove('hide'); this.elements.quoteDisplay.classList.remove('hide');
this.elements.errorMessage.classList.remove('show'); this.elements.errorMessage.classList.remove('show');
} }
// 显示加载状态 // 显示加载状态
showLoading() { showLoading() {
this.hideAllStates(); this.hideAllStates();
this.elements.loading.classList.add('show'); this.elements.loading.classList.add('show');
this.elements.quoteDisplay.classList.add('hide'); this.elements.quoteDisplay.classList.add('hide');
this.elements.refreshBtn.disabled = true; this.elements.refreshBtn.disabled = true;
this.isLoading = true; this.isLoading = true;
} }
// 显示一言内容 // 显示一言内容
showQuoteDisplay() { showQuoteDisplay() {
this.hideAllStates(); this.hideAllStates();
this.elements.quoteDisplay.classList.remove('hide'); this.elements.quoteDisplay.classList.remove('hide');
this.elements.refreshBtn.disabled = false; this.elements.refreshBtn.disabled = false;
this.isLoading = false; this.isLoading = false;
} }
// 显示错误信息 // 显示错误信息
showError(message) { showError(message) {
this.hideAllStates(); this.hideAllStates();
this.elements.errorMessage.classList.add('show'); this.elements.errorMessage.classList.add('show');
this.elements.errorText.textContent = message; this.elements.errorText.textContent = message;
this.elements.refreshBtn.disabled = false; this.elements.refreshBtn.disabled = false;
this.isLoading = false; this.isLoading = false;
} }
// 获取一言数据 // 获取一言数据
async fetchHitokoto() { async fetchHitokoto() {
if (this.isLoading) return; if (this.isLoading) return;
this.showLoading(); this.showLoading();
// 尝试所有API接口 // 尝试所有API接口
for (let i = 0; i < this.apiEndpoints.length; i++) { for (let i = 0; i < this.apiEndpoints.length; i++) {
const endpointIndex = (this.currentEndpointIndex + i) % this.apiEndpoints.length; const endpointIndex = (this.currentEndpointIndex + i) % this.apiEndpoints.length;
const endpoint = this.apiEndpoints[endpointIndex]; const endpoint = this.apiEndpoints[endpointIndex];
try { try {
const result = await this.tryFetchFromEndpoint(endpoint); const result = await this.tryFetchFromEndpoint(endpoint);
if (result.success) { if (result.success) {
this.currentEndpointIndex = endpointIndex; this.currentEndpointIndex = endpointIndex;
this.displayHitokoto(result.data); this.displayHitokoto(result.data);
return; return;
} }
} catch (error) { } catch (error) {
console.warn(`接口 ${endpoint} 请求失败:`, error.message); console.warn(`接口 ${endpoint} 请求失败:`, error.message);
continue; continue;
} }
} }
// 所有接口都失败 // 所有接口都失败
this.showError('所有接口都无法访问,请检查网络连接或稍后重试'); this.showError('所有接口都无法访问,请检查网络连接或稍后重试');
} }
// 尝试从指定接口获取数据 // 尝试从指定接口获取数据
async tryFetchFromEndpoint(endpoint) { async tryFetchFromEndpoint(endpoint) {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时 const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
try { try {
// 移除URL中的encoding=text参数确保返回JSON格式 // 移除URL中的encoding=text参数确保返回JSON格式
const response = await fetch(`${endpoint}/v2/hitokoto`, { const response = await fetch(`${endpoint}/v2/hitokoto`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
signal: controller.signal signal: controller.signal
}); });
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`); throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} }
const data = await response.json(); const data = await response.json();
// 验证返回数据格式 // 验证返回数据格式
if (data.code === 200 && data.data && data.data.hitokoto) { if (data.code === 200 && data.data && data.data.hitokoto) {
return { return {
success: true, success: true,
data: data.data data: data.data
}; };
} else { } else {
throw new Error('返回数据格式不正确'); throw new Error('返回数据格式不正确');
} }
} catch (error) { } catch (error) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (error.name === 'AbortError') { if (error.name === 'AbortError') {
throw new Error('请求超时'); throw new Error('请求超时');
} }
throw error; throw error;
} }
} }
// 显示一言内容 // 显示一言内容
displayHitokoto(data) { displayHitokoto(data) {
// 更新一言文本 // 更新一言文本
this.elements.quoteText.textContent = data.hitokoto; this.elements.quoteText.textContent = data.hitokoto;
// 更新序号信息 // 更新序号信息
if (data.index) { if (data.index) {
this.elements.quoteIndex.textContent = `${data.index}`; this.elements.quoteIndex.textContent = `${data.index}`;
} else { } else {
this.elements.quoteIndex.textContent = ''; this.elements.quoteIndex.textContent = '';
} }
// 添加淡入动画效果 // 添加淡入动画效果
this.elements.quoteText.style.opacity = '0'; this.elements.quoteText.style.opacity = '0';
this.elements.quoteIndex.style.opacity = '0'; this.elements.quoteIndex.style.opacity = '0';
setTimeout(() => { setTimeout(() => {
this.elements.quoteText.style.transition = 'opacity 0.5s ease'; this.elements.quoteText.style.transition = 'opacity 0.5s ease';
this.elements.quoteIndex.style.transition = 'opacity 0.5s ease'; this.elements.quoteIndex.style.transition = 'opacity 0.5s ease';
this.elements.quoteText.style.opacity = '1'; this.elements.quoteText.style.opacity = '1';
this.elements.quoteIndex.style.opacity = '1'; this.elements.quoteIndex.style.opacity = '1';
}, 100); }, 100);
this.showQuoteDisplay(); this.showQuoteDisplay();
// 控制台输出调试信息 // 控制台输出调试信息
console.log('一言获取成功:', { console.log('一言获取成功:', {
content: data.hitokoto, content: data.hitokoto,
index: data.index, index: data.index,
endpoint: this.apiEndpoints[this.currentEndpointIndex] endpoint: this.apiEndpoints[this.currentEndpointIndex]
}); });
} }
// 获取随机接口(用于负载均衡) // 获取随机接口(用于负载均衡)
getRandomEndpoint() { getRandomEndpoint() {
const randomIndex = Math.floor(Math.random() * this.apiEndpoints.length); const randomIndex = Math.floor(Math.random() * this.apiEndpoints.length);
return this.apiEndpoints[randomIndex]; return this.apiEndpoints[randomIndex];
} }
} }
// 页面加载完成后初始化应用 // 页面加载完成后初始化应用
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const app = new HitokotoApp(); const app = new HitokotoApp();
// 添加全局错误处理 // 添加全局错误处理
window.addEventListener('error', (event) => { window.addEventListener('error', (event) => {
console.error('页面发生错误:', event.error); console.error('页面发生错误:', event.error);
}); });
window.addEventListener('unhandledrejection', (event) => { window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的Promise拒绝:', event.reason); console.error('未处理的Promise拒绝:', event.reason);
}); });
// 页面可见性变化时的处理 // 页面可见性变化时的处理
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (!document.hidden && !app.isLoading) { if (!document.hidden && !app.isLoading) {
// 页面重新可见时,可以选择刷新内容 // 页面重新可见时,可以选择刷新内容
console.log('页面重新可见'); console.log('页面重新可见');
} }
}); });
console.log('随机一言应用初始化完成'); console.log('随机一言应用初始化完成');
}); });
// 导出应用类(如果需要在其他地方使用) // 导出应用类(如果需要在其他地方使用)
if (typeof module !== 'undefined' && module.exports) { if (typeof module !== 'undefined' && module.exports) {
module.exports = HitokotoApp; module.exports = HitokotoApp;
} }

View File

@@ -1,3 +1,3 @@
[ [
"https://60s.api.shumengya.top" "https://60s.api.shumengya.top"
] ]

View File

@@ -1,8 +1,8 @@
{ {
"code": 200, "code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841", "message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": { "data": {
"index": 2862, "index": 2862,
"hitokoto": "你带上罪恶之冠,即使背负上所有罪恶和孤独,绝不让你受伤" "hitokoto": "你带上罪恶之冠,即使背负上所有罪恶和孤独,绝不让你受伤"
} }
} }

View File

@@ -1,16 +1,16 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>每日笑话</title> <title>每日笑话</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
</head> </head>
<body> <body>
<div class="card"> <div class="card">
<p id="joke">加载中...</p> <p id="joke">加载中...</p>
<button id="next">换一个</button> <button id="next">换一个</button>
</div> </div>
<script src="script.js"></script> <script src="script.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,107 +1,107 @@
/* background.css - 动态渐变背景 */ /* background.css - 动态渐变背景 */
body { body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%; background-size: 400% 400%;
animation: gradient 15s ease infinite; animation: gradient 15s ease infinite;
} }
@keyframes gradient { @keyframes gradient {
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
} }
:root { :root {
--bg-yellow: #FFFDE7; /* 浅黄 */ --bg-yellow: #FFFDE7; /* 浅黄 */
--bg-blue: #E3F2FD; /* 淡蓝 */ --bg-blue: #E3F2FD; /* 淡蓝 */
} }
body { body {
background: linear-gradient(180deg, var(--bg-yellow) 0%, var(--bg-blue) 100%); background: linear-gradient(180deg, var(--bg-yellow) 0%, var(--bg-blue) 100%);
background-attachment: fixed; /* 固定背景,滚动时不移动 */ background-attachment: fixed; /* 固定背景,滚动时不移动 */
} }
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
overflow: hidden; overflow: hidden;
transition: background-color 0.5s ease; transition: background-color 0.5s ease;
} }
/* Light Theme (Default) */ /* Light Theme (Default) */
[data-theme="light"] { [data-theme="light"] {
background: linear-gradient(to bottom, #87CEEB, #B0E0E6); background: linear-gradient(to bottom, #87CEEB, #B0E0E6);
} }
/* Dark Theme */ /* Dark Theme */
[data-theme="dark"] { [data-theme="dark"] {
background: linear-gradient(to bottom, #232526, #414345); background: linear-gradient(to bottom, #232526, #414345);
} }
[data-theme="dark"] .snowflake { [data-theme="dark"] .snowflake {
color: #999; color: #999;
} }
/* Winter Theme */ /* Winter Theme */
[data-theme="winter"] { [data-theme="winter"] {
background: linear-gradient(to bottom, #a1c4fd, #c2e9fb); background: linear-gradient(to bottom, #a1c4fd, #c2e9fb);
} }
[data-theme="winter"] .background-bottom { [data-theme="winter"] .background-bottom {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100px; height: 100px;
background: linear-gradient(to top, white, rgba(255, 255, 255, 0)); background: linear-gradient(to top, white, rgba(255, 255, 255, 0));
z-index: -1; z-index: -1;
border-radius: 50% 50% 0 0 / 20px; border-radius: 50% 50% 0 0 / 20px;
box-shadow: 0 -10px 20px rgba(255, 255, 255, 0.5); box-shadow: 0 -10px 20px rgba(255, 255, 255, 0.5);
} }
#snowflake-container { #snowflake-container {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
z-index: 0; z-index: 0;
} }
.snowflake { .snowflake {
position: absolute; position: absolute;
top: -10%; top: -10%;
color: white; color: white;
font-size: 20px; font-size: 20px;
user-select: none; user-select: none;
animation: fall linear infinite; animation: fall linear infinite;
} }
@keyframes fall { @keyframes fall {
to { to {
transform: translateY(105vh) rotate(360deg); transform: translateY(105vh) rotate(360deg);
} }
} }
#frost-overlay { #frost-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: url('https://www.transparenttextures.com/patterns/ice-age.png') repeat; background: url('https://www.transparenttextures.com/patterns/ice-age.png') repeat;
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
transition: opacity 0.5s ease-in-out; transition: opacity 0.5s ease-in-out;
z-index: 100; z-index: 100;
} }
#frost-overlay.is-frosted { #frost-overlay.is-frosted {
opacity: 0.3; opacity: 0.3;
} }

View File

@@ -1,217 +1,217 @@
:root { :root {
--primary-color-light: #4A90E2; --primary-color-light: #4A90E2;
--text-color-light: #333; --text-color-light: #333;
--card-bg-light: rgba(255, 255, 255, 0.85); --card-bg-light: rgba(255, 255, 255, 0.85);
--primary-color-dark: #5271C4; --primary-color-dark: #5271C4;
--text-color-dark: #E0E0E0; --text-color-dark: #E0E0E0;
--card-bg-dark: rgba(40, 40, 40, 0.85); --card-bg-dark: rgba(40, 40, 40, 0.85);
--primary-color-winter: #6A82FB; --primary-color-winter: #6A82FB;
--text-color-winter: #2c3e50; --text-color-winter: #2c3e50;
--card-bg-winter: rgba(255, 255, 255, 0.7); --card-bg-winter: rgba(255, 255, 255, 0.7);
} }
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 100vh; min-height: 100vh;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
text-align: center; text-align: center;
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.top-nav { .top-nav {
position: absolute; position: absolute;
top: 20px; top: 20px;
right: 20px; right: 20px;
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
padding: 5px; padding: 5px;
border-radius: 50px; border-radius: 50px;
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
} }
.theme-switcher { .theme-switcher {
display: flex; display: flex;
gap: 5px; gap: 5px;
} }
.theme-btn { .theme-btn {
background: transparent; background: transparent;
border: 2px solid transparent; border: 2px solid transparent;
border-radius: 50%; border-radius: 50%;
width: 40px; width: 40px;
height: 40px; height: 40px;
font-size: 1.5em; font-size: 1.5em;
cursor: pointer; cursor: pointer;
transition: transform 0.2s, border-color 0.2s; transition: transform 0.2s, border-color 0.2s;
} }
.theme-btn:hover { .theme-btn:hover {
transform: scale(1.1); transform: scale(1.1);
} }
.theme-btn.active { .theme-btn.active {
border-color: white; border-color: white;
} }
.title { .title {
font-family: 'ZCOOL KuaiLe', cursive; font-family: 'ZCOOL KuaiLe', cursive;
font-size: 3em; font-size: 3em;
margin-bottom: 20px; margin-bottom: 20px;
transition: color 0.5s ease; transition: color 0.5s ease;
} }
.joke-stream { .joke-stream {
width: 100%; width: 100%;
max-width: 500px; max-width: 500px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 25px; gap: 25px;
} }
.joke-card { .joke-card {
border-radius: 20px; border-radius: 20px;
padding: 30px 40px; padding: 30px 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
width: 100%; width: 100%;
max-width: 500px; max-width: 500px;
min-height: 150px; min-height: 150px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: relative; position: relative;
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
transition: background-color 0.5s ease, border-color 0.5s ease; transition: background-color 0.5s ease, border-color 0.5s ease;
} }
#joke-text { #joke-text {
font-size: 1.5em; font-size: 1.5em;
line-height: 1.6; line-height: 1.6;
transition: opacity 0.3s, color 0.5s ease; transition: opacity 0.3s, color 0.5s ease;
} }
/* --- Theming --- */ /* --- Theming --- */
/* Light Theme */ /* Light Theme */
[data-theme="light"] .title { color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); } [data-theme="light"] .title { color: white; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); }
[data-theme="light"] .joke-card { background-color: var(--card-bg-light); } [data-theme="light"] .joke-card { background-color: var(--card-bg-light); }
[data-theme="light"] #joke-text { color: var(--text-color-light); } [data-theme="light"] #joke-text { color: var(--text-color-light); }
[data-theme="light"] #new-joke-btn { background-color: var(--primary-color-light); box-shadow: 0 4px 15px rgba(74, 144, 226, 0.4); } [data-theme="light"] #new-joke-btn { background-color: var(--primary-color-light); box-shadow: 0 4px 15px rgba(74, 144, 226, 0.4); }
[data-theme="light"] footer { color: rgba(255, 255, 255, 0.8); } [data-theme="light"] footer { color: rgba(255, 255, 255, 0.8); }
/* Dark Theme */ /* Dark Theme */
[data-theme="dark"] .title { color: #EAEAEA; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); } [data-theme="dark"] .title { color: #EAEAEA; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); }
[data-theme="dark"] .joke-card { background-color: var(--card-bg-dark); border-color: rgba(255, 255, 255, 0.1); } [data-theme="dark"] .joke-card { background-color: var(--card-bg-dark); border-color: rgba(255, 255, 255, 0.1); }
[data-theme="dark"] #joke-text { color: var(--text-color-dark); } [data-theme="dark"] #joke-text { color: var(--text-color-dark); }
[data-theme="dark"] #new-joke-btn { background-color: var(--primary-color-dark); box-shadow: 0 4px 15px rgba(82, 113, 196, 0.4); } [data-theme="dark"] #new-joke-btn { background-color: var(--primary-color-dark); box-shadow: 0 4px 15px rgba(82, 113, 196, 0.4); }
[data-theme="dark"] footer { color: rgba(200, 200, 200, 0.7); } [data-theme="dark"] footer { color: rgba(200, 200, 200, 0.7); }
/* Winter Theme */ /* Winter Theme */
[data-theme="winter"] .title { color: #1e3a5f; text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.7); } [data-theme="winter"] .title { color: #1e3a5f; text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.7); }
[data-theme="winter"] .joke-card { [data-theme="winter"] .joke-card {
background-color: var(--card-bg-winter); background-color: var(--card-bg-winter);
border-color: rgba(255, 255, 255, 0.8); border-color: rgba(255, 255, 255, 0.8);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1), inset 0 0 15px rgba(255, 255, 255, 0.5); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1), inset 0 0 15px rgba(255, 255, 255, 0.5);
} }
[data-theme="winter"] #joke-text { color: var(--text-color-winter); } [data-theme="winter"] #joke-text { color: var(--text-color-winter); }
[data-theme="winter"] #new-joke-btn { background-color: var(--primary-color-winter); box-shadow: 0 4px 15px rgba(106, 130, 251, 0.4); } [data-theme="winter"] #new-joke-btn { background-color: var(--primary-color-winter); box-shadow: 0 4px 15px rgba(106, 130, 251, 0.4); }
[data-theme="winter"] footer { color: #1e3a5f; } [data-theme="winter"] footer { color: #1e3a5f; }
.controls { .controls {
margin-top: 30px; margin-top: 30px;
} }
#new-joke-btn { #new-joke-btn {
color: white; color: white;
font-size: 1.2em; font-size: 1.2em;
font-weight: bold; font-weight: bold;
padding: 15px 35px; padding: 15px 35px;
border: none; border: none;
border-radius: 50px; border-radius: 50px;
cursor: pointer; cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.5s ease; transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.5s ease;
} }
#new-joke-btn:hover { #new-joke-btn:hover {
transform: translateY(-3px); transform: translateY(-3px);
} }
#new-joke-btn:active { #new-joke-btn:active {
transform: translateY(1px); transform: translateY(1px);
} }
.interactions { .interactions {
margin-top: 25px; margin-top: 25px;
display: flex; display: flex;
gap: 20px; gap: 20px;
} }
.interaction-btn { .interaction-btn {
background: rgba(255, 255, 255, 0.7); background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.9); border: 1px solid rgba(255, 255, 255, 0.9);
border-radius: 50%; border-radius: 50%;
width: 50px; width: 50px;
height: 50px; height: 50px;
font-size: 1.5em; font-size: 1.5em;
cursor: pointer; cursor: pointer;
transition: transform 0.2s, background-color 0.2s; transition: transform 0.2s, background-color 0.2s;
} }
.interaction-btn:hover { .interaction-btn:hover {
transform: scale(1.1); transform: scale(1.1);
background: white; background: white;
} }
footer { footer {
position: absolute; position: absolute;
bottom: 10px; bottom: 10px;
font-size: 0.9em; font-size: 0.9em;
transition: color 0.5s ease; transition: color 0.5s ease;
} }
/* Loader */ /* Loader */
#loader { #loader {
position: absolute; position: absolute;
transition: color 0.5s ease; transition: color 0.5s ease;
} }
[data-theme="light"] #loader { color: var(--primary-color-light); } [data-theme="light"] #loader { color: var(--primary-color-light); }
[data-theme="dark"] #loader { color: var(--primary-color-dark); } [data-theme="dark"] #loader { color: var(--primary-color-dark); }
[data-theme="winter"] #loader { color: var(--primary-color-winter); } [data-theme="winter"] #loader { color: var(--primary-color-winter); }
.snowflake-loader { .snowflake-loader {
font-size: 40px; font-size: 40px;
display: inline-block; display: inline-block;
animation: spin 1.5s linear infinite; animation: spin 1.5s linear infinite;
} }
.snowflake-loader::before { .snowflake-loader::before {
content: '❄'; content: '❄';
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
.hidden { .hidden {
display: none; display: none;
} }
/* Responsive */ /* Responsive */
@media (max-width: 600px) { @media (max-width: 600px) {
.title { .title {
font-size: 2.5em; font-size: 2.5em;
} }
.joke-card { .joke-card {
padding: 25px; padding: 25px;
} }
#joke-text { #joke-text {
font-size: 1.2em; font-size: 1.2em;
} }
.top-nav { .top-nav {
top: 10px; top: 10px;
right: 10px; right: 10px;
} }
} }

View File

@@ -1,58 +1,58 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>随机冷笑话</title> <title>随机冷笑话</title>
<link href="https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/background.css"> <link rel="stylesheet" href="css/background.css">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
</head> </head>
<body data-theme="light"> <body data-theme="light">
<div id="snowflake-container"></div> <div id="snowflake-container"></div>
<div id="frost-overlay"></div> <div id="frost-overlay"></div>
<div class="background-bottom"></div> <div class="background-bottom"></div>
<nav class="top-nav"> <nav class="top-nav">
<div class="theme-switcher"> <div class="theme-switcher">
<button class="theme-btn" data-theme-target="light" title="清新风">☀️</button> <button class="theme-btn" data-theme-target="light" title="清新风">☀️</button>
<button class="theme-btn" data-theme-target="dark" title="暗黑风">🌙</button> <button class="theme-btn" data-theme-target="dark" title="暗黑风">🌙</button>
<button class="theme-btn" data-theme-target="winter" title="冰雪风">❄️</button> <button class="theme-btn" data-theme-target="winter" title="冰雪风">❄️</button>
</div> </div>
</nav> </nav>
<div class="container"> <div class="container">
<header> <header>
<h1 class="title">冷笑话生成器</h1> <h1 class="title">冷笑话生成器</h1>
</header> </header>
<main class="joke-card"> <main class="joke-card">
<div id="loader" class="hidden"> <div id="loader" class="hidden">
<div class="snowflake-loader"></div> <div class="snowflake-loader"></div>
<p>思考中...</p> <p>思考中...</p>
</div> </div>
<p id="joke-text">点击下面的按钮,来点冷笑话吧!</p> <p id="joke-text">点击下面的按钮,来点冷笑话吧!</p>
</main> </main>
<div class="controls"> <div class="controls">
<button id="new-joke-btn">再来一个</button> <button id="new-joke-btn">再来一个</button>
</div> </div>
<div class="interactions"> <div class="interactions">
<button class="interaction-btn" id="like-btn" title="好笑">👍</button> <button class="interaction-btn" id="like-btn" title="好笑">👍</button>
<button class="interaction-btn" id="dislike-btn" title="不好笑">👎</button> <button class="interaction-btn" id="dislike-btn" title="不好笑">👎</button>
<button class="interaction-btn" id="collect-btn" title="收藏">⭐️</button> <button class="interaction-btn" id="collect-btn" title="收藏">⭐️</button>
<button class="interaction-btn" id="share-btn" title="分享">🔗</button> <button class="interaction-btn" id="share-btn" title="分享">🔗</button>
</div> </div>
</div> </div>
<footer> <footer>
<p>&copy; 2024 冷笑话工坊</p> <p>&copy; 2024 冷笑话工坊</p>
</footer> </footer>
<audio id="wind-sound" src="https://www.soundjay.com/nature/sounds/wind-howl-01.mp3" preload="auto"></audio> <audio id="wind-sound" src="https://www.soundjay.com/nature/sounds/wind-howl-01.mp3" preload="auto"></audio>
<audio id="snow-sound" src="https://www.soundjay.com/nature/sounds/walking-in-snow-01.mp3" preload="auto"></audio> <audio id="snow-sound" src="https://www.soundjay.com/nature/sounds/walking-in-snow-01.mp3" preload="auto"></audio>
<script src="js/script.js"></script> <script src="js/script.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,117 +1,117 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const jokeTextElem = document.getElementById('joke-text'); const jokeTextElem = document.getElementById('joke-text');
const newJokeBtn = document.getElementById('new-joke-btn'); const newJokeBtn = document.getElementById('new-joke-btn');
const snowflakeContainer = document.getElementById('snowflake-container'); const snowflakeContainer = document.getElementById('snowflake-container');
const frostOverlay = document.getElementById('frost-overlay'); const frostOverlay = document.getElementById('frost-overlay');
const windSound = document.getElementById('wind-sound'); const windSound = document.getElementById('wind-sound');
const snowSound = document.getElementById('snow-sound'); const snowSound = document.getElementById('snow-sound');
const loader = document.getElementById('loader'); const loader = document.getElementById('loader');
const themeBtns = document.querySelectorAll('.theme-btn'); const themeBtns = document.querySelectorAll('.theme-btn');
const apiEndpoints = [ const apiEndpoints = [
'https://60s.api.shumengya.top/v2/dad-joke', 'https://60s.api.shumengya.top/v2/dad-joke',
]; ];
let currentApiIndex = 0; let currentApiIndex = 0;
async function fetchJoke() { async function fetchJoke() {
jokeTextElem.classList.add('hidden'); jokeTextElem.classList.add('hidden');
loader.classList.remove('hidden'); loader.classList.remove('hidden');
try { try {
const response = await fetch(apiEndpoints[currentApiIndex]); const response = await fetch(apiEndpoints[currentApiIndex]);
if (!response.ok) throw new Error('Network response was not ok'); if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json(); const data = await response.json();
if (data.code === 200 && data.data.content) { if (data.code === 200 && data.data.content) {
updateJokeText(data.data.content); updateJokeText(data.data.content);
if (document.body.dataset.theme === 'winter' && Math.random() < 0.3) { if (document.body.dataset.theme === 'winter' && Math.random() < 0.3) {
triggerFrostEffect(); triggerFrostEffect();
} }
} else { } else {
throw new Error('API returned invalid data'); throw new Error('API returned invalid data');
} }
} catch (error) { } catch (error) {
console.error('Fetch error:', error); console.error('Fetch error:', error);
currentApiIndex = (currentApiIndex + 1) % apiEndpoints.length; currentApiIndex = (currentApiIndex + 1) % apiEndpoints.length;
if (currentApiIndex !== 0) { if (currentApiIndex !== 0) {
fetchJoke(); fetchJoke();
} else { } else {
jokeTextElem.textContent = '冰箱坏了,暂时没有冷笑话...'; jokeTextElem.textContent = '冰箱坏了,暂时没有冷笑话...';
} }
} finally { } finally {
loader.classList.add('hidden'); loader.classList.add('hidden');
jokeTextElem.classList.remove('hidden'); jokeTextElem.classList.remove('hidden');
} }
} }
function updateJokeText(text) { function updateJokeText(text) {
jokeTextElem.textContent = ''; jokeTextElem.textContent = '';
let i = 0; let i = 0;
const typing = setInterval(() => { const typing = setInterval(() => {
if (i < text.length) { if (i < text.length) {
jokeTextElem.textContent += text.charAt(i); jokeTextElem.textContent += text.charAt(i);
i++; i++;
} else { } else {
clearInterval(typing); clearInterval(typing);
} }
}, 50); }, 50);
} }
function createSnowflakes() { function createSnowflakes() {
const snowflakeCount = document.body.dataset.theme === 'dark' ? 50 : 30; const snowflakeCount = document.body.dataset.theme === 'dark' ? 50 : 30;
snowflakeContainer.innerHTML = ''; snowflakeContainer.innerHTML = '';
for (let i = 0; i < snowflakeCount; i++) { for (let i = 0; i < snowflakeCount; i++) {
const snowflake = document.createElement('div'); const snowflake = document.createElement('div');
snowflake.className = 'snowflake'; snowflake.className = 'snowflake';
snowflake.textContent = '❄️'; snowflake.textContent = '❄️';
snowflake.style.left = `${Math.random() * 100}vw`; snowflake.style.left = `${Math.random() * 100}vw`;
snowflake.style.fontSize = `${Math.random() * 15 + 10}px`; snowflake.style.fontSize = `${Math.random() * 15 + 10}px`;
snowflake.style.opacity = Math.random() * 0.5 + 0.3; snowflake.style.opacity = Math.random() * 0.5 + 0.3;
const duration = Math.random() * 10 + 8; const duration = Math.random() * 10 + 8;
const delay = Math.random() * 10; const delay = Math.random() * 10;
snowflake.style.animation = `fall ${duration}s linear ${delay}s infinite`; snowflake.style.animation = `fall ${duration}s linear ${delay}s infinite`;
snowflakeContainer.appendChild(snowflake); snowflakeContainer.appendChild(snowflake);
} }
} }
function triggerFrostEffect() { function triggerFrostEffect() {
frostOverlay.classList.add('is-frosted'); frostOverlay.classList.add('is-frosted');
windSound.play().catch(e => console.error("Audio play failed:", e)); windSound.play().catch(e => console.error("Audio play failed:", e));
setTimeout(() => { setTimeout(() => {
frostOverlay.classList.remove('is-frosted'); frostOverlay.classList.remove('is-frosted');
}, 2000); }, 2000);
} }
function setTheme(theme) { function setTheme(theme) {
document.body.dataset.theme = theme; document.body.dataset.theme = theme;
localStorage.setItem('joke-theme', theme); localStorage.setItem('joke-theme', theme);
themeBtns.forEach(btn => { themeBtns.forEach(btn => {
btn.classList.toggle('active', btn.dataset.themeTarget === theme); btn.classList.toggle('active', btn.dataset.themeTarget === theme);
}); });
if (theme === 'winter') { if (theme === 'winter') {
snowSound.play().catch(e => console.error("Audio play failed:", e)); snowSound.play().catch(e => console.error("Audio play failed:", e));
} }
// Recreate snowflakes for theme-specific density // Recreate snowflakes for theme-specific density
createSnowflakes(); createSnowflakes();
} }
themeBtns.forEach(btn => { themeBtns.forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
setTheme(btn.dataset.themeTarget); setTheme(btn.dataset.themeTarget);
}); });
}); });
newJokeBtn.addEventListener('click', fetchJoke); newJokeBtn.addEventListener('click', fetchJoke);
// Initial setup // Initial setup
const savedTheme = localStorage.getItem('joke-theme') || 'light'; const savedTheme = localStorage.getItem('joke-theme') || 'light';
setTheme(savedTheme); setTheme(savedTheme);
fetchJoke(); fetchJoke();
}); });

View File

@@ -1,8 +1,8 @@
{ {
"code": 200, "code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841", "message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": { "data": {
"index": 121, "index": 121,
"content": "这个世界上谁最懂猪?蜘蛛(知猪)人。" "content": "这个世界上谁最懂猪?蜘蛛(知猪)人。"
} }
} }

View File

@@ -1,90 +1,90 @@
body { body {
background-color: #1a1a1a; background-color: #1a1a1a;
color: #e0e0e0; color: #e0e0e0;
font-family: 'Courier New', Courier, monospace; font-family: 'Courier New', Courier, monospace;
overflow: hidden; overflow: hidden;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
#bg-container { #bg-container {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: -2; z-index: -2;
overflow: hidden; overflow: hidden;
transition: transform 0.2s ease-out; transition: transform 0.2s ease-out;
} }
.floating-emoji { .floating-emoji {
position: absolute; position: absolute;
user-select: none; user-select: none;
opacity: 0; opacity: 0;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: linear; animation-timing-function: linear;
} }
@keyframes float-top-to-bottom { @keyframes float-top-to-bottom {
0% { transform: translateY(-10vh) rotate(0deg); opacity: 0; } 0% { transform: translateY(-10vh) rotate(0deg); opacity: 0; }
10%, 90% { opacity: 0.7; } 10%, 90% { opacity: 0.7; }
100% { transform: translateY(110vh) rotate(360deg); opacity: 0; } 100% { transform: translateY(110vh) rotate(360deg); opacity: 0; }
} }
@keyframes float-bottom-to-top { @keyframes float-bottom-to-top {
0% { transform: translateY(110vh) rotate(0deg); opacity: 0; } 0% { transform: translateY(110vh) rotate(0deg); opacity: 0; }
10%, 90% { opacity: 0.7; } 10%, 90% { opacity: 0.7; }
100% { transform: translateY(-10vh) rotate(360deg); opacity: 0; } 100% { transform: translateY(-10vh) rotate(360deg); opacity: 0; }
} }
@keyframes float-left-to-right { @keyframes float-left-to-right {
0% { transform: translateX(-10vw) rotate(0deg); opacity: 0; } 0% { transform: translateX(-10vw) rotate(0deg); opacity: 0; }
10%, 90% { opacity: 0.7; } 10%, 90% { opacity: 0.7; }
100% { transform: translateX(110vw) rotate(360deg); opacity: 0; } 100% { transform: translateX(110vw) rotate(360deg); opacity: 0; }
} }
@keyframes float-right-to-left { @keyframes float-right-to-left {
0% { transform: translateX(110vw) rotate(0deg); opacity: 0; } 0% { transform: translateX(110vw) rotate(0deg); opacity: 0; }
10%, 90% { opacity: 0.7; } 10%, 90% { opacity: 0.7; }
100% { transform: translateX(-10vw) rotate(360deg); opacity: 0; } 100% { transform: translateX(-10vw) rotate(360deg); opacity: 0; }
} }
.text-fragment { .text-fragment {
position: absolute; position: absolute;
font-size: 24px; font-size: 24px;
color: rgba(255, 0, 255, 0.4); color: rgba(255, 0, 255, 0.4);
opacity: 0; opacity: 0;
animation: float-fragment 15s linear infinite, fade-in-out 15s linear infinite; animation: float-fragment 15s linear infinite, fade-in-out 15s linear infinite;
user-select: none; user-select: none;
} }
@keyframes float-fragment { @keyframes float-fragment {
0% { transform: translate(0, 0) rotate(0deg); } 0% { transform: translate(0, 0) rotate(0deg); }
25% { transform: translate(20px, 40px) rotate(15deg); } 25% { transform: translate(20px, 40px) rotate(15deg); }
50% { transform: translate(-30px, -10px) rotate(-10deg); } 50% { transform: translate(-30px, -10px) rotate(-10deg); }
75% { transform: translate(10px, -30px) rotate(5deg); } 75% { transform: translate(10px, -30px) rotate(5deg); }
100% { transform: translate(0, 0) rotate(0deg); } 100% { transform: translate(0, 0) rotate(0deg); }
} }
@keyframes fade-in-out { @keyframes fade-in-out {
0%, 100% { opacity: 0; } 0%, 100% { opacity: 0; }
10%, 90% { opacity: 0.4; } 10%, 90% { opacity: 0.4; }
} }
.screen-crack { .screen-crack {
position: absolute; position: absolute;
width: 200px; width: 200px;
height: 200px; height: 200px;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M 100 100 L 0 0 M 100 100 L 200 0 M 100 100 L 50 200 M 100 100 L 150 200" stroke="rgba(255,255,255,0.5)" stroke-width="1" fill="none"/></svg>'); background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M 100 100 L 0 0 M 100 100 L 200 0 M 100 100 L 50 200 M 100 100 L 150 200" stroke="rgba(255,255,255,0.5)" stroke-width="1" fill="none"/></svg>');
opacity: 0; opacity: 0;
animation: flicker-crack 25s steps(1, end) infinite; animation: flicker-crack 25s steps(1, end) infinite;
} }
@keyframes flicker-crack { @keyframes flicker-crack {
0%, 100% { opacity: 0; } 0%, 100% { opacity: 0; }
50% { opacity: 0.3; } 50% { opacity: 0.3; }
51% { opacity: 0; } 51% { opacity: 0; }
75% { opacity: 0.2; } 75% { opacity: 0.2; }
76% { opacity: 0; } 76% { opacity: 0; }
} }

View File

@@ -1,235 +1,235 @@
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 100vh; min-height: 100vh;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.content-card { .content-card {
background: rgba(20, 20, 20, 0.7); background: rgba(20, 20, 20, 0.7);
border: none; border: none;
padding: 40px; padding: 40px;
max-width: 600px; max-width: 600px;
width: 100%; width: 100%;
text-align: center; text-align: center;
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
position: relative; position: relative;
clip-path: polygon(2% 5%, 97% 0%, 100% 95%, 0% 100%); clip-path: polygon(2% 5%, 97% 0%, 100% 95%, 0% 100%);
} }
.body-animated .content-card { .body-animated .content-card {
animation: tremble 0.4s infinite, glitch-shadow 1.5s steps(1, end) infinite; animation: tremble 0.4s infinite, glitch-shadow 1.5s steps(1, end) infinite;
} }
@keyframes tremble { @keyframes tremble {
0% { clip-path: polygon(2% 5%, 97% 0%, 100% 95%, 0% 100%); } 0% { clip-path: polygon(2% 5%, 97% 0%, 100% 95%, 0% 100%); }
25% { clip-path: polygon(2% 5%, 98% 2%, 99% 100%, 1% 98%); } 25% { clip-path: polygon(2% 5%, 98% 2%, 99% 100%, 1% 98%); }
50% { clip-path: polygon(3% 4%, 96% 1%, 100% 96%, 2% 100%); } 50% { clip-path: polygon(3% 4%, 96% 1%, 100% 96%, 2% 100%); }
75% { clip-path: polygon(1% 6%, 97% 3%, 98% 95%, 0% 99%); } 75% { clip-path: polygon(1% 6%, 97% 3%, 98% 95%, 0% 99%); }
100% { clip-path: polygon(2% 5%, 97% 0%, 100% 95%, 0% 100%); } 100% { clip-path: polygon(2% 5%, 97% 0%, 100% 95%, 0% 100%); }
} }
@keyframes glitch-shadow { @keyframes glitch-shadow {
0% { 0% {
box-shadow: box-shadow:
0 0 8px rgba(255, 0, 255, 0.5), 0 0 8px rgba(255, 0, 255, 0.5),
inset 0 0 8px rgba(255, 0, 255, 0.4); inset 0 0 8px rgba(255, 0, 255, 0.4);
} }
33% { 33% {
box-shadow: box-shadow:
0 0 8px rgba(0, 255, 255, 0.5), 0 0 8px rgba(0, 255, 255, 0.5),
inset 0 0 8px rgba(0, 255, 255, 0.4); inset 0 0 8px rgba(0, 255, 255, 0.4);
} }
66% { 66% {
box-shadow: box-shadow:
0 0 8px rgba(0, 255, 0, 0.5), 0 0 8px rgba(0, 255, 0, 0.5),
inset 0 0 8px rgba(0, 255, 0, 0.4); inset 0 0 8px rgba(0, 255, 0, 0.4);
} }
100% { 100% {
box-shadow: box-shadow:
0 0 8px rgba(255, 0, 255, 0.5), 0 0 8px rgba(255, 0, 255, 0.5),
inset 0 0 8px rgba(255, 0, 255, 0.4); inset 0 0 8px rgba(255, 0, 255, 0.4);
} }
} }
#literature-text { #literature-text {
font-size: 1.2em; font-size: 1.2em;
line-height: 1.8; line-height: 1.8;
min-height: 100px; min-height: 100px;
color: #e0e0e0; color: #e0e0e0;
text-shadow: 0 0 5px rgba(0, 255, 135, 0.5); text-shadow: 0 0 5px rgba(0, 255, 135, 0.5);
animation: text-flicker 15s linear infinite; animation: text-flicker 15s linear infinite;
} }
.body-animated #literature-text { .body-animated #literature-text {
animation: text-flicker 15s linear infinite, text-shadow-glitch 2s steps(1, end) infinite; animation: text-flicker 15s linear infinite, text-shadow-glitch 2s steps(1, end) infinite;
} }
@keyframes text-flicker { @keyframes text-flicker {
0%, 100% { opacity: 1; } 0%, 100% { opacity: 1; }
50.0% { opacity: 0.95; } 50.0% { opacity: 0.95; }
50.5% { opacity: 1; } 50.5% { opacity: 1; }
} }
@keyframes text-shadow-glitch { @keyframes text-shadow-glitch {
0% { 0% {
text-shadow: text-shadow:
1px 0 0 rgba(255,0,255,0.5), 1px 0 0 rgba(255,0,255,0.5),
-1px 0 0 rgba(0,255,255,0.5); -1px 0 0 rgba(0,255,255,0.5);
} }
10% { 10% {
text-shadow: text-shadow:
-1px 0 0 rgba(255,0,255,0.5), -1px 0 0 rgba(255,0,255,0.5),
1px 0 0 rgba(0,255,255,0.5); 1px 0 0 rgba(0,255,255,0.5);
} }
11%, 100% { 11%, 100% {
text-shadow: none; text-shadow: none;
} }
} }
.controls { .controls {
margin-top: 30px; margin-top: 30px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20px; gap: 20px;
} }
#new-literature-btn { #new-literature-btn {
background-color: #ff00ff; background-color: #ff00ff;
color: #fff; color: #fff;
border: none; border: none;
padding: 12px 25px; padding: 12px 25px;
font-size: 1em; font-size: 1em;
cursor: pointer; cursor: pointer;
border-radius: 5px; border-radius: 5px;
text-transform: uppercase; text-transform: uppercase;
font-weight: bold; font-weight: bold;
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff; box-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff;
} }
#new-literature-btn:hover { #new-literature-btn:hover {
transform: scale(1.05); transform: scale(1.05);
box-shadow: 0 0 15px #ff00ff, 0 0 30px #ff00ff; box-shadow: 0 0 15px #ff00ff, 0 0 30px #ff00ff;
} }
#new-literature-btn:active { #new-literature-btn:active {
transform: scale(0.95); transform: scale(0.95);
} }
/* Animation Toggle Switch */ /* Animation Toggle Switch */
.switch-container { .switch-container {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
color: #aaa; color: #aaa;
} }
.switch { .switch {
position: relative; position: relative;
display: inline-block; display: inline-block;
width: 50px; width: 50px;
height: 24px; height: 24px;
} }
.switch input { .switch input {
opacity: 0; opacity: 0;
width: 0; width: 0;
height: 0; height: 0;
} }
.slider { .slider {
position: absolute; position: absolute;
cursor: pointer; cursor: pointer;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background-color: #ccc; background-color: #ccc;
transition: .4s; transition: .4s;
} }
.slider:before { .slider:before {
position: absolute; position: absolute;
content: ""; content: "";
height: 16px; height: 16px;
width: 16px; width: 16px;
left: 4px; left: 4px;
bottom: 4px; bottom: 4px;
background-color: white; background-color: white;
transition: .4s; transition: .4s;
} }
input:checked + .slider { input:checked + .slider {
background-color: #ff00ff; background-color: #ff00ff;
} }
input:checked + .slider:before { input:checked + .slider:before {
transform: translateX(26px); transform: translateX(26px);
} }
.slider.round { .slider.round {
border-radius: 34px; border-radius: 34px;
} }
.slider.round:before { .slider.round:before {
border-radius: 50%; border-radius: 50%;
} }
/* Glitch Overlay & Animations */ /* Glitch Overlay & Animations */
#glitch-overlay { #glitch-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: -1; z-index: -1;
pointer-events: none; pointer-events: none;
} }
.body-animated #glitch-overlay { .body-animated #glitch-overlay {
animation: color-shift 15s steps(1, end) infinite; animation: color-shift 15s steps(1, end) infinite;
} }
@keyframes color-shift { @keyframes color-shift {
0%, 100% { background: transparent; } 0%, 100% { background: transparent; }
10% { background: rgba(255, 0, 0, 0.05); } 10% { background: rgba(255, 0, 0, 0.05); }
10.1% { background: transparent; } 10.1% { background: transparent; }
20% { background: rgba(0, 255, 0, 0.05); } 20% { background: rgba(0, 255, 0, 0.05); }
20.1% { background: transparent; } 20.1% { background: transparent; }
30% { background: rgba(0, 0, 255, 0.05); } 30% { background: rgba(0, 0, 255, 0.05); }
30.1% { background: transparent; } 30.1% { background: transparent; }
} }
.flicker-block { .flicker-block {
position: absolute; position: absolute;
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
opacity: 0; opacity: 0;
} }
.body-animated .flicker-block { .body-animated .flicker-block {
animation: flicker 3s infinite; animation: flicker 3s infinite;
} }
@keyframes flicker { @keyframes flicker {
0%, 100% { opacity: 0; } 0%, 100% { opacity: 0; }
50% { opacity: 1; } 50% { opacity: 1; }
} }
/* Responsive Design */ /* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.content-card { .content-card {
padding: 20px; padding: 20px;
} }
#literature-text { #literature-text {
font-size: 1em; font-size: 1em;
} }
.controls { .controls {
flex-direction: column; flex-direction: column;
} }
} }

View File

@@ -1,34 +1,34 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>随机发病文学</title> <title>随机发病文学</title>
<link rel="stylesheet" href="css/background.css"> <link rel="stylesheet" href="css/background.css">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
</head> </head>
<body> <body>
<div id="bg-container"> <div id="bg-container">
</div> </div>
<div id="glitch-overlay"></div> <div id="glitch-overlay"></div>
<div class="container"> <div class="container">
<div class="content-card"> <div class="content-card">
<p id="literature-text">正在加载发病文学...</p> <p id="literature-text">正在加载发病文学...</p>
</div> </div>
<div class="controls"> <div class="controls">
<button id="new-literature-btn">再疯一次</button> <button id="new-literature-btn">再疯一次</button>
<div class="switch-container"> <div class="switch-container">
<label for="animation-toggle">关闭动画</label> <label for="animation-toggle">关闭动画</label>
<label class="switch"> <label class="switch">
<input type="checkbox" id="animation-toggle" checked> <input type="checkbox" id="animation-toggle" checked>
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<script src="js/script.js"></script> <script src="js/script.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,147 +1,147 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const literatureTextElem = document.getElementById('literature-text'); const literatureTextElem = document.getElementById('literature-text');
const newLiteratureBtn = document.getElementById('new-literature-btn'); const newLiteratureBtn = document.getElementById('new-literature-btn');
const animationToggle = document.getElementById('animation-toggle'); const animationToggle = document.getElementById('animation-toggle');
const bgContainer = document.getElementById('bg-container'); const bgContainer = document.getElementById('bg-container');
const body = document.body; const body = document.body;
const apiEndpoints = [ const apiEndpoints = [
'https://60s.api.shumengya.top/v2/fabing', 'https://60s.api.shumengya.top/v2/fabing',
// Add fallback APIs here if available // Add fallback APIs here if available
]; ];
let currentApiIndex = 0; let currentApiIndex = 0;
async function fetchLiterature() { async function fetchLiterature() {
literatureTextElem.textContent = '正在卖力发疯中...'; literatureTextElem.textContent = '正在卖力发疯中...';
literatureTextElem.style.opacity = '0.5'; literatureTextElem.style.opacity = '0.5';
try { try {
const response = await fetch(apiEndpoints[currentApiIndex]); const response = await fetch(apiEndpoints[currentApiIndex]);
if (!response.ok) { if (!response.ok) {
throw new Error('Network response was not ok'); throw new Error('Network response was not ok');
} }
const data = await response.json(); const data = await response.json();
if (data.code === 200) { if (data.code === 200) {
literatureTextElem.textContent = data.data.saying; literatureTextElem.textContent = data.data.saying;
} else { } else {
throw new Error('API returned an error'); throw new Error('API returned an error');
} }
} catch (error) { } catch (error) {
console.error('Fetch error:', error); console.error('Fetch error:', error);
currentApiIndex = (currentApiIndex + 1) % apiEndpoints.length; currentApiIndex = (currentApiIndex + 1) % apiEndpoints.length;
if (currentApiIndex !== 0) { if (currentApiIndex !== 0) {
fetchLiterature(); // Retry with the next API fetchLiterature(); // Retry with the next API
} else { } else {
literatureTextElem.textContent = '疯不起来了,请稍后再试。'; literatureTextElem.textContent = '疯不起来了,请稍后再试。';
} }
} finally { } finally {
literatureTextElem.style.opacity = '1'; literatureTextElem.style.opacity = '1';
} }
} }
function createFloatingEmojis() { function createFloatingEmojis() {
const existingEmojis = bgContainer.querySelectorAll('.floating-emoji'); const existingEmojis = bgContainer.querySelectorAll('.floating-emoji');
existingEmojis.forEach(e => e.remove()); existingEmojis.forEach(e => e.remove());
const emojis = ['🤯', '😵', '🤪', '🥴', '🤡', '👹', '👻', '💀', '💥', '🔥', '🌪️', '😵‍💫']; const emojis = ['🤯', '😵', '🤪', '🥴', '🤡', '👹', '👻', '💀', '💥', '🔥', '🌪️', '😵‍💫'];
const animationNames = ['float-top-to-bottom', 'float-bottom-to-top', 'float-left-to-right', 'float-right-to-left']; const animationNames = ['float-top-to-bottom', 'float-bottom-to-top', 'float-left-to-right', 'float-right-to-left'];
const emojiCount = 25; const emojiCount = 25;
for (let i = 0; i < emojiCount; i++) { for (let i = 0; i < emojiCount; i++) {
const emojiEl = document.createElement('div'); const emojiEl = document.createElement('div');
emojiEl.className = 'floating-emoji'; emojiEl.className = 'floating-emoji';
emojiEl.textContent = emojis[Math.floor(Math.random() * emojis.length)]; emojiEl.textContent = emojis[Math.floor(Math.random() * emojis.length)];
const animationName = animationNames[Math.floor(Math.random() * animationNames.length)]; const animationName = animationNames[Math.floor(Math.random() * animationNames.length)];
emojiEl.style.animationName = animationName; emojiEl.style.animationName = animationName;
emojiEl.style.animationDuration = `${Math.random() * 10 + 15}s`; // 15-25 seconds emojiEl.style.animationDuration = `${Math.random() * 10 + 15}s`; // 15-25 seconds
emojiEl.style.animationDelay = `${Math.random() * 20}s`; emojiEl.style.animationDelay = `${Math.random() * 20}s`;
emojiEl.style.fontSize = `${Math.random() * 20 + 20}px`; emojiEl.style.fontSize = `${Math.random() * 20 + 20}px`;
// Set initial position based on animation direction // Set initial position based on animation direction
if (animationName.includes('top') || animationName.includes('bottom')) { // Vertical movement if (animationName.includes('top') || animationName.includes('bottom')) { // Vertical movement
emojiEl.style.left = `${Math.random() * 100}vw`; emojiEl.style.left = `${Math.random() * 100}vw`;
} else { // Horizontal movement } else { // Horizontal movement
emojiEl.style.top = `${Math.random() * 100}vh`; emojiEl.style.top = `${Math.random() * 100}vh`;
} }
bgContainer.appendChild(emojiEl); bgContainer.appendChild(emojiEl);
} }
} }
function createFragments() { function createFragments() {
const existingFragments = bgContainer.querySelectorAll('.text-fragment'); const existingFragments = bgContainer.querySelectorAll('.text-fragment');
existingFragments.forEach(f => f.remove()); existingFragments.forEach(f => f.remove());
const fragments = ['我', '疯', '了', '', '', '…', '救命', '为什么', '好烦', '啊啊啊']; const fragments = ['我', '疯', '了', '', '', '…', '救命', '为什么', '好烦', '啊啊啊'];
for (let i = 0; i < 20; i++) { for (let i = 0; i < 20; i++) {
const frag = document.createElement('div'); const frag = document.createElement('div');
frag.className = 'text-fragment'; frag.className = 'text-fragment';
frag.textContent = fragments[Math.floor(Math.random() * fragments.length)]; frag.textContent = fragments[Math.floor(Math.random() * fragments.length)];
frag.style.top = `${Math.random() * 100}%`; frag.style.top = `${Math.random() * 100}%`;
frag.style.left = `${Math.random() * 100}%`; frag.style.left = `${Math.random() * 100}%`;
frag.style.animationDelay = `${Math.random() * 15}s`; frag.style.animationDelay = `${Math.random() * 15}s`;
frag.style.fontSize = `${Math.random() * 12 + 12}px`; frag.style.fontSize = `${Math.random() * 12 + 12}px`;
bgContainer.appendChild(frag); bgContainer.appendChild(frag);
} }
} }
function createCracks() { function createCracks() {
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
const crack = document.createElement('div'); const crack = document.createElement('div');
crack.className = 'screen-crack'; crack.className = 'screen-crack';
crack.style.top = `${Math.random() * 80}%`; crack.style.top = `${Math.random() * 80}%`;
crack.style.left = `${Math.random() * 80}%`; crack.style.left = `${Math.random() * 80}%`;
crack.style.transform = `rotate(${Math.random() * 360}deg)`; crack.style.transform = `rotate(${Math.random() * 360}deg)`;
crack.style.animationDelay = `${Math.random() * 25}s`; crack.style.animationDelay = `${Math.random() * 25}s`;
bgContainer.appendChild(crack); bgContainer.appendChild(crack);
} }
} }
function createFlickerBlocks() { function createFlickerBlocks() {
const glitchOverlay = document.getElementById('glitch-overlay'); const glitchOverlay = document.getElementById('glitch-overlay');
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const block = document.createElement('div'); const block = document.createElement('div');
block.className = 'flicker-block'; block.className = 'flicker-block';
block.style.width = `${Math.random() * 100 + 50}px`; block.style.width = `${Math.random() * 100 + 50}px`;
block.style.height = `${Math.random() * 100 + 50}px`; block.style.height = `${Math.random() * 100 + 50}px`;
block.style.top = `${Math.random() * 90}%`; block.style.top = `${Math.random() * 90}%`;
block.style.left = `${Math.random() * 90}%`; block.style.left = `${Math.random() * 90}%`;
block.style.animationDuration = `${Math.random() * 2 + 2}s`; block.style.animationDuration = `${Math.random() * 2 + 2}s`;
block.style.animationDelay = `${Math.random() * 3}s`; block.style.animationDelay = `${Math.random() * 3}s`;
glitchOverlay.appendChild(block); glitchOverlay.appendChild(block);
} }
} }
function toggleAnimations() { function toggleAnimations() {
if (animationToggle.checked) { if (animationToggle.checked) {
body.classList.add('body-animated'); body.classList.add('body-animated');
} else { } else {
body.classList.remove('body-animated'); body.classList.remove('body-animated');
} }
} }
document.addEventListener('mousemove', (e) => { document.addEventListener('mousemove', (e) => {
if (!animationToggle.checked) return; if (!animationToggle.checked) return;
const x = (window.innerWidth / 2) - e.pageX; const x = (window.innerWidth / 2) - e.pageX;
const y = (window.innerHeight / 2) - e.pageY; const y = (window.innerHeight / 2) - e.pageY;
bgContainer.style.transform = `translateX(${x / 50}px) translateY(${y / 50}px)`; bgContainer.style.transform = `translateX(${x / 50}px) translateY(${y / 50}px)`;
}); });
newLiteratureBtn.addEventListener('click', fetchLiterature); newLiteratureBtn.addEventListener('click', fetchLiterature);
animationToggle.addEventListener('change', toggleAnimations); animationToggle.addEventListener('change', toggleAnimations);
// Initial setup // Initial setup
createFloatingEmojis(); createFloatingEmojis();
createFragments(); createFragments();
createCracks(); createCracks();
createFlickerBlocks(); createFlickerBlocks();
toggleAnimations(); toggleAnimations();
fetchLiterature(); fetchLiterature();
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
createFloatingEmojis(); createFloatingEmojis();
createFragments(); createFragments();
}); });
}); });

View File

@@ -1,8 +1,8 @@
{ {
"code": 200, "code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841", "message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": { "data": {
"index": 347, "index": 347,
"duanzi": "我不想读书,主要是因为家里牛啊,猪啊羊啊都没人喂。" "duanzi": "我不想读书,主要是因为家里牛啊,猪啊羊啊都没人喂。"
} }
} }

View File

@@ -1,251 +1,251 @@
/* 随机唱歌音频 - 淡绿色清新风格样式 */ /* 随机唱歌音频 - 淡绿色清新风格样式 */
/* 重置样式 */ /* 重置样式 */
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 50%, #ffd3a5 100%); background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 50%, #ffd3a5 100%);
min-height: 100vh; min-height: 100vh;
color: #2d5016; color: #2d5016;
line-height: 1.6; line-height: 1.6;
overflow-x: hidden; overflow-x: hidden;
} }
.container { .container {
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
} }
/* 头部 */ /* 头部 */
.header { .header {
text-align: center; text-align: center;
margin-bottom: 20px; margin-bottom: 20px;
background: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.85);
border-radius: 20px; border-radius: 20px;
padding: 24px; padding: 24px;
box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08); box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
.header h1 { .header h1 {
font-size: 2rem; font-size: 2rem;
color: #2d5016; color: #2d5016;
margin-bottom: 10px; margin-bottom: 10px;
font-weight: 700; font-weight: 700;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 12px; gap: 12px;
} }
.header p { .header p {
color: #5a7c65; color: #5a7c65;
font-size: 1rem; font-size: 1rem;
} }
/* 用户卡片 */ /* 用户卡片 */
.user-card { .user-card {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 15px; gap: 15px;
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
padding: 16px; padding: 16px;
border-radius: 15px; border-radius: 15px;
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08); box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
margin-bottom: 15px; margin-bottom: 15px;
text-align: center; text-align: center;
} }
.avatar { .avatar {
width: 56px; width: 56px;
height: 56px; height: 56px;
border-radius: 50%; border-radius: 50%;
object-fit: cover; object-fit: cover;
border: 3px solid rgba(129, 199, 132, 0.5); border: 3px solid rgba(129, 199, 132, 0.5);
} }
.user-info { .user-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.nickname { .nickname {
font-weight: 700; font-weight: 700;
font-size: 1.1rem; font-size: 1.1rem;
color: #2d5016; color: #2d5016;
} }
.meta { .meta {
color: #5a7c65; color: #5a7c65;
font-size: 0.9rem; font-size: 0.9rem;
} }
/* 歌曲信息 */ /* 歌曲信息 */
.song-card { .song-card {
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
padding: 16px; padding: 16px;
border-radius: 15px; border-radius: 15px;
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08); box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
margin-bottom: 15px; margin-bottom: 15px;
text-align: center; text-align: center;
} }
.song-title { .song-title {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 700; font-weight: 700;
margin-bottom: 8px; margin-bottom: 8px;
color: #1b5e20; color: #1b5e20;
} }
.song-meta { .song-meta {
color: #5a7c65; color: #5a7c65;
font-size: 0.95rem; font-size: 0.95rem;
margin-bottom: 10px; margin-bottom: 10px;
} }
/* 歌词 */ /* 歌词 */
.lyrics { .lyrics {
background: rgba(129, 199, 132, 0.1); background: rgba(129, 199, 132, 0.1);
border-radius: 12px; border-radius: 12px;
padding: 12px; padding: 12px;
max-height: 220px; max-height: 220px;
overflow: auto; overflow: auto;
} }
.lyrics p { .lyrics p {
margin-bottom: 6px; margin-bottom: 6px;
} }
/* 音频播放器卡片 */ /* 音频播放器卡片 */
.audio-card { .audio-card {
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
padding: 16px; padding: 16px;
border-radius: 15px; border-radius: 15px;
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08); box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
margin-bottom: 15px; margin-bottom: 15px;
} }
.audio-actions { .audio-actions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
align-items: center; align-items: center;
margin-top: 10px; margin-top: 10px;
} }
.btn { .btn {
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%); background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
color: white; color: white;
border: none; border: none;
padding: 10px 18px; padding: 10px 18px;
border-radius: 10px; border-radius: 10px;
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.25s ease; transition: all 0.25s ease;
box-shadow: 0 4px 12px rgba(129, 199, 132, 0.35); box-shadow: 0 4px 12px rgba(129, 199, 132, 0.35);
text-decoration: none; text-decoration: none;
} }
.btn:hover { .btn:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 6px 18px rgba(129, 199, 132, 0.45); box-shadow: 0 6px 18px rgba(129, 199, 132, 0.45);
} }
.info { .info {
color: #5a7c65; color: #5a7c65;
font-size: 0.9rem; font-size: 0.9rem;
} }
/* 加载与错误 */ /* 加载与错误 */
.loading, .error { .loading, .error {
text-align: center; text-align: center;
padding: 30px; padding: 30px;
background: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.85);
border-radius: 15px; border-radius: 15px;
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08); box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
} }
.spinner { .spinner {
width: 36px; width: 36px;
height: 36px; height: 36px;
border: 4px solid #e8f5e8; border: 4px solid #e8f5e8;
border-top: 4px solid #81c784; border-top: 4px solid #81c784;
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin: 0 auto 18px; margin: 0 auto 18px;
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
/* 动画 */ /* 动画 */
.fade-in { .fade-in {
animation: fadeIn 0.5s ease-in-out; animation: fadeIn 0.5s ease-in-out;
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); } from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
/* 平板端适配 */ /* 平板端适配 */
@media (max-width: 1024px) and (min-width: 768px) { @media (max-width: 1024px) and (min-width: 768px) {
.container { padding: 16px; } .container { padding: 16px; }
.header h1 { font-size: 1.8rem; } .header h1 { font-size: 1.8rem; }
} }
/* 手机端优先优化 */ /* 手机端优先优化 */
@media (max-width: 767px) { @media (max-width: 767px) {
.container { padding: 12px; } .container { padding: 12px; }
.header { padding: 18px; } .header { padding: 18px; }
.header h1 { font-size: 1.6rem; gap: 8px; } .header h1 { font-size: 1.6rem; gap: 8px; }
.user-card { .user-card {
padding: 16px; padding: 16px;
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
} }
.avatar { .avatar {
width: 80px; width: 80px;
height: 80px; height: 80px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.song-card, .audio-card { .song-card, .audio-card {
padding: 16px; padding: 16px;
} }
.lyrics { .lyrics {
max-height: 180px; max-height: 180px;
text-align: left; text-align: left;
padding: 16px; padding: 16px;
} }
.audio-actions { .audio-actions {
flex-direction: column; flex-direction: column;
gap: 15px; gap: 15px;
align-items: center; align-items: center;
} }
.info { .info {
text-align: center; text-align: center;
line-height: 1.8; line-height: 1.8;
} }
.btn { .btn {
width: 100%; width: 100%;
max-width: 200px; max-width: 200px;
text-align: center; text-align: center;
} }
} }

View File

@@ -1,67 +1,67 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>随机唱歌音频 - 60s API 集合</title> <title>随机唱歌音频 - 60s API 集合</title>
<meta name="description" content="随机唱歌音频,数据源自 60s.viki.moe提供用户信息、歌曲信息、歌词与音频播放。" /> <meta name="description" content="随机唱歌音频,数据源自 60s.viki.moe提供用户信息、歌曲信息、歌词与音频播放。" />
<link rel="stylesheet" href="./css/style.css" /> <link rel="stylesheet" href="./css/style.css" />
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<header class="header"> <header class="header">
<h1> <h1>
🎵 随机唱歌音频 🎵 随机唱歌音频
</h1> </h1>
<p>数据来自官方/权威源头,以确保稳定与实时 · 支持本地数据回退</p> <p>数据来自官方/权威源头,以确保稳定与实时 · 支持本地数据回退</p>
</header> </header>
<!-- 加载与错误状态 --> <!-- 加载与错误状态 -->
<section id="loading" class="loading"> <section id="loading" class="loading">
<div class="spinner"></div> <div class="spinner"></div>
<p>正在加载中,请稍候…</p> <p>正在加载中,请稍候…</p>
</section> </section>
<section id="error" class="error" style="display: none;"> <section id="error" class="error" style="display: none;">
<p>获取数据失败,请稍后重试</p> <p>获取数据失败,请稍后重试</p>
</section> </section>
<!-- 内容区域 --> <!-- 内容区域 -->
<main id="content" style="display: none;" class="fade-in"> <main id="content" style="display: none;" class="fade-in">
<!-- 用户信息 --> <!-- 用户信息 -->
<div class="user-card"> <div class="user-card">
<img id="avatar" class="avatar" src="" alt="用户头像" /> <img id="avatar" class="avatar" src="" alt="用户头像" />
<div class="user-info"> <div class="user-info">
<div class="nickname" id="nickname">-</div> <div class="nickname" id="nickname">-</div>
<div class="meta">性别:<span id="gender">-</span></div> <div class="meta">性别:<span id="gender">-</span></div>
</div> </div>
</div> </div>
<!-- 歌曲信息 --> <!-- 歌曲信息 -->
<div class="song-card"> <div class="song-card">
<div class="song-title" id="song-title">-</div> <div class="song-title" id="song-title">-</div>
<div class="song-meta" id="song-meta">-</div> <div class="song-meta" id="song-meta">-</div>
<div class="lyrics" id="lyrics"></div> <div class="lyrics" id="lyrics"></div>
</div> </div>
<!-- 音频播放 --> <!-- 音频播放 -->
<div class="audio-card"> <div class="audio-card">
<audio id="audio" controls style="width: 100%;"></audio> <audio id="audio" controls style="width: 100%;"></audio>
<div class="audio-actions"> <div class="audio-actions">
<button class="btn" id="refresh-btn">换一首</button> <button class="btn" id="refresh-btn">换一首</button>
<div class="info"> <div class="info">
❤ 喜欢:<span id="like-count">-</span> ❤ 喜欢:<span id="like-count">-</span>
· ⏱ 时长:<span id="duration">--:--</span> · ⏱ 时长:<span id="duration">--:--</span>
· 🗓 发布:<span id="publish-time">-</span> · 🗓 发布:<span id="publish-time">-</span>
· 🔗 <a id="link" href="#" class="btn" style="padding: 6px 10px; border-radius: 8px; background: #81c784;">查看原帖</a> · 🔗 <a id="link" href="#" class="btn" style="padding: 6px 10px; border-radius: 8px; background: #81c784;">查看原帖</a>
</div> </div>
</div> </div>
</div> </div>
</main> </main>
</div> </div>
<script src="./js/script.js"></script> <script src="./js/script.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,252 +1,252 @@
// 随机唱歌音频 页面脚本 // 随机唱歌音频 页面脚本
(function () { (function () {
'use strict'; 'use strict';
const API = { const API = {
endpoints: [], endpoints: [],
currentIndex: 0, currentIndex: 0,
params: { params: {
encoding: 'json' encoding: 'json'
}, },
localFallback: '返回接口.json', localFallback: '返回接口.json',
// 初始化API接口列表 // 初始化API接口列表
async init() { async init() {
try { try {
const res = await fetch('./接口集合.json'); const res = await fetch('./接口集合.json');
const endpoints = await res.json(); const endpoints = await res.json();
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/changya`); this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/changya`);
} catch (e) { } catch (e) {
// 如果无法加载接口集合,使用默认接口 // 如果无法加载接口集合,使用默认接口
this.endpoints = ['https://60s.api.shumengya.top/v2/changya']; this.endpoints = ['https://60s.api.shumengya.top/v2/changya'];
} }
}, },
// 获取当前接口URL // 获取当前接口URL
getCurrentUrl() { getCurrentUrl() {
if (this.endpoints.length === 0) return null; if (this.endpoints.length === 0) return null;
const url = new URL(this.endpoints[this.currentIndex]); const url = new URL(this.endpoints[this.currentIndex]);
Object.entries(this.params).forEach(([k, v]) => url.searchParams.append(k, v)); Object.entries(this.params).forEach(([k, v]) => url.searchParams.append(k, v));
return url.toString(); return url.toString();
}, },
// 切换到下一个接口 // 切换到下一个接口
switchToNext() { switchToNext() {
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length; this.currentIndex = (this.currentIndex + 1) % this.endpoints.length;
return this.currentIndex < this.endpoints.length; return this.currentIndex < this.endpoints.length;
}, },
// 重置到第一个接口 // 重置到第一个接口
reset() { reset() {
this.currentIndex = 0; this.currentIndex = 0;
} }
}; };
// DOM 元素引用 // DOM 元素引用
const els = { const els = {
loading: null, loading: null,
error: null, error: null,
container: null, container: null,
avatar: null, avatar: null,
nickname: null, nickname: null,
gender: null, gender: null,
songTitle: null, songTitle: null,
songMeta: null, songMeta: null,
lyrics: null, lyrics: null,
audio: null, audio: null,
likeCount: null, likeCount: null,
publishTime: null, publishTime: null,
link: null, link: null,
refreshBtn: null, refreshBtn: null,
}; };
function initDom() { function initDom() {
els.loading = document.getElementById('loading'); els.loading = document.getElementById('loading');
els.error = document.getElementById('error'); els.error = document.getElementById('error');
els.container = document.getElementById('content'); els.container = document.getElementById('content');
els.avatar = document.getElementById('avatar'); els.avatar = document.getElementById('avatar');
els.nickname = document.getElementById('nickname'); els.nickname = document.getElementById('nickname');
els.gender = document.getElementById('gender'); els.gender = document.getElementById('gender');
els.songTitle = document.getElementById('song-title'); els.songTitle = document.getElementById('song-title');
els.songMeta = document.getElementById('song-meta'); els.songMeta = document.getElementById('song-meta');
els.lyrics = document.getElementById('lyrics'); els.lyrics = document.getElementById('lyrics');
els.audio = document.getElementById('audio'); els.audio = document.getElementById('audio');
els.likeCount = document.getElementById('like-count'); els.likeCount = document.getElementById('like-count');
els.publishTime = document.getElementById('publish-time'); els.publishTime = document.getElementById('publish-time');
els.link = document.getElementById('link'); els.link = document.getElementById('link');
els.refreshBtn = document.getElementById('refresh-btn'); els.refreshBtn = document.getElementById('refresh-btn');
} }
function showLoading() { function showLoading() {
els.loading.style.display = 'block'; els.loading.style.display = 'block';
els.error.style.display = 'none'; els.error.style.display = 'none';
els.container.style.display = 'none'; els.container.style.display = 'none';
} }
function showError(msg) { function showError(msg) {
els.loading.style.display = 'none'; els.loading.style.display = 'none';
els.error.style.display = 'block'; els.error.style.display = 'block';
els.container.style.display = 'none'; els.container.style.display = 'none';
els.error.querySelector('p').textContent = msg || '获取数据失败,请稍后重试'; els.error.querySelector('p').textContent = msg || '获取数据失败,请稍后重试';
} }
function showContent() { function showContent() {
els.loading.style.display = 'none'; els.loading.style.display = 'none';
els.error.style.display = 'none'; els.error.style.display = 'none';
els.container.style.display = 'block'; els.container.style.display = 'block';
} }
function formatDuration(ms) { function formatDuration(ms) {
if (!ms && ms !== 0) return ''; if (!ms && ms !== 0) return '';
const totalSeconds = Math.floor(ms / 1000); const totalSeconds = Math.floor(ms / 1000);
const m = Math.floor(totalSeconds / 60).toString().padStart(2, '0'); const m = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
const s = (totalSeconds % 60).toString().padStart(2, '0'); const s = (totalSeconds % 60).toString().padStart(2, '0');
return `${m}:${s}`; return `${m}:${s}`;
} }
function safeText(text) { function safeText(text) {
const div = document.createElement('div'); const div = document.createElement('div');
div.textContent = text == null ? '' : String(text); div.textContent = text == null ? '' : String(text);
return div.innerHTML; return div.innerHTML;
} }
async function fetchFromAPI() { async function fetchFromAPI() {
// 初始化API接口列表 // 初始化API接口列表
await API.init(); await API.init();
// 重置API索引到第一个接口 // 重置API索引到第一个接口
API.reset(); API.reset();
// 尝试所有API接口 // 尝试所有API接口
for (let i = 0; i < API.endpoints.length; i++) { for (let i = 0; i < API.endpoints.length; i++) {
try { try {
const url = API.getCurrentUrl(); const url = API.getCurrentUrl();
console.log(`尝试接口 ${i + 1}/${API.endpoints.length}: ${url}`); console.log(`尝试接口 ${i + 1}/${API.endpoints.length}: ${url}`);
const resp = await fetch(url, { const resp = await fetch(url, {
cache: 'no-store', cache: 'no-store',
timeout: 10000 // 10秒超时 timeout: 10000 // 10秒超时
}); });
if (!resp.ok) { if (!resp.ok) {
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`); throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
} }
const data = await resp.json(); const data = await resp.json();
if (data && data.code === 200) { if (data && data.code === 200) {
console.log(`接口 ${i + 1} 请求成功`); console.log(`接口 ${i + 1} 请求成功`);
return data; return data;
} }
throw new Error(data && data.message ? data.message : '接口返回异常'); throw new Error(data && data.message ? data.message : '接口返回异常');
} catch (e) { } catch (e) {
console.warn(`接口 ${i + 1} 失败:`, e.message); console.warn(`接口 ${i + 1} 失败:`, e.message);
// 如果不是最后一个接口,切换到下一个 // 如果不是最后一个接口,切换到下一个
if (i < API.endpoints.length - 1) { if (i < API.endpoints.length - 1) {
API.switchToNext(); API.switchToNext();
continue; continue;
} }
// 所有接口都失败了 // 所有接口都失败了
console.warn('所有远程接口都失败,尝试本地数据'); console.warn('所有远程接口都失败,尝试本地数据');
return null; return null;
} }
} }
} }
async function fetchFromLocal() { async function fetchFromLocal() {
try { try {
const resp = await fetch(API.localFallback + `?t=${Date.now()}`); const resp = await fetch(API.localFallback + `?t=${Date.now()}`);
if (!resp.ok) throw new Error(`本地文件HTTP ${resp.status}`); if (!resp.ok) throw new Error(`本地文件HTTP ${resp.status}`);
const data = await resp.json(); const data = await resp.json();
return data; return data;
} catch (e) { } catch (e) {
console.error('读取本地返回接口.json失败:', e); console.error('读取本地返回接口.json失败:', e);
return null; return null;
} }
} }
function render(data) { function render(data) {
const d = data?.data || {}; const d = data?.data || {};
const user = d.user || {}; const user = d.user || {};
const song = d.song || {}; const song = d.song || {};
const audio = d.audio || {}; const audio = d.audio || {};
// 用户信息 // 用户信息
els.avatar.src = user.avatar_url || ''; els.avatar.src = user.avatar_url || '';
els.avatar.alt = (user.nickname || '用户') + ' 头像'; els.avatar.alt = (user.nickname || '用户') + ' 头像';
els.nickname.textContent = user.nickname || '未知用户'; els.nickname.textContent = user.nickname || '未知用户';
els.gender.textContent = user.gender === 'female' ? '女' : user.gender === 'male' ? '男' : '未知'; els.gender.textContent = user.gender === 'female' ? '女' : user.gender === 'male' ? '男' : '未知';
// 歌曲信息 // 歌曲信息
els.songTitle.textContent = song.name || '未知歌曲'; els.songTitle.textContent = song.name || '未知歌曲';
els.songMeta.textContent = song.singer ? `演唱:${song.singer}` : ''; els.songMeta.textContent = song.singer ? `演唱:${song.singer}` : '';
els.lyrics.innerHTML = ''; els.lyrics.innerHTML = '';
if (Array.isArray(song.lyrics)) { if (Array.isArray(song.lyrics)) {
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
song.lyrics.forEach(line => { song.lyrics.forEach(line => {
const p = document.createElement('p'); const p = document.createElement('p');
p.innerHTML = safeText(line); p.innerHTML = safeText(line);
frag.appendChild(p); frag.appendChild(p);
}); });
els.lyrics.appendChild(frag); els.lyrics.appendChild(frag);
} }
// 音频 // 音频
els.audio.src = audio.url || ''; els.audio.src = audio.url || '';
els.audio.preload = 'none'; els.audio.preload = 'none';
// 其他信息 // 其他信息
els.likeCount.textContent = typeof audio.like_count === 'number' ? audio.like_count : '-'; els.likeCount.textContent = typeof audio.like_count === 'number' ? audio.like_count : '-';
const publish = audio.publish || (audio.publish_at ? new Date(audio.publish_at).toLocaleString() : ''); const publish = audio.publish || (audio.publish_at ? new Date(audio.publish_at).toLocaleString() : '');
els.publishTime.textContent = publish; els.publishTime.textContent = publish;
els.link.href = audio.link || '#'; els.link.href = audio.link || '#';
els.link.target = '_blank'; els.link.target = '_blank';
// 时长信息 // 时长信息
const durationEl = document.getElementById('duration'); const durationEl = document.getElementById('duration');
durationEl.textContent = formatDuration(audio.duration); durationEl.textContent = formatDuration(audio.duration);
showContent(); showContent();
} }
async function load() { async function load() {
showLoading(); showLoading();
try { try {
// 先尝试远程API // 先尝试远程API
const data = await fetchFromAPI(); const data = await fetchFromAPI();
if (data) { if (data) {
render(data); render(data);
return; return;
} }
// 远程API失败尝试本地数据 // 远程API失败尝试本地数据
const localData = await fetchFromLocal(); const localData = await fetchFromLocal();
if (localData) { if (localData) {
render(localData); render(localData);
return; return;
} }
// 都失败了 // 都失败了
showError('获取数据失败,请稍后重试'); showError('获取数据失败,请稍后重试');
} catch (e) { } catch (e) {
console.error('加载数据时发生错误:', e); console.error('加载数据时发生错误:', e);
showError('获取数据失败,请稍后重试'); showError('获取数据失败,请稍后重试');
} }
} }
function bindEvents() { function bindEvents() {
if (els.refreshBtn) { if (els.refreshBtn) {
els.refreshBtn.addEventListener('click', load); els.refreshBtn.addEventListener('click', load);
} }
// 快捷键 Ctrl+R 刷新(不拦截浏览器默认刷新) // 快捷键 Ctrl+R 刷新(不拦截浏览器默认刷新)
} }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initDom(); initDom();
bindEvents(); bindEvents();
load(); load();
}); });
})(); })();

View File

@@ -1,3 +1,3 @@
[ [
"https://60s.api.shumengya.top" "https://60s.api.shumengya.top"
] ]

View File

@@ -1,32 +1,32 @@
{ {
"code": 200, "code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841", "message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": { "data": {
"user": { "user": {
"nickname": "𝑮𝑺_迷鹿_", "nickname": "𝑮𝑺_迷鹿_",
"gender": "female", "gender": "female",
"avatar_url": "http://img-cdn.api.singduck.cn/user-img/6afbebcfae6144478c150d0c1d0d5899.jpg" "avatar_url": "http://img-cdn.api.singduck.cn/user-img/6afbebcfae6144478c150d0c1d0d5899.jpg"
}, },
"song": { "song": {
"name": "恶作剧", "name": "恶作剧",
"singer": "王蓝茵", "singer": "王蓝茵",
"lyrics": [ "lyrics": [
"我想我会开始想念你", "我想我会开始想念你",
"可是我刚刚才遇见了你", "可是我刚刚才遇见了你",
"我怀疑这奇遇只是个恶作剧", "我怀疑这奇遇只是个恶作剧",
"我想我已慢慢喜欢你", "我想我已慢慢喜欢你",
"因为我拥有爱情的勇气", "因为我拥有爱情的勇气",
"我任性投入你给的恶作剧", "我任性投入你给的恶作剧",
"你给的恶作剧" "你给的恶作剧"
] ]
}, },
"audio": { "audio": {
"url": "http://audio-cdn.api.singduck.cn/ugc/220929_965696173_b822a290c553.wav?auth_key=1755845643-0-0-4029539b73e17337dcac49cc4e0ecfcc", "url": "http://audio-cdn.api.singduck.cn/ugc/220929_965696173_b822a290c553.wav?auth_key=1755845643-0-0-4029539b73e17337dcac49cc4e0ecfcc",
"duration": 35050, "duration": 35050,
"like_count": 955, "like_count": 955,
"link": "https://m.api.singduck.cn/user-piece/toGZlBfZbukck2sHb", "link": "https://m.api.singduck.cn/user-piece/toGZlBfZbukck2sHb",
"publish": "2022/09/29 18:33:51", "publish": "2022/09/29 18:33:51",
"publish_at": 1664447631000 "publish_at": 1664447631000
} }
} }
} }

View File

@@ -1,36 +1,36 @@
body { body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0; margin: 0;
min-height: 100vh; min-height: 100vh;
overflow-x: hidden; overflow-x: hidden;
transition: background 0.5s ease; transition: background 0.5s ease;
} }
/* Hand-drawn Comic Theme Background - FRESH GREEN VERSION */ /* Hand-drawn Comic Theme Background - FRESH GREEN VERSION */
body.theme-comic { body.theme-comic {
background: linear-gradient(-45deg, #c8e6c9, #dcedc8, #f1f8e9, #e8f5e8); background: linear-gradient(-45deg, #c8e6c9, #dcedc8, #f1f8e9, #e8f5e8);
background-size: 400% 400%; background-size: 400% 400%;
animation: gradientBG 15s ease infinite; animation: gradientBG 15s ease infinite;
} }
@keyframes gradientBG { @keyframes gradientBG {
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
} }
/* Placeholder for Emoji Theme Background */ /* Placeholder for Emoji Theme Background */
body.theme-emoji { body.theme-emoji {
background-color: #fffde7; background-color: #fffde7;
} }
/* Placeholder for Retro TV Theme Background */ /* Placeholder for Retro TV Theme Background */
body.theme-retro { body.theme-retro {
background-color: #3d2b1f; background-color: #3d2b1f;
} }

View File

@@ -1,200 +1,200 @@
@import url('https://fonts.googleapis.com/css2?family=Zhi+Mang+Xing&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Zhi+Mang+Xing&display=swap');
/* --- General & Theme Switcher --- */ /* --- General & Theme Switcher --- */
.container { .container {
max-width: 600px; max-width: 600px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
text-align: center; text-align: center;
} }
.theme-switcher { .theme-switcher {
position: fixed; position: fixed;
top: 15px; top: 15px;
right: 15px; right: 15px;
display: flex; display: flex;
gap: 5px; gap: 5px;
background: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.8);
padding: 5px; padding: 5px;
border-radius: 20px; border-radius: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 100; z-index: 100;
} }
.theme-icon { .theme-icon {
width: 30px; width: 30px;
height: 30px; height: 30px;
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
border: 2px solid transparent; border: 2px solid transparent;
} }
.theme-icon.active { .theme-icon.active {
border-color: #66bb6a; border-color: #66bb6a;
transform: scale(1.1); transform: scale(1.1);
} }
/* --- Comic Theme Styles --- */ /* --- Comic Theme Styles --- */
.theme-comic header h1 { .theme-comic header h1 {
font-family: 'Zhi Mang Xing', cursive; font-family: 'Zhi Mang Xing', cursive;
font-size: 4em; font-size: 4em;
color: #2e7d32; /* Fresh Green */ color: #2e7d32; /* Fresh Green */
text-shadow: 2px 2px 0 #fff; text-shadow: 2px 2px 0 #fff;
margin: 0.2em 0; margin: 0.2em 0;
} }
.theme-comic .divider { .theme-comic .divider {
height: 3px; height: 3px;
background: linear-gradient(90deg, #81c784, #a5d6a7, #c8e6c9, #66bb6a); background: linear-gradient(90deg, #81c784, #a5d6a7, #c8e6c9, #66bb6a);
border-radius: 3px; border-radius: 3px;
margin: 20px auto; margin: 20px auto;
width: 80%; width: 80%;
} }
.theme-comic .joke-card { .theme-comic .joke-card {
background: rgba(248, 255, 248, 0.9); /* Light green tinted white */ background: rgba(248, 255, 248, 0.9); /* Light green tinted white */
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
border-radius: 15px; border-radius: 15px;
padding: 40px; padding: 40px;
min-height: 200px; min-height: 200px;
box-shadow: 0 8px 25px rgba(102, 187, 106, 0.15); box-shadow: 0 8px 25px rgba(102, 187, 106, 0.15);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: relative; position: relative;
margin-bottom: 20px; margin-bottom: 20px;
transform: rotate(-1deg); transform: rotate(-1deg);
transition: transform 0.2s ease; transition: transform 0.2s ease;
border: 1px solid rgba(129, 199, 132, 0.3); border: 1px solid rgba(129, 199, 132, 0.3);
} }
.theme-comic .joke-card:hover { .theme-comic .joke-card:hover {
transform: rotate(1deg) scale(1.02); transform: rotate(1deg) scale(1.02);
} }
.theme-comic .joke-text { .theme-comic .joke-text {
font-family: 'Zhi Mang Xing', cursive; font-family: 'Zhi Mang Xing', cursive;
font-size: 2em; font-size: 2em;
line-height: 1.6; line-height: 1.6;
color: #1b5e20; color: #1b5e20;
} }
.theme-comic .new-joke-btn { .theme-comic .new-joke-btn {
background: linear-gradient(135deg, #66bb6a, #81c784); /* Fresh Green Gradient */ background: linear-gradient(135deg, #66bb6a, #81c784); /* Fresh Green Gradient */
color: white; color: white;
font-family: 'Zhi Mang Xing', cursive; font-family: 'Zhi Mang Xing', cursive;
font-size: 2.5em; font-size: 2.5em;
border: none; border: none;
border-radius: 50px; border-radius: 50px;
padding: 10px 30px; padding: 10px 30px;
cursor: pointer; cursor: pointer;
box-shadow: 0 5px 0 #388e3c; /* Darker Green */ box-shadow: 0 5px 0 #388e3c; /* Darker Green */
transition: all 0.1s ease-in-out; transition: all 0.1s ease-in-out;
} }
.theme-comic .new-joke-btn:active { .theme-comic .new-joke-btn:active {
transform: translateY(5px); transform: translateY(5px);
box-shadow: none; box-shadow: none;
} }
/* --- Loading Animation --- */ /* --- Loading Animation --- */
.loading-container { display: none; } .loading-container { display: none; }
.loading-container.visible { display: block; } .loading-container.visible { display: block; }
.loading-anim { .loading-anim {
height: 60px; height: 60px;
width: 80px; width: 80px;
margin: 0 auto 10px; margin: 0 auto 10px;
} }
.book { .book {
transform-style: preserve-3d; transform-style: preserve-3d;
transform: rotateY(-30deg); transform: rotateY(-30deg);
animation: flip 3s infinite; animation: flip 3s infinite;
} }
.book, .book-page { .book, .book-page {
width: 40px; width: 40px;
height: 55px; height: 55px;
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 50%; top: 50%;
margin-left: -20px; margin-left: -20px;
margin-top: -27.5px; margin-top: -27.5px;
} }
.book-page { .book-page {
background: #a5d6a7; background: #a5d6a7;
border: 1px solid #66bb6a; border: 1px solid #66bb6a;
border-radius: 3px; border-radius: 3px;
transform-origin: left; transform-origin: left;
} }
.book-page:nth-child(1) { animation: flip-page 3s infinite; } .book-page:nth-child(1) { animation: flip-page 3s infinite; }
.book-page:nth-child(2) { animation: flip-page 3s -1s infinite; } .book-page:nth-child(2) { animation: flip-page 3s -1s infinite; }
.book-page:nth-child(3) { animation: flip-page 3s -2s infinite; } .book-page:nth-child(3) { animation: flip-page 3s -2s infinite; }
@keyframes flip { 50% { transform: rotateY(30deg); } } @keyframes flip { 50% { transform: rotateY(30deg); } }
@keyframes flip-page { 30%, 100% { transform: rotateY(180deg); } } @keyframes flip-page { 30%, 100% { transform: rotateY(180deg); } }
/* --- Feedback Buttons & Animations --- */ /* --- Feedback Buttons & Animations --- */
.feedback-buttons { .feedback-buttons {
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 15px; gap: 15px;
margin-bottom: 30px; margin-bottom: 30px;
} }
.feedback-btn { .feedback-btn {
background: none; background: none;
border: none; border: none;
font-size: 2em; font-size: 2em;
cursor: pointer; cursor: pointer;
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
.feedback-btn:hover { transform: scale(1.2); } .feedback-btn:hover { transform: scale(1.2); }
#animation-container { #animation-container {
position: fixed; position: fixed;
top: 0; left: 0; width: 100%; height: 100%; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 999; pointer-events: none; z-index: 999;
} }
.confetti, .snowflake { .confetti, .snowflake {
position: absolute; position: absolute;
animation-timing-function: linear; animation-timing-function: linear;
animation-iteration-count: infinite; animation-iteration-count: infinite;
} }
.confetti { .confetti {
width: 10px; height: 10px; width: 10px; height: 10px;
animation-name: fall; animation-name: fall;
} }
.snowflake { .snowflake {
font-size: 20px; color: #fff; font-size: 20px; color: #fff;
animation-name: fall; animation-name: fall;
} }
@keyframes fall { @keyframes fall {
from { transform: translateY(-10vh) rotate(0deg); } from { transform: translateY(-10vh) rotate(0deg); }
to { transform: translateY(110vh) rotate(360deg); } to { transform: translateY(110vh) rotate(360deg); }
} }
.joke-card.absurd { .joke-card.absurd {
animation: absurd-flash 0.5s 2; animation: absurd-flash 0.5s 2;
} }
@keyframes absurd-flash { @keyframes absurd-flash {
0%, 100% { border: 2px solid transparent; } 0%, 100% { border: 2px solid transparent; }
50% { border: 5px solid red; } 50% { border: 5px solid red; }
} }
/* --- General Joke Text Visibility --- */ /* --- General Joke Text Visibility --- */
.joke-text { .joke-text {
opacity: 0; opacity: 0;
transform: scale(0.9); transform: scale(0.9);
transition: opacity 0.4s ease, transform 0.4s ease; transition: opacity 0.4s ease, transform 0.4s ease;
} }
.joke-text.visible { .joke-text.visible {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
/* --- Responsive --- */ /* --- Responsive --- */
@media (max-width: 600px) { @media (max-width: 600px) {
.theme-comic header h1 { font-size: 3em; } .theme-comic header h1 { font-size: 3em; }
.theme-comic .joke-card { padding: 25px; transform: rotate(0); } .theme-comic .joke-card { padding: 25px; transform: rotate(0); }
.theme-comic .joke-card:hover { transform: rotate(0); } .theme-comic .joke-card:hover { transform: rotate(0); }
.theme-comic .joke-text { font-size: 1.5em; } .theme-comic .joke-text { font-size: 1.5em; }
} }

View File

@@ -1,57 +1,57 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>段子游乐场</title> <title>段子游乐场</title>
<link rel="stylesheet" href="css/background.css"> <link rel="stylesheet" href="css/background.css">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
</head> </head>
<body class="theme-comic"> <!-- Default Theme --> <body class="theme-comic"> <!-- Default Theme -->
<div class="theme-switcher"> <div class="theme-switcher">
<div class="theme-icon" data-theme="theme-comic" title="手绘漫画">✏️</div> <div class="theme-icon" data-theme="theme-comic" title="手绘漫画">✏️</div>
</div> </div>
<div class="container"> <div class="container">
<header> <header>
<h1>段子游乐场</h1> <h1>段子游乐场</h1>
<div class="divider"></div> <div class="divider"></div>
</header> </header>
<main> <main>
<div id="joke-card" class="joke-card"> <div id="joke-card" class="joke-card">
<div class="loading-container"> <div class="loading-container">
<div class="loading-anim"> <div class="loading-anim">
<div class="book"> <div class="book">
<div class="book-page"></div> <div class="book-page"></div>
<div class="book-page"></div> <div class="book-page"></div>
<div class="book-page"></div> <div class="book-page"></div>
</div> </div>
</div> </div>
<p>段子菌正在翻笑话库...</p> <p>段子菌正在翻笑话库...</p>
</div> </div>
<p id="joke-text" class="joke-text"></p> <p id="joke-text" class="joke-text"></p>
</div> </div>
<div class="feedback-buttons"> <div class="feedback-buttons">
<button id="btn-lol" class="feedback-btn" title="笑到拍桌">🤣</button> <button id="btn-lol" class="feedback-btn" title="笑到拍桌">🤣</button>
<button id="btn-cold" class="feedback-btn" title="有点冷">🥶</button> <button id="btn-cold" class="feedback-btn" title="有点冷">🥶</button>
<button id="btn-seen" class="feedback-btn" title="似曾相识">🤔</button> <button id="btn-seen" class="feedback-btn" title="似曾相识">🤔</button>
<button id="btn-absurd" class="feedback-btn" title="离谱但好笑">🤯</button> <button id="btn-absurd" class="feedback-btn" title="离谱但好笑">🤯</button>
</div> </div>
<button id="new-joke-btn" class="new-joke-btn"> <button id="new-joke-btn" class="new-joke-btn">
<span class="btn-text">再来一个!</span> <span class="btn-text">再来一个!</span>
</button> </button>
</main> </main>
</div> </div>
<!-- Animation & Sound Containers --> <!-- Animation & Sound Containers -->
<div id="animation-container"></div> <div id="animation-container"></div>
<audio id="sound-lol" src="https://www.myinstants.com/media/sounds/yay-6326.mp3" preload="auto"></audio> <audio id="sound-lol" src="https://www.myinstants.com/media/sounds/yay-6326.mp3" preload="auto"></audio>
<audio id="sound-cold" src="https://www.myinstants.com/media/sounds/zapsplat_cartoon_whoosh_fast_swoosh_001_76761.mp3" preload="auto"></audio> <audio id="sound-cold" src="https://www.myinstants.com/media/sounds/zapsplat_cartoon_whoosh_fast_swoosh_001_76761.mp3" preload="auto"></audio>
<script src="js/script.js"></script> <script src="js/script.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,122 +1,122 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Elements // Elements
const body = document.body; const body = document.body;
const jokeTextElem = document.getElementById('joke-text'); const jokeTextElem = document.getElementById('joke-text');
const newJokeBtn = document.getElementById('new-joke-btn'); const newJokeBtn = document.getElementById('new-joke-btn');
const loadingContainer = document.querySelector('.loading-container'); const loadingContainer = document.querySelector('.loading-container');
const animationContainer = document.getElementById('animation-container'); const animationContainer = document.getElementById('animation-container');
const jokeCard = document.getElementById('joke-card'); const jokeCard = document.getElementById('joke-card');
// API // API
const apiBaseUrls = ["https://60s.api.shumengya.top"]; const apiBaseUrls = ["https://60s.api.shumengya.top"];
const apiPath = "/v2/duanzi"; const apiPath = "/v2/duanzi";
let currentApiIndex = 0; let currentApiIndex = 0;
// --- Core Functions --- // --- Core Functions ---
const showLoading = (isLoading) => { const showLoading = (isLoading) => {
loadingContainer.classList.toggle('visible', isLoading); loadingContainer.classList.toggle('visible', isLoading);
if (isLoading) jokeTextElem.classList.remove('visible'); if (isLoading) jokeTextElem.classList.remove('visible');
}; };
const displayJoke = (joke) => { const displayJoke = (joke) => {
jokeTextElem.textContent = joke; jokeTextElem.textContent = joke;
showLoading(false); showLoading(false);
setTimeout(() => jokeTextElem.classList.add('visible'), 50); setTimeout(() => jokeTextElem.classList.add('visible'), 50);
}; };
const fetchJoke = async () => { const fetchJoke = async () => {
showLoading(true); showLoading(true);
try { try {
const url = apiBaseUrls[currentApiIndex] + apiPath; const url = apiBaseUrls[currentApiIndex] + apiPath;
const response = await fetch(url, { timeout: 5000 }); const response = await fetch(url, { timeout: 5000 });
if (!response.ok) throw new Error('Network response was not ok'); if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json(); const data = await response.json();
if (data.code === 200 && data.data && data.data.duanzi) { if (data.code === 200 && data.data && data.data.duanzi) {
displayJoke(data.data.duanzi); displayJoke(data.data.duanzi);
} else { } else {
throw new Error('Invalid data format'); throw new Error('Invalid data format');
} }
} catch (error) { } catch (error) {
console.error(`API error with ${apiBaseUrls[currentApiIndex]}:`, error); console.error(`API error with ${apiBaseUrls[currentApiIndex]}:`, error);
currentApiIndex = (currentApiIndex + 1) % apiBaseUrls.length; currentApiIndex = (currentApiIndex + 1) % apiBaseUrls.length;
if (currentApiIndex !== 0) { if (currentApiIndex !== 0) {
fetchJoke(); // Try next API fetchJoke(); // Try next API
} else { } else {
displayJoke('段子菌迷路了!点击‘再来一个’让它重新找路~'); displayJoke('段子菌迷路了!点击‘再来一个’让它重新找路~');
} }
} }
}; };
// --- Theme Switcher --- // --- Theme Switcher ---
const themeSwitcher = document.querySelector('.theme-switcher'); const themeSwitcher = document.querySelector('.theme-switcher');
themeSwitcher.addEventListener('click', (e) => { themeSwitcher.addEventListener('click', (e) => {
if (e.target.classList.contains('theme-icon')) { if (e.target.classList.contains('theme-icon')) {
const theme = e.target.dataset.theme; const theme = e.target.dataset.theme;
body.className = theme; // Set body class to the selected theme body.className = theme; // Set body class to the selected theme
// Update active icon // Update active icon
themeSwitcher.querySelectorAll('.theme-icon').forEach(icon => icon.classList.remove('active')); themeSwitcher.querySelectorAll('.theme-icon').forEach(icon => icon.classList.remove('active'));
e.target.classList.add('active'); e.target.classList.add('active');
alert(`主题已切换!部分主题(如表情包、复古电视)将在后续阶段实现。`); alert(`主题已切换!部分主题(如表情包、复古电视)将在后续阶段实现。`);
} }
}); });
// Set initial active theme icon // Set initial active theme icon
themeSwitcher.querySelector(`[data-theme="${body.className}"]`).classList.add('active'); themeSwitcher.querySelector(`[data-theme="${body.className}"]`).classList.add('active');
// --- Feedback Buttons & Animations --- // --- Feedback Buttons & Animations ---
const btnLol = document.getElementById('btn-lol'); const btnLol = document.getElementById('btn-lol');
const btnCold = document.getElementById('btn-cold'); const btnCold = document.getElementById('btn-cold');
const btnSeen = document.getElementById('btn-seen'); const btnSeen = document.getElementById('btn-seen');
const btnAbsurd = document.getElementById('btn-absurd'); const btnAbsurd = document.getElementById('btn-absurd');
const soundLol = document.getElementById('sound-lol'); const soundLol = document.getElementById('sound-lol');
const soundCold = document.getElementById('sound-cold'); const soundCold = document.getElementById('sound-cold');
btnLol.addEventListener('click', () => { btnLol.addEventListener('click', () => {
soundLol.play(); soundLol.play();
createParticles(20, 'confetti'); createParticles(20, 'confetti');
}); });
btnCold.addEventListener('click', () => { btnCold.addEventListener('click', () => {
soundCold.play(); soundCold.play();
createParticles(15, 'snowflake'); createParticles(15, 'snowflake');
}); });
btnSeen.addEventListener('click', () => { btnSeen.addEventListener('click', () => {
displayJoke("原来你也听过!那再给你换个新鲜的~"); displayJoke("原来你也听过!那再给你换个新鲜的~");
setTimeout(fetchJoke, 1500); setTimeout(fetchJoke, 1500);
}); });
btnAbsurd.addEventListener('click', () => { btnAbsurd.addEventListener('click', () => {
jokeCard.classList.add('absurd'); jokeCard.classList.add('absurd');
setTimeout(() => jokeCard.classList.remove('absurd'), 1000); setTimeout(() => jokeCard.classList.remove('absurd'), 1000);
}); });
function createParticles(count, type) { function createParticles(count, type) {
animationContainer.innerHTML = ''; // Clear previous animationContainer.innerHTML = ''; // Clear previous
const colors = ['#ffca28', '#ff7043', '#29b6f6', '#66bb6a']; const colors = ['#ffca28', '#ff7043', '#29b6f6', '#66bb6a'];
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const particle = document.createElement('div'); const particle = document.createElement('div');
particle.classList.add(type); particle.classList.add(type);
if (type === 'confetti') { if (type === 'confetti') {
particle.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)]; particle.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
} else { } else {
particle.textContent = '❄️'; particle.textContent = '❄️';
} }
particle.style.left = `${Math.random() * 100}vw`; particle.style.left = `${Math.random() * 100}vw`;
const duration = Math.random() * 3 + 2; // 2-5 seconds const duration = Math.random() * 3 + 2; // 2-5 seconds
const delay = Math.random() * -duration; // Start at different times const delay = Math.random() * -duration; // Start at different times
particle.style.animationDuration = `${duration}s`; particle.style.animationDuration = `${duration}s`;
particle.style.animationDelay = `${delay}s`; particle.style.animationDelay = `${delay}s`;
animationContainer.appendChild(particle); animationContainer.appendChild(particle);
} }
} }
// --- Event Listeners --- // --- Event Listeners ---
newJokeBtn.addEventListener('click', fetchJoke); newJokeBtn.addEventListener('click', fetchJoke);
// --- Initial Load --- // --- Initial Load ---
fetchJoke(); fetchJoke();
}); });

View File

@@ -1,8 +1,8 @@
{ {
"code": 200, "code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841", "message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": { "data": {
"index": 347, "index": 347,
"duanzi": "我不想读书,主要是因为家里牛啊,猪啊羊啊都没人喂。" "duanzi": "我不想读书,主要是因为家里牛啊,猪啊羊啊都没人喂。"
} }
} }

View File

@@ -1,10 +1,10 @@
{ {
"code": 200, "code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841", "message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": { "data": {
"id": "63", "id": "63",
"answer": "那不值得纠结", "answer": "那不值得纠结",
"answer_en": "It's not worth worrying about", "answer_en": "It's not worth worrying about",
"index": 62 "index": 62
} }
} }

View File

@@ -1,26 +1,26 @@
body { body {
background: linear-gradient(-45deg, #f1f8e9, #e8f5e8, #c8e6c9, #dcedc8); background: linear-gradient(-45deg, #f1f8e9, #e8f5e8, #c8e6c9, #dcedc8);
background-size: 400% 400%; background-size: 400% 400%;
animation: gradientBG 20s ease infinite; animation: gradientBG 20s ease infinite;
color: #2e7d32; color: #2e7d32;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0; margin: 0;
padding: 0; padding: 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 100vh; min-height: 100vh;
overflow-x: hidden; overflow-x: hidden;
} }
@keyframes gradientBG { @keyframes gradientBG {
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
} }

View File

@@ -1,342 +1,342 @@
.container { .container {
text-align: center; text-align: center;
padding: 20px; padding: 20px;
max-width: 600px; max-width: 600px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
header h1 { header h1 {
font-size: 2.8em; font-size: 2.8em;
color: #2e7d32; color: #2e7d32;
text-shadow: 0 0 10px #81c784, 0 0 20px #a5d6a7; text-shadow: 0 0 10px #81c784, 0 0 20px #a5d6a7;
margin-bottom: 0.2em; margin-bottom: 0.2em;
} }
header p { header p {
font-size: 1.2em; font-size: 1.2em;
color: #388e3c; color: #388e3c;
margin-bottom: 40px; margin-bottom: 40px;
} }
.crystal-ball-container { .crystal-ball-container {
perspective: 1000px; perspective: 1000px;
margin-bottom: 40px; margin-bottom: 40px;
} }
.crystal-ball { .crystal-ball {
width: 200px; width: 200px;
height: 200px; height: 200px;
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.6), rgba(200, 230, 201, 0.3)); background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.6), rgba(200, 230, 201, 0.3));
border-radius: 50%; border-radius: 50%;
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
box-shadow: 0 0 30px #81c784, 0 0 60px #66bb6a, inset 0 0 20px rgba(220, 255, 220, 0.3); box-shadow: 0 0 30px #81c784, 0 0 60px #66bb6a, inset 0 0 20px rgba(220, 255, 220, 0.3);
animation: float 6s ease-in-out infinite; animation: float 6s ease-in-out infinite;
transform-style: preserve-3d; transform-style: preserve-3d;
} }
.reflection { .reflection {
width: 80px; width: 80px;
height: 40px; height: 40px;
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
border-radius: 50%; border-radius: 50%;
position: absolute; position: absolute;
top: 20px; top: 20px;
left: 40px; left: 40px;
transform: rotate(-30deg); transform: rotate(-30deg);
filter: blur(5px); filter: blur(5px);
} }
.swirl { .swirl {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
width: 120%; width: 120%;
height: 120%; height: 120%;
background: linear-gradient(45deg, rgba(200, 230, 201, 0.2), rgba(129, 199, 132, 0.3)); background: linear-gradient(45deg, rgba(200, 230, 201, 0.2), rgba(129, 199, 132, 0.3));
border-radius: 50%; border-radius: 50%;
animation: swirl 10s linear infinite; animation: swirl 10s linear infinite;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
@keyframes float { @keyframes float {
0%, 100% { transform: translateY(0); } 0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); } 50% { transform: translateY(-20px); }
} }
@keyframes swirl { @keyframes swirl {
from { transform: translate(-50%, -50%) rotate(0deg); } from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); } to { transform: translate(-50%, -50%) rotate(360deg); }
} }
.fortune-card { .fortune-card {
background: rgba(248, 255, 248, 0.8); background: rgba(248, 255, 248, 0.8);
border-radius: 15px; border-radius: 15px;
padding: 30px; padding: 30px;
margin-bottom: 30px; margin-bottom: 30px;
min-height: 120px; min-height: 120px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border: 1px solid rgba(129, 199, 132, 0.3); border: 1px solid rgba(129, 199, 132, 0.3);
box-shadow: 0 8px 32px 0 rgba(102, 187, 106, 0.2); box-shadow: 0 8px 32px 0 rgba(102, 187, 106, 0.2);
transition: opacity 0.5s ease-in-out; transition: opacity 0.5s ease-in-out;
} }
.fortune-content { .fortune-content {
opacity: 0; opacity: 0;
transition: opacity 0.5s ease-in-out; transition: opacity 0.5s ease-in-out;
} }
.fortune-content.visible { .fortune-content.visible {
opacity: 1; opacity: 1;
} }
#luck-desc { #luck-desc {
font-size: 2em; font-size: 2em;
color: #2e7d32; color: #2e7d32;
margin: 0 0 10px; margin: 0 0 10px;
} }
#luck-tip { #luck-tip {
font-size: 1.1em; font-size: 1.1em;
color: #388e3c; color: #388e3c;
margin: 0; margin: 0;
padding-bottom: 20px; /* Add some space before the new details */ padding-bottom: 20px; /* Add some space before the new details */
} }
.fortune-details { .fortune-details {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
margin-top: 20px; margin-top: 20px;
padding-top: 20px; padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.2); border-top: 1px solid rgba(255, 255, 255, 0.2);
} }
.detail-item { .detail-item {
text-align: center; text-align: center;
} }
.detail-item h3 { .detail-item h3 {
font-size: 0.9em; font-size: 0.9em;
color: #66bb6a; color: #66bb6a;
margin: 0 0 5px; margin: 0 0 5px;
font-weight: normal; font-weight: normal;
} }
.detail-item p { .detail-item p {
font-size: 1.2em; font-size: 1.2em;
margin: 0; margin: 0;
font-weight: bold; font-weight: bold;
} }
#lucky-color { #lucky-color {
display: inline-block; display: inline-block;
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 50%; border-radius: 50%;
border: 2px solid white; border: 2px solid white;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
/* Remove the text content */ /* Remove the text content */
font-size: 0; font-size: 0;
} }
/* Tarot Card Styles */ /* Tarot Card Styles */
.tarot-container { .tarot-container {
margin-top: 40px; margin-top: 40px;
margin-bottom: 40px; margin-bottom: 40px;
} }
.tarot-container h2 { .tarot-container h2 {
font-size: 1.5em; font-size: 1.5em;
color: #2e7d32; color: #2e7d32;
text-shadow: 0 0 8px #81c784; text-shadow: 0 0 8px #81c784;
margin-bottom: 20px; margin-bottom: 20px;
} }
.tarot-card-container { .tarot-card-container {
width: 180px; width: 180px;
height: 280px; height: 280px;
perspective: 1000px; perspective: 1000px;
margin: 0 auto; margin: 0 auto;
cursor: pointer; cursor: pointer;
} }
.tarot-card-inner { .tarot-card-inner {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
transition: transform 0.8s; transition: transform 0.8s;
transform-style: preserve-3d; transform-style: preserve-3d;
} }
.tarot-card-container.flipped .tarot-card-inner { .tarot-card-container.flipped .tarot-card-inner {
transform: rotateY(180deg); transform: rotateY(180deg);
} }
.tarot-card-front, .tarot-card-front,
.tarot-card-back { .tarot-card-back {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
-webkit-backface-visibility: hidden; -webkit-backface-visibility: hidden;
backface-visibility: hidden; backface-visibility: hidden;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
} }
.tarot-card-back { .tarot-card-back {
background: linear-gradient(135deg, #66bb6a, #388e3c); background: linear-gradient(135deg, #66bb6a, #388e3c);
border: 2px solid #81c784; border: 2px solid #81c784;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 3em; font-size: 3em;
color: #c8e6c9; color: #c8e6c9;
} }
.tarot-card-back::after { .tarot-card-back::after {
content: '✧'; /* A simple star symbol */ content: '✧'; /* A simple star symbol */
text-shadow: 0 0 10px #e8f5e8; text-shadow: 0 0 10px #e8f5e8;
} }
.tarot-card-front { .tarot-card-front {
background: linear-gradient(135deg, #4caf50, #66bb6a); background: linear-gradient(135deg, #4caf50, #66bb6a);
border: 2px solid #81c784; border: 2px solid #81c784;
color: white; color: white;
transform: rotateY(180deg); transform: rotateY(180deg);
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
#tarot-name { #tarot-name {
font-size: 1.4em; font-size: 1.4em;
color: #e8f5e8; color: #e8f5e8;
margin: 0 0 10px; margin: 0 0 10px;
} }
#tarot-interpretation { #tarot-interpretation {
font-size: 0.9em; font-size: 0.9em;
text-align: center; text-align: center;
margin: 0; margin: 0;
} }
/* Side Decorations */ /* Side Decorations */
.side-decor { .side-decor {
position: fixed; position: fixed;
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 15vw; width: 15vw;
height: 100vh; height: 100vh;
pointer-events: none; pointer-events: none;
z-index: 0; z-index: 0;
} }
.left-decor { .left-decor {
left: 0; left: 0;
} }
.right-decor { .right-decor {
right: 0; right: 0;
} }
.decor-symbol { .decor-symbol {
position: absolute; position: absolute;
color: rgba(129, 199, 132, 0.5); color: rgba(129, 199, 132, 0.5);
text-shadow: 0 0 10px rgba(200, 230, 201, 0.7); text-shadow: 0 0 10px rgba(200, 230, 201, 0.7);
animation: floatSymbol 20s infinite ease-in-out; animation: floatSymbol 20s infinite ease-in-out;
} }
@keyframes floatSymbol { @keyframes floatSymbol {
0%, 100% { 0%, 100% {
transform: translateY(0) rotate(0deg); transform: translateY(0) rotate(0deg);
opacity: 0; opacity: 0;
} }
25%, 75% { 25%, 75% {
opacity: 0.8; opacity: 0.8;
} }
50% { 50% {
transform: translateY(-20vh) rotate(180deg); transform: translateY(-20vh) rotate(180deg);
opacity: 0.3; opacity: 0.3;
} }
} }
#get-fortune-btn { #get-fortune-btn {
background: linear-gradient(45deg, #66bb6a, #4caf50); background: linear-gradient(45deg, #66bb6a, #4caf50);
color: white; color: white;
border: none; border: none;
border-radius: 50px; border-radius: 50px;
padding: 15px 30px; padding: 15px 30px;
font-size: 1.1em; font-size: 1.1em;
cursor: pointer; cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 0 15px #81c784; box-shadow: 0 0 15px #81c784;
} }
#get-fortune-btn:hover { #get-fortune-btn:hover {
transform: scale(1.05); transform: scale(1.05);
box-shadow: 0 0 25px #a5d6a7; box-shadow: 0 0 25px #a5d6a7;
} }
#get-fortune-btn:active { #get-fortune-btn:active {
transform: scale(0.98); transform: scale(0.98);
} }
.loading-spinner { .loading-spinner {
border: 4px solid rgba(129, 199, 132, 0.3); border: 4px solid rgba(129, 199, 132, 0.3);
border-left-color: #66bb6a; border-left-color: #66bb6a;
border-radius: 50%; border-radius: 50%;
width: 40px; width: 40px;
height: 40px; height: 40px;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
display: none; /* Hidden by default */ display: none; /* Hidden by default */
} }
.loading-spinner.visible { .loading-spinner.visible {
display: block; display: block;
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
footer { footer {
margin-top: 40px; margin-top: 40px;
color: rgba(46, 125, 50, 0.7); color: rgba(46, 125, 50, 0.7);
} }
/* Responsive Design */ /* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
header h1 { header h1 {
font-size: 2.2em; font-size: 2.2em;
} }
.crystal-ball { .crystal-ball {
width: 150px; width: 150px;
height: 150px; height: 150px;
} }
.fortune-card { .fortune-card {
padding: 20px; padding: 20px;
} }
.fortune-details { .fortune-details {
flex-direction: column; flex-direction: column;
gap: 15px; gap: 15px;
} }
.tarot-card-container { .tarot-card-container {
width: 150px; width: 150px;
height: 233px; height: 233px;
} }
} }
/* Hide side decor on smaller screens */ /* Hide side decor on smaller screens */
@media (max-width: 1200px) { @media (max-width: 1200px) {
.side-decor { .side-decor {
display: none; display: none;
} }
} }

View File

@@ -1,71 +1,71 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>水晶球占卜</title> <title>水晶球占卜</title>
<link rel="stylesheet" href="css/background.css"> <link rel="stylesheet" href="css/background.css">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
</head> </head>
<body> <body>
<div class="side-decor left-decor"></div> <div class="side-decor left-decor"></div>
<div class="container"> <div class="container">
<header> <header>
<h1>水晶球占卜</h1> <h1>水晶球占卜</h1>
<p>洞察你今日的运势</p> <p>洞察你今日的运势</p>
</header> </header>
<main> <main>
<div class="crystal-ball-container"> <div class="crystal-ball-container">
<div class="crystal-ball"> <div class="crystal-ball">
<div class="reflection"></div> <div class="reflection"></div>
<div class="swirl"></div> <div class="swirl"></div>
</div> </div>
</div> </div>
<div id="fortune-card" class="fortune-card"> <div id="fortune-card" class="fortune-card">
<div class="loading-spinner"></div> <div class="loading-spinner"></div>
<div class="fortune-content"> <div class="fortune-content">
<h2 id="luck-desc"></h2> <h2 id="luck-desc"></h2>
<p id="luck-tip"></p> <p id="luck-tip"></p>
<div class="fortune-details"> <div class="fortune-details">
<div class="detail-item"> <div class="detail-item">
<h3>今日咒语</h3> <h3>今日咒语</h3>
<p id="fortune-summary"></p> <p id="fortune-summary"></p>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<h3>幸运色</h3> <h3>幸运色</h3>
<p id="lucky-color"></p> <p id="lucky-color"></p>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<h3>幸运数字</h3> <h3>幸运数字</h3>
<p id="lucky-number"></p> <p id="lucky-number"></p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- New Tarot Card Section --> <!-- New Tarot Card Section -->
<div class="tarot-container"> <div class="tarot-container">
<h2>每日塔罗指引</h2> <h2>每日塔罗指引</h2>
<div id="tarot-card" class="tarot-card-container"> <div id="tarot-card" class="tarot-card-container">
<div class="tarot-card-inner"> <div class="tarot-card-inner">
<div class="tarot-card-back"> <div class="tarot-card-back">
<!-- Back of the card design --> <!-- Back of the card design -->
</div> </div>
<div class="tarot-card-front"> <div class="tarot-card-front">
<h3 id="tarot-name"></h3> <h3 id="tarot-name"></h3>
<p id="tarot-interpretation"></p> <p id="tarot-interpretation"></p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<button id="get-fortune-btn">再次占卜</button> <button id="get-fortune-btn">再次占卜</button>
</main> </main>
<footer> <footer>
<p>仅供娱乐,祝您好运</p> <p>仅供娱乐,祝您好运</p>
</footer> </footer>
</div> </div>
<div class="side-decor right-decor"></div> <div class="side-decor right-decor"></div>
<script src="js/script.js"></script> <script src="js/script.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,170 +1,170 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const getFortuneBtn = document.getElementById('get-fortune-btn'); const getFortuneBtn = document.getElementById('get-fortune-btn');
const fortuneCard = document.getElementById('fortune-card'); const fortuneCard = document.getElementById('fortune-card');
const fortuneContent = fortuneCard.querySelector('.fortune-content'); const fortuneContent = fortuneCard.querySelector('.fortune-content');
const luckDescElem = document.getElementById('luck-desc'); const luckDescElem = document.getElementById('luck-desc');
const luckTipElem = document.getElementById('luck-tip'); const luckTipElem = document.getElementById('luck-tip');
const fortuneSummaryElem = document.getElementById('fortune-summary'); const fortuneSummaryElem = document.getElementById('fortune-summary');
const luckyColorElem = document.getElementById('lucky-color'); const luckyColorElem = document.getElementById('lucky-color');
const luckyNumberElem = document.getElementById('lucky-number'); const luckyNumberElem = document.getElementById('lucky-number');
const loadingSpinner = fortuneCard.querySelector('.loading-spinner'); const loadingSpinner = fortuneCard.querySelector('.loading-spinner');
const tarotCardContainer = document.getElementById('tarot-card'); const tarotCardContainer = document.getElementById('tarot-card');
const tarotNameElem = document.getElementById('tarot-name'); const tarotNameElem = document.getElementById('tarot-name');
const tarotInterpretationElem = document.getElementById('tarot-interpretation'); const tarotInterpretationElem = document.getElementById('tarot-interpretation');
const apiBaseUrls = [ const apiBaseUrls = [
"https://60s.api.shumengya.top", "https://60s.api.shumengya.top",
]; ];
const apiPath = "/v2/luck"; const apiPath = "/v2/luck";
const mantras = [ const mantras = [
"顺其自然,皆是美好。", "顺其自然,皆是美好。",
"相信直觉,它知道方向。", "相信直觉,它知道方向。",
"每一次呼吸都是新的开始。", "每一次呼吸都是新的开始。",
"心怀感恩,好运自来。", "心怀感恩,好运自来。",
"拥抱变化,发现惊喜。", "拥抱变化,发现惊喜。",
"你的能量,超乎想象。", "你的能量,超乎想象。",
"保持微笑,宇宙会回应你。" "保持微笑,宇宙会回应你。"
]; ];
const tarotDeck = [ const tarotDeck = [
{ name: "愚者", interpretation: "新的开始,无限的潜力,天真和自由。勇敢地迈出第一步。" }, { name: "愚者", interpretation: "新的开始,无限的潜力,天真和自由。勇敢地迈出第一步。" },
{ name: "魔术师", interpretation: "创造力,意志力,显化。你拥有实现目标所需的一切资源。" }, { name: "魔术师", interpretation: "创造力,意志力,显化。你拥有实现目标所需的一切资源。" },
{ name: "女祭司", interpretation: "直觉,潜意识,神秘。倾听你内心的声音,智慧在你之内。" }, { name: "女祭司", interpretation: "直觉,潜意识,神秘。倾听你内心的声音,智慧在你之内。" },
{ name: "皇后", interpretation: "丰饶,母性,创造。享受生活的美好,与自然和谐相处。" }, { name: "皇后", interpretation: "丰饶,母性,创造。享受生活的美好,与自然和谐相处。" },
{ name: "皇帝", interpretation: "权威,结构,控制。建立秩序和纪律,掌控你的生活。" }, { name: "皇帝", interpretation: "权威,结构,控制。建立秩序和纪律,掌控你的生活。" },
{ name: "教皇", interpretation: "传统,信仰,灵性指导。寻求智慧和知识,遵循传统。" }, { name: "教皇", interpretation: "传统,信仰,灵性指导。寻求智慧和知识,遵循传统。" },
{ name: "恋人", interpretation: "爱,和谐,选择。做出与你内心价值观一致的决定。" }, { name: "恋人", interpretation: "爱,和谐,选择。做出与你内心价值观一致的决定。" },
{ name: "战车", interpretation: "胜利,决心,控制。以坚定的意志力克服障碍,勇往直前。" }, { name: "战车", interpretation: "胜利,决心,控制。以坚定的意志力克服障碍,勇往直前。" },
{ name: "力量", interpretation: "勇气,内在力量,同情。用温柔和耐心驯服内心的野兽。" }, { name: "力量", interpretation: "勇气,内在力量,同情。用温柔和耐心驯服内心的野兽。" },
{ name: "隐士", interpretation: "内省,孤独,寻求真理。花时间独处,向内寻求答案。" }, { name: "隐士", interpretation: "内省,孤独,寻求真理。花时间独处,向内寻求答案。" },
{ name: "命运之轮", interpretation: "变化,命运,转折点。生活总在变化,顺应潮流。" }, { name: "命运之轮", interpretation: "变化,命运,转折点。生活总在变化,顺应潮流。" },
{ name: "正义", interpretation: "公平,真理,因果。为你的行为负责,寻求平衡。" }, { name: "正义", interpretation: "公平,真理,因果。为你的行为负责,寻求平衡。" },
{ name: "倒吊人", interpretation: "新的视角,顺从,牺牲。放手,从不同的角度看问题。" }, { name: "倒吊人", interpretation: "新的视角,顺从,牺牲。放手,从不同的角度看问题。" },
{ name: "死神", interpretation: "结束,转变,新生。一个周期的结束是另一个周期的开始。" }, { name: "死神", interpretation: "结束,转变,新生。一个周期的结束是另一个周期的开始。" },
{ name: "节制", interpretation: "平衡,和谐,耐心。融合对立的力量,找到中间道路。" }, { name: "节制", interpretation: "平衡,和谐,耐心。融合对立的力量,找到中间道路。" },
{ name: "恶魔", interpretation: "束缚,物质主义,诱惑。认识到你的束缚,并寻求解放。" }, { name: "恶魔", interpretation: "束缚,物质主义,诱惑。认识到你的束缚,并寻求解放。" },
{ name: "塔", interpretation: "突变,启示,解放。旧的结构正在崩塌,为新的结构让路。" }, { name: "塔", interpretation: "突变,启示,解放。旧的结构正在崩塌,为新的结构让路。" },
{ name: "星星", interpretation: "希望,灵感,平静。在黑暗之后,总有希望的曙光。" }, { name: "星星", interpretation: "希望,灵感,平静。在黑暗之后,总有希望的曙光。" },
{ name: "月亮", interpretation: "幻觉,恐惧,潜意识。面对你的恐惧,相信你的直觉。" }, { name: "月亮", interpretation: "幻觉,恐惧,潜意识。面对你的恐惧,相信你的直觉。" },
{ name: "太阳", interpretation: "成功,喜悦,活力。拥抱光明,享受生活的乐趣。" }, { name: "太阳", interpretation: "成功,喜悦,活力。拥抱光明,享受生活的乐趣。" },
{ name: "审判", interpretation: "觉醒,重生,评估。一个反思和更新的时刻。" }, { name: "审判", interpretation: "觉醒,重生,评估。一个反思和更新的时刻。" },
{ name: "世界", interpretation: "完成,整合,成就。一个旅程的成功结束,庆祝你的成就。" } { name: "世界", interpretation: "完成,整合,成就。一个旅程的成功结束,庆祝你的成就。" }
]; ];
let currentApiIndex = 0; let currentApiIndex = 0;
const showLoading = (isLoading) => { const showLoading = (isLoading) => {
if (isLoading) { if (isLoading) {
fortuneContent.classList.remove('visible'); fortuneContent.classList.remove('visible');
loadingSpinner.classList.add('visible'); loadingSpinner.classList.add('visible');
} else { } else {
loadingSpinner.classList.remove('visible'); loadingSpinner.classList.remove('visible');
setTimeout(() => { setTimeout(() => {
fortuneContent.classList.add('visible'); fortuneContent.classList.add('visible');
}, 100); }, 100);
} }
}; };
const fetchFortune = async () => { const fetchFortune = async () => {
showLoading(true); showLoading(true);
tarotCardContainer.classList.remove('flipped'); // Reset card on new fetch tarotCardContainer.classList.remove('flipped'); // Reset card on new fetch
try { try {
const url = apiBaseUrls[currentApiIndex] + apiPath; const url = apiBaseUrls[currentApiIndex] + apiPath;
const response = await fetch(url, { timeout: 5000 }); const response = await fetch(url, { timeout: 5000 });
if (!response.ok) { if (!response.ok) {
throw new Error('Network response was not ok'); throw new Error('Network response was not ok');
} }
const data = await response.json(); const data = await response.json();
if (data.code === 200 && data.data) { if (data.code === 200 && data.data) {
updateFortune(data.data); updateFortune(data.data);
drawTarotCard(); // Draw a tarot card on success drawTarotCard(); // Draw a tarot card on success
} else { } else {
throw new Error('Invalid data format'); throw new Error('Invalid data format');
} }
} catch (error) { } catch (error) {
console.error(`API error with ${apiBaseUrls[currentApiIndex]}:`, error); console.error(`API error with ${apiBaseUrls[currentApiIndex]}:`, error);
currentApiIndex = (currentApiIndex + 1) % apiBaseUrls.length; currentApiIndex = (currentApiIndex + 1) % apiBaseUrls.length;
if (currentApiIndex !== 0) { if (currentApiIndex !== 0) {
fetchFortune(); // Try next API fetchFortune(); // Try next API
} else { } else {
displayError(); displayError();
} }
} }
}; };
const updateFortune = (data) => { const updateFortune = (data) => {
luckDescElem.textContent = data.luck_desc || '运势'; luckDescElem.textContent = data.luck_desc || '运势';
luckTipElem.textContent = data.luck_tip || '今日运势平平,保持好心情。'; luckTipElem.textContent = data.luck_tip || '今日运势平平,保持好心情。';
// Generate and display additional details // Generate and display additional details
fortuneSummaryElem.textContent = mantras[Math.floor(Math.random() * mantras.length)]; fortuneSummaryElem.textContent = mantras[Math.floor(Math.random() * mantras.length)];
luckyColorElem.style.backgroundColor = `#${Math.floor(Math.random()*16777215).toString(16).padStart(6, '0')}`; luckyColorElem.style.backgroundColor = `#${Math.floor(Math.random()*16777215).toString(16).padStart(6, '0')}`;
luckyNumberElem.textContent = Math.floor(Math.random() * 100); luckyNumberElem.textContent = Math.floor(Math.random() * 100);
showLoading(false); showLoading(false);
}; };
const displayError = () => { const displayError = () => {
luckDescElem.textContent = '占卜失败'; luckDescElem.textContent = '占卜失败';
luckTipElem.textContent = '无法连接到星辰之力,请稍后再试。'; luckTipElem.textContent = '无法连接到星辰之力,请稍后再试。';
fortuneSummaryElem.textContent = '---'; fortuneSummaryElem.textContent = '---';
luckyColorElem.style.backgroundColor = 'transparent'; luckyColorElem.style.backgroundColor = 'transparent';
luckyNumberElem.textContent = '-'; luckyNumberElem.textContent = '-';
showLoading(false); showLoading(false);
tarotNameElem.textContent = '指引中断'; tarotNameElem.textContent = '指引中断';
tarotInterpretationElem.textContent = '星辰之力暂时无法连接。'; tarotInterpretationElem.textContent = '星辰之力暂时无法连接。';
tarotCardContainer.classList.add('flipped'); // Show error on card tarotCardContainer.classList.add('flipped'); // Show error on card
}; };
const drawTarotCard = () => { const drawTarotCard = () => {
const card = tarotDeck[Math.floor(Math.random() * tarotDeck.length)]; const card = tarotDeck[Math.floor(Math.random() * tarotDeck.length)];
tarotNameElem.textContent = card.name; tarotNameElem.textContent = card.name;
tarotInterpretationElem.textContent = card.interpretation; tarotInterpretationElem.textContent = card.interpretation;
// Flip the card after a short delay to allow the main content to appear // Flip the card after a short delay to allow the main content to appear
setTimeout(() => { setTimeout(() => {
tarotCardContainer.classList.add('flipped'); tarotCardContainer.classList.add('flipped');
}, 500); }, 500);
}; };
const createSideDecorations = () => { const createSideDecorations = () => {
const leftContainer = document.querySelector('.left-decor'); const leftContainer = document.querySelector('.left-decor');
const rightContainer = document.querySelector('.right-decor'); const rightContainer = document.querySelector('.right-decor');
if (!leftContainer || !rightContainer) return; if (!leftContainer || !rightContainer) return;
const symbols = ['✧', '✦', '☾', '✶', '✵', '✩', '✨']; const symbols = ['✧', '✦', '☾', '✶', '✵', '✩', '✨'];
const symbolCount = 15; // Number of symbols per side const symbolCount = 15; // Number of symbols per side
const createSymbols = (container) => { const createSymbols = (container) => {
for (let i = 0; i < symbolCount; i++) { for (let i = 0; i < symbolCount; i++) {
const symbol = document.createElement('span'); const symbol = document.createElement('span');
symbol.classList.add('decor-symbol'); symbol.classList.add('decor-symbol');
symbol.textContent = symbols[Math.floor(Math.random() * symbols.length)]; symbol.textContent = symbols[Math.floor(Math.random() * symbols.length)];
// Randomize properties for a more natural look // Randomize properties for a more natural look
symbol.style.top = `${Math.random() * 90}vh`; symbol.style.top = `${Math.random() * 90}vh`;
symbol.style.left = `${Math.random() * 80}%`; symbol.style.left = `${Math.random() * 80}%`;
symbol.style.fontSize = `${Math.random() * 20 + 10}px`; symbol.style.fontSize = `${Math.random() * 20 + 10}px`;
symbol.style.animationDelay = `${Math.random() * 20}s`; symbol.style.animationDelay = `${Math.random() * 20}s`;
symbol.style.animationDuration = `${Math.random() * 20 + 15}s`; // Duration between 15s and 35s symbol.style.animationDuration = `${Math.random() * 20 + 15}s`; // Duration between 15s and 35s
container.appendChild(symbol); container.appendChild(symbol);
} }
}; };
createSymbols(leftContainer); createSymbols(leftContainer);
createSymbols(rightContainer); createSymbols(rightContainer);
}; };
getFortuneBtn.addEventListener('click', fetchFortune); getFortuneBtn.addEventListener('click', fetchFortune);
tarotCardContainer.addEventListener('click', () => { tarotCardContainer.addEventListener('click', () => {
tarotCardContainer.classList.toggle('flipped'); tarotCardContainer.classList.toggle('flipped');
}); });
// Initial actions on page load // Initial actions on page load
fetchFortune(); fetchFortune();
createSideDecorations(); createSideDecorations();
}); });

View File

@@ -1,3 +1,3 @@
[ [
"https://60s.api.shumengya.top" "https://60s.api.shumengya.top"
] ]

View File

@@ -1,10 +1,10 @@
{ {
"code": 200, "code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841", "message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": { "data": {
"luck_desc": "恋愛運", "luck_desc": "恋愛運",
"luck_rank": 21, "luck_rank": 21,
"luck_tip": "闪亮的邂逅之日!顺其自然吧", "luck_tip": "闪亮的邂逅之日!顺其自然吧",
"luck_tip_index": 19 "luck_tip_index": 19
} }
} }

View File

@@ -1,330 +1,330 @@
/* Epic Games 免费游戏 - 淡绿色清新风格样式 */ /* Epic Games 免费游戏 - 淡绿色清新风格样式 */
/* 重置样式 */ /* 重置样式 */
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 50%, #ffd3a5 100%); background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 50%, #ffd3a5 100%);
min-height: 100vh; min-height: 100vh;
color: #2d5016; color: #2d5016;
line-height: 1.6; line-height: 1.6;
overflow-x: hidden; overflow-x: hidden;
} }
.container { .container {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
} }
/* 头部 */ /* 头部 */
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 30px;
background: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.85);
border-radius: 20px; border-radius: 20px;
padding: 24px; padding: 24px;
box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08); box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
.header h1 { .header h1 {
font-size: 2.2rem; font-size: 2.2rem;
color: #2d5016; color: #2d5016;
margin-bottom: 10px; margin-bottom: 10px;
font-weight: 700; font-weight: 700;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 12px; gap: 12px;
} }
.header p { .header p {
color: #5a7c65; color: #5a7c65;
font-size: 1rem; font-size: 1rem;
} }
/* 统计信息 */ /* 统计信息 */
.stats { .stats {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px; gap: 15px;
margin-bottom: 25px; margin-bottom: 25px;
} }
.stat-card { .stat-card {
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
padding: 16px; padding: 16px;
border-radius: 15px; border-radius: 15px;
text-align: center; text-align: center;
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08); box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
} }
.stat-number { .stat-number {
font-size: 1.8rem; font-size: 1.8rem;
font-weight: 700; font-weight: 700;
color: #1b5e20; color: #1b5e20;
margin-bottom: 5px; margin-bottom: 5px;
} }
.stat-label { .stat-label {
color: #5a7c65; color: #5a7c65;
font-size: 0.9rem; font-size: 0.9rem;
} }
/* 游戏网格 */ /* 游戏网格 */
.games-grid { .games-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px; gap: 20px;
margin-bottom: 20px; margin-bottom: 20px;
} }
/* 游戏卡片 */ /* 游戏卡片 */
.game-card { .game-card {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
border-radius: 18px; border-radius: 18px;
overflow: hidden; overflow: hidden;
box-shadow: 0 6px 25px rgba(45, 80, 22, 0.1); box-shadow: 0 6px 25px rgba(45, 80, 22, 0.1);
transition: all 0.3s ease; transition: all 0.3s ease;
position: relative; position: relative;
} }
.game-card:hover { .game-card:hover {
transform: translateY(-5px); transform: translateY(-5px);
box-shadow: 0 12px 35px rgba(45, 80, 22, 0.15); box-shadow: 0 12px 35px rgba(45, 80, 22, 0.15);
} }
.game-cover { .game-cover {
width: 100%; width: 100%;
height: 180px; height: 180px;
object-fit: cover; object-fit: cover;
border-radius: 0; border-radius: 0;
} }
.game-info { .game-info {
padding: 18px; padding: 18px;
} }
.game-title { .game-title {
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 700; font-weight: 700;
color: #1b5e20; color: #1b5e20;
margin-bottom: 8px; margin-bottom: 8px;
line-height: 1.3; line-height: 1.3;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }
.game-description { .game-description {
color: #5a7c65; color: #5a7c65;
font-size: 0.9rem; font-size: 0.9rem;
margin-bottom: 12px; margin-bottom: 12px;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
line-height: 1.4; line-height: 1.4;
} }
.game-meta { .game-meta {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 12px; margin-bottom: 12px;
} }
.game-price { .game-price {
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
} }
.original-price { .original-price {
color: #81c784; color: #81c784;
text-decoration: line-through; text-decoration: line-through;
} }
.free-price { .free-price {
color: #2e7d32; color: #2e7d32;
font-weight: 700; font-weight: 700;
} }
.game-seller { .game-seller {
color: #5a7c65; color: #5a7c65;
font-size: 0.85rem; font-size: 0.85rem;
} }
.game-dates { .game-dates {
background: rgba(129, 199, 132, 0.1); background: rgba(129, 199, 132, 0.1);
padding: 10px; padding: 10px;
border-radius: 10px; border-radius: 10px;
margin-bottom: 12px; margin-bottom: 12px;
font-size: 0.85rem; font-size: 0.85rem;
color: #2d5016; color: #2d5016;
} }
.free-period { .free-period {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 4px; margin-bottom: 4px;
} }
.game-actions { .game-actions {
display: flex; display: flex;
gap: 10px; gap: 10px;
} }
.btn { .btn {
flex: 1; flex: 1;
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%); background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
color: white; color: white;
border: none; border: none;
padding: 10px 16px; padding: 10px 16px;
border-radius: 10px; border-radius: 10px;
font-size: 0.9rem; font-size: 0.9rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.25s ease; transition: all 0.25s ease;
box-shadow: 0 4px 12px rgba(129, 199, 132, 0.35); box-shadow: 0 4px 12px rgba(129, 199, 132, 0.35);
text-decoration: none; text-decoration: none;
text-align: center; text-align: center;
display: inline-block; display: inline-block;
} }
.btn:hover { .btn:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 6px 18px rgba(129, 199, 132, 0.45); box-shadow: 0 6px 18px rgba(129, 199, 132, 0.45);
} }
.btn-secondary { .btn-secondary {
background: linear-gradient(135deg, #a5d6a7 0%, #81c784 100%); background: linear-gradient(135deg, #a5d6a7 0%, #81c784 100%);
} }
/* 状态标签 */ /* 状态标签 */
.status-badge { .status-badge {
position: absolute; position: absolute;
top: 12px; top: 12px;
right: 12px; right: 12px;
padding: 6px 12px; padding: 6px 12px;
border-radius: 20px; border-radius: 20px;
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 600; font-weight: 600;
color: white; color: white;
text-shadow: 0 1px 2px rgba(0,0,0,0.2); text-shadow: 0 1px 2px rgba(0,0,0,0.2);
} }
.status-free { .status-free {
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%); background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
} }
.status-upcoming { .status-upcoming {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%); background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
} }
/* 加载与错误 */ /* 加载与错误 */
.loading, .error { .loading, .error {
text-align: center; text-align: center;
padding: 40px; padding: 40px;
background: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.85);
border-radius: 15px; border-radius: 15px;
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08); box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
} }
.spinner { .spinner {
width: 40px; width: 40px;
height: 40px; height: 40px;
border: 4px solid #e8f5e8; border: 4px solid #e8f5e8;
border-top: 4px solid #81c784; border-top: 4px solid #81c784;
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin: 0 auto 20px; margin: 0 auto 20px;
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
/* 动画 */ /* 动画 */
.fade-in { .fade-in {
animation: fadeIn 0.6s ease-in-out; animation: fadeIn 0.6s ease-in-out;
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); } from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
/* 平板端适配 */ /* 平板端适配 */
@media (max-width: 1024px) and (min-width: 768px) { @media (max-width: 1024px) and (min-width: 768px) {
.container { padding: 16px; } .container { padding: 16px; }
.header h1 { font-size: 2rem; } .header h1 { font-size: 2rem; }
.games-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } .games-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
} }
/* 手机端优化 */ /* 手机端优化 */
@media (max-width: 767px) { @media (max-width: 767px) {
.container { padding: 12px; } .container { padding: 12px; }
.header { padding: 18px; } .header { padding: 18px; }
.header h1 { font-size: 1.8rem; gap: 8px; } .header h1 { font-size: 1.8rem; gap: 8px; }
.stats { .stats {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
} }
.stat-card { .stat-card {
padding: 10px 8px; padding: 10px 8px;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.stat-number { font-size: 1.4rem; } .stat-number { font-size: 1.4rem; }
.stat-label { font-size: 0.75rem; } .stat-label { font-size: 0.75rem; }
.games-grid { .games-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 16px; gap: 16px;
} }
.game-card { margin: 0 4px; } .game-card { margin: 0 4px; }
.game-cover { height: 160px; } .game-cover { height: 160px; }
.game-info { padding: 14px; } .game-info { padding: 14px; }
.game-actions { .game-actions {
flex-direction: column; flex-direction: column;
} }
.btn { .btn {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
} }
} }
/* 小屏手机优化 */ /* 小屏手机优化 */
@media (max-width: 480px) { @media (max-width: 480px) {
.stats { .stats {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.game-meta { .game-meta {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 6px; gap: 6px;
} }
} }
/* 高分辨率显示器优化 */ /* 高分辨率显示器优化 */
@media (min-width: 1400px) { @media (min-width: 1400px) {
.games-grid { .games-grid {
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
} }
} }

View File

@@ -1,63 +1,63 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Epic Games 免费游戏 - 60s API 集合</title> <title>Epic Games 免费游戏 - 60s API 集合</title>
<meta name="description" content="Epic Games 免费游戏列表,数据源自 60s.viki.moe提供当前免费和即将免费的游戏信息。" /> <meta name="description" content="Epic Games 免费游戏列表,数据源自 60s.viki.moe提供当前免费和即将免费的游戏信息。" />
<link rel="stylesheet" href="./css/style.css" /> <link rel="stylesheet" href="./css/style.css" />
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<header class="header"> <header class="header">
<h1> <h1>
🎮 Epic Games 免费游戏 🎮 Epic Games 免费游戏
</h1> </h1>
</header> </header>
<!-- 加载与错误状态 --> <!-- 加载与错误状态 -->
<section id="loading" class="loading"> <section id="loading" class="loading">
<div class="spinner"></div> <div class="spinner"></div>
<p>正在加载游戏数据,请稍候…</p> <p>正在加载游戏数据,请稍候…</p>
</section> </section>
<section id="error" class="error" style="display: none;"> <section id="error" class="error" style="display: none;">
<p>获取数据失败,请稍后重试</p> <p>获取数据失败,请稍后重试</p>
<button id="refresh-btn" class="btn" style="margin-top: 15px;">重新加载</button> <button id="refresh-btn" class="btn" style="margin-top: 15px;">重新加载</button>
</section> </section>
<!-- 内容区域 --> <!-- 内容区域 -->
<main id="content" style="display: none;" class="fade-in"> <main id="content" style="display: none;" class="fade-in">
<!-- 统计信息 --> <!-- 统计信息 -->
<div class="stats"> <div class="stats">
<div class="stat-card"> <div class="stat-card">
<div class="stat-number" id="total-games">0</div> <div class="stat-number" id="total-games">0</div>
<div class="stat-label">总游戏数</div> <div class="stat-label">总游戏数</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-number" id="free-now">0</div> <div class="stat-number" id="free-now">0</div>
<div class="stat-label">当前免费</div> <div class="stat-label">当前免费</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-number" id="upcoming">0</div> <div class="stat-number" id="upcoming">0</div>
<div class="stat-label">即将免费</div> <div class="stat-label">即将免费</div>
</div> </div>
</div> </div>
<!-- 游戏列表 --> <!-- 游戏列表 -->
<div class="games-grid" id="games-grid"> <div class="games-grid" id="games-grid">
<!-- 游戏卡片将通过 JavaScript 动态生成 --> <!-- 游戏卡片将通过 JavaScript 动态生成 -->
</div> </div>
<!-- 刷新按钮 --> <!-- 刷新按钮 -->
<div style="text-align: center; margin-top: 30px;"> <div style="text-align: center; margin-top: 30px;">
<button id="refresh-btn" class="btn btn-secondary">🔄 刷新数据</button> <button id="refresh-btn" class="btn btn-secondary">🔄 刷新数据</button>
</div> </div>
</main> </main>
</div> </div>
<script src="./js/script.js"></script> <script src="./js/script.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,266 +1,266 @@
// Epic Games 免费游戏 页面脚本 // Epic Games 免费游戏 页面脚本
(function () { (function () {
'use strict'; 'use strict';
const API = { const API = {
endpoints: [], endpoints: [],
currentIndex: 0, currentIndex: 0,
// 初始化API接口列表 // 初始化API接口列表
async init() { async init() {
try { try {
const res = await fetch('./接口集合.json'); const res = await fetch('./接口集合.json');
const endpoints = await res.json(); const endpoints = await res.json();
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/epic`); this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/epic`);
} catch (e) { } catch (e) {
// 如果无法加载接口集合,使用默认接口 // 如果无法加载接口集合,使用默认接口
this.endpoints = ['https://60s.api.shumengya.top/v2/epic']; this.endpoints = ['https://60s.api.shumengya.top/v2/epic'];
} }
}, },
// 获取当前接口URL // 获取当前接口URL
getCurrentUrl(encoding) { getCurrentUrl(encoding) {
if (this.endpoints.length === 0) return null; if (this.endpoints.length === 0) return null;
const url = new URL(this.endpoints[this.currentIndex]); const url = new URL(this.endpoints[this.currentIndex]);
if (encoding) url.searchParams.set('encoding', encoding); if (encoding) url.searchParams.set('encoding', encoding);
return url.toString(); return url.toString();
}, },
// 切换到下一个接口 // 切换到下一个接口
switchToNext() { switchToNext() {
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length; this.currentIndex = (this.currentIndex + 1) % this.endpoints.length;
return this.currentIndex < this.endpoints.length; return this.currentIndex < this.endpoints.length;
}, },
// 重置到第一个接口 // 重置到第一个接口
reset() { reset() {
this.currentIndex = 0; this.currentIndex = 0;
} }
}; };
// DOM 元素引用 // DOM 元素引用
const els = { const els = {
loading: null, loading: null,
error: null, error: null,
container: null, container: null,
gamesGrid: null, gamesGrid: null,
totalGames: null, totalGames: null,
freeNow: null, freeNow: null,
upcoming: null, upcoming: null,
refreshBtn: null, refreshBtn: null,
}; };
function initDom() { function initDom() {
els.loading = document.getElementById('loading'); els.loading = document.getElementById('loading');
els.error = document.getElementById('error'); els.error = document.getElementById('error');
els.container = document.getElementById('content'); els.container = document.getElementById('content');
els.gamesGrid = document.getElementById('games-grid'); els.gamesGrid = document.getElementById('games-grid');
els.totalGames = document.getElementById('total-games'); els.totalGames = document.getElementById('total-games');
els.freeNow = document.getElementById('free-now'); els.freeNow = document.getElementById('free-now');
els.upcoming = document.getElementById('upcoming'); els.upcoming = document.getElementById('upcoming');
els.refreshBtn = document.getElementById('refresh-btn'); els.refreshBtn = document.getElementById('refresh-btn');
} }
function showLoading() { function showLoading() {
els.loading.style.display = 'block'; els.loading.style.display = 'block';
els.error.style.display = 'none'; els.error.style.display = 'none';
els.container.style.display = 'none'; els.container.style.display = 'none';
} }
function showError(msg) { function showError(msg) {
els.loading.style.display = 'none'; els.loading.style.display = 'none';
els.error.style.display = 'block'; els.error.style.display = 'block';
els.container.style.display = 'none'; els.container.style.display = 'none';
els.error.querySelector('p').textContent = msg || '获取数据失败,请稍后重试'; els.error.querySelector('p').textContent = msg || '获取数据失败,请稍后重试';
} }
function showContent() { function showContent() {
els.loading.style.display = 'none'; els.loading.style.display = 'none';
els.error.style.display = 'none'; els.error.style.display = 'none';
els.container.style.display = 'block'; els.container.style.display = 'block';
} }
function safeText(text) { function safeText(text) {
const div = document.createElement('div'); const div = document.createElement('div');
div.textContent = text == null ? '' : String(text); div.textContent = text == null ? '' : String(text);
return div.innerHTML; return div.innerHTML;
} }
function formatDate(dateStr) { function formatDate(dateStr) {
if (!dateStr) return ''; if (!dateStr) return '';
try { try {
const date = new Date(dateStr); const date = new Date(dateStr);
return date.toLocaleDateString('zh-CN', { return date.toLocaleDateString('zh-CN', {
month: '2-digit', month: '2-digit',
day: '2-digit', day: '2-digit',
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit'
}); });
} catch (e) { } catch (e) {
return dateStr; return dateStr;
} }
} }
async function fetchData(preferLocal = false) { async function fetchData(preferLocal = false) {
if (preferLocal) { if (preferLocal) {
try { try {
const res = await fetch('./返回接口.json', { cache: 'no-store' }); const res = await fetch('./返回接口.json', { cache: 'no-store' });
const json = await res.json(); const json = await res.json();
return json; return json;
} catch (e) { } catch (e) {
throw new Error('本地数据加载失败'); throw new Error('本地数据加载失败');
} }
} }
// 重置API索引到第一个接口 // 重置API索引到第一个接口
API.reset(); API.reset();
// 尝试所有API接口 // 尝试所有API接口
for (let i = 0; i < API.endpoints.length; i++) { for (let i = 0; i < API.endpoints.length; i++) {
try { try {
const url = API.getCurrentUrl(); const url = API.getCurrentUrl();
console.log(`尝试接口 ${i + 1}/${API.endpoints.length}: ${url}`); console.log(`尝试接口 ${i + 1}/${API.endpoints.length}: ${url}`);
const res = await fetch(url, { const res = await fetch(url, {
cache: 'no-store', cache: 'no-store',
timeout: 10000 // 10秒超时 timeout: 10000 // 10秒超时
}); });
if (!res.ok) { if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`); throw new Error(`HTTP ${res.status}: ${res.statusText}`);
} }
const json = await res.json(); const json = await res.json();
if (json && json.code === 200) { if (json && json.code === 200) {
console.log(`接口 ${i + 1} 请求成功`); console.log(`接口 ${i + 1} 请求成功`);
return json; return json;
} }
throw new Error(json && json.message ? json.message : '接口返回异常'); throw new Error(json && json.message ? json.message : '接口返回异常');
} catch (e) { } catch (e) {
console.warn(`接口 ${i + 1} 失败:`, e.message); console.warn(`接口 ${i + 1} 失败:`, e.message);
// 如果不是最后一个接口,切换到下一个 // 如果不是最后一个接口,切换到下一个
if (i < API.endpoints.length - 1) { if (i < API.endpoints.length - 1) {
API.switchToNext(); API.switchToNext();
continue; continue;
} }
// 所有接口都失败了,尝试本地数据 // 所有接口都失败了,尝试本地数据
console.warn('所有远程接口都失败,尝试本地数据'); console.warn('所有远程接口都失败,尝试本地数据');
try { try {
const res = await fetch('./返回接口.json', { cache: 'no-store' }); const res = await fetch('./返回接口.json', { cache: 'no-store' });
const json = await res.json(); const json = await res.json();
return json; return json;
} catch (e2) { } catch (e2) {
throw new Error('所有接口和本地数据都无法访问'); throw new Error('所有接口和本地数据都无法访问');
} }
} }
} }
} }
function createGameCard(game) { function createGameCard(game) {
const isFree = game.is_free_now; const isFree = game.is_free_now;
const statusClass = isFree ? 'status-free' : 'status-upcoming'; const statusClass = isFree ? 'status-free' : 'status-upcoming';
const statusText = isFree ? '限时免费' : '即将免费'; const statusText = isFree ? '限时免费' : '即将免费';
return ` return `
<div class="game-card fade-in"> <div class="game-card fade-in">
<div class="status-badge ${statusClass}">${statusText}</div> <div class="status-badge ${statusClass}">${statusText}</div>
<img class="game-cover" src="${safeText(game.cover)}" alt="${safeText(game.title)} 封面" loading="lazy" /> <img class="game-cover" src="${safeText(game.cover)}" alt="${safeText(game.title)} 封面" loading="lazy" />
<div class="game-info"> <div class="game-info">
<h3 class="game-title">${safeText(game.title)}</h3> <h3 class="game-title">${safeText(game.title)}</h3>
<p class="game-description">${safeText(game.description)}</p> <p class="game-description">${safeText(game.description)}</p>
<div class="game-meta"> <div class="game-meta">
<div class="game-price"> <div class="game-price">
<span class="original-price">${safeText(game.original_price_desc)}</span> <span class="original-price">${safeText(game.original_price_desc)}</span>
<span class="free-price">免费</span> <span class="free-price">免费</span>
</div> </div>
<div class="game-seller">${safeText(game.seller)}</div> <div class="game-seller">${safeText(game.seller)}</div>
</div> </div>
<div class="game-dates"> <div class="game-dates">
<div class="free-period"> <div class="free-period">
<span>开始:${formatDate(game.free_start)}</span> <span>开始:${formatDate(game.free_start)}</span>
<span>结束:${formatDate(game.free_end)}</span> <span>结束:${formatDate(game.free_end)}</span>
</div> </div>
</div> </div>
<div class="game-actions"> <div class="game-actions">
<a href="${safeText(game.link)}" target="_blank" class="btn"> <a href="${safeText(game.link)}" target="_blank" class="btn">
${isFree ? '立即领取' : '查看详情'} ${isFree ? '立即领取' : '查看详情'}
</a> </a>
</div> </div>
</div> </div>
</div> </div>
`; `;
} }
function updateStats(games) { function updateStats(games) {
const total = games.length; const total = games.length;
const freeNow = games.filter(game => game.is_free_now).length; const freeNow = games.filter(game => game.is_free_now).length;
const upcoming = total - freeNow; const upcoming = total - freeNow;
els.totalGames.textContent = total; els.totalGames.textContent = total;
els.freeNow.textContent = freeNow; els.freeNow.textContent = freeNow;
els.upcoming.textContent = upcoming; els.upcoming.textContent = upcoming;
} }
function renderGames(games) { function renderGames(games) {
if (!Array.isArray(games) || games.length === 0) { if (!Array.isArray(games) || games.length === 0) {
els.gamesGrid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: #5a7c65;">暂无游戏数据</div>'; els.gamesGrid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: #5a7c65;">暂无游戏数据</div>';
return; return;
} }
// 按状态排序:免费的在前 // 按状态排序:免费的在前
const sortedGames = [...games].sort((a, b) => { 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;
if (!a.is_free_now && b.is_free_now) return 1; if (!a.is_free_now && b.is_free_now) return 1;
return 0; return 0;
}); });
const html = sortedGames.map(game => createGameCard(game)).join(''); const html = sortedGames.map(game => createGameCard(game)).join('');
els.gamesGrid.innerHTML = html; els.gamesGrid.innerHTML = html;
updateStats(games); updateStats(games);
} }
function render(data) { function render(data) {
const games = data?.data || []; const games = data?.data || [];
renderGames(games); renderGames(games);
showContent(); showContent();
} }
async function load() { async function load() {
showLoading(); showLoading();
// 初始化API接口列表 // 初始化API接口列表
await API.init(); await API.init();
try { try {
const data = await fetchData(false); const data = await fetchData(false);
render(data); render(data);
} catch (e) { } catch (e) {
console.error('数据获取失败:', e); console.error('数据获取失败:', e);
showError(e.message || '获取数据失败,请稍后重试'); showError(e.message || '获取数据失败,请稍后重试');
} }
} }
function bindEvents() { function bindEvents() {
if (els.refreshBtn) { if (els.refreshBtn) {
els.refreshBtn.addEventListener('click', load); els.refreshBtn.addEventListener('click', load);
} }
// 快捷键 Ctrl+R 刷新(不拦截浏览器默认刷新) // 快捷键 Ctrl+R 刷新(不拦截浏览器默认刷新)
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'r' && !e.defaultPrevented) { if (e.ctrlKey && e.key === 'r' && !e.defaultPrevented) {
// 不阻止默认行为,让浏览器正常刷新 // 不阻止默认行为,让浏览器正常刷新
} }
}); });
} }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initDom(); initDom();
bindEvents(); bindEvents();
load(); load();
}); });
})(); })();

View File

@@ -1,3 +1,3 @@
[ [
"https://60s.api.shumengya.top" "https://60s.api.shumengya.top"
] ]

View File

@@ -1,66 +1,66 @@
{ {
"code": 200, "code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841", "message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": [ "data": [
{ {
"id": "9aa227e2ba294bb1a95c95fde892eb31", "id": "9aa227e2ba294bb1a95c95fde892eb31",
"title": "《Totally Reliable Delivery Service》 Standard Edition", "title": "《Totally Reliable Delivery Service》 Standard Edition",
"cover": "https://cdn1.epicgames.com/52b90f9a982a404781b189f6a7903226/offer/EGS_TotallyReliableDeliveryService_WereFiveGames_S1-2560x1440-47e6e9562d62705a75ea7b7096d0b8dc.jpg", "cover": "https://cdn1.epicgames.com/52b90f9a982a404781b189f6a7903226/offer/EGS_TotallyReliableDeliveryService_WereFiveGames_S1-2560x1440-47e6e9562d62705a75ea7b7096d0b8dc.jpg",
"original_price": 52, "original_price": 52,
"original_price_desc": "¥52.00", "original_price_desc": "¥52.00",
"description": "穿好护腰护具发动货车送货的时间到啦在一个高度互动的沙盒世界中与最多三位好友一起随意地完成送货。货物已试投这就是我们靠谱快递Totally Reliable Delivery Service的品质保证", "description": "穿好护腰护具发动货车送货的时间到啦在一个高度互动的沙盒世界中与最多三位好友一起随意地完成送货。货物已试投这就是我们靠谱快递Totally Reliable Delivery Service的品质保证",
"seller": "Infogrames LLC", "seller": "Infogrames LLC",
"is_free_now": true, "is_free_now": true,
"free_start": "2025/08/14 23:00:00", "free_start": "2025/08/14 23:00:00",
"free_start_at": 1755183600000, "free_start_at": 1755183600000,
"free_end": "2025/08/21 23:00:00", "free_end": "2025/08/21 23:00:00",
"free_end_at": 1755788400000, "free_end_at": 1755788400000,
"link": "https://store.epicgames.com/store/zh-CN/p/totally-reliable-delivery-service/home" "link": "https://store.epicgames.com/store/zh-CN/p/totally-reliable-delivery-service/home"
}, },
{ {
"id": "8ea3500dc38e4f429702bf889c172d3d", "id": "8ea3500dc38e4f429702bf889c172d3d",
"title": "Hidden Folks", "title": "Hidden Folks",
"cover": "https://cdn1.epicgames.com/spt-assets/7bfd56b0586348dcb139945d9e59f988/hidden-folks-1b7hh.png", "cover": "https://cdn1.epicgames.com/spt-assets/7bfd56b0586348dcb139945d9e59f988/hidden-folks-1b7hh.png",
"original_price": 47, "original_price": 47,
"original_price_desc": "¥47.00", "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!!!!!", "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", "seller": "Adriaan de Jongh",
"is_free_now": true, "is_free_now": true,
"free_start": "2025/08/14 23:00:00", "free_start": "2025/08/14 23:00:00",
"free_start_at": 1755183600000, "free_start_at": 1755183600000,
"free_end": "2025/08/21 23:00:00", "free_end": "2025/08/21 23:00:00",
"free_end_at": 1755788400000, "free_end_at": 1755788400000,
"link": "https://store.epicgames.com/store/zh-CN/p/hidden-folks-239d16" "link": "https://store.epicgames.com/store/zh-CN/p/hidden-folks-239d16"
}, },
{ {
"id": "4cbb6c3704d240f19c3dd5f5cb2b0cb4", "id": "4cbb6c3704d240f19c3dd5f5cb2b0cb4",
"title": "Kamaeru", "title": "Kamaeru",
"cover": "https://cdn1.epicgames.com/spt-assets/44313cfbb62b4df5801d0c8d541c2624/kamaeru-40asc.png", "cover": "https://cdn1.epicgames.com/spt-assets/44313cfbb62b4df5801d0c8d541c2624/kamaeru-40asc.png",
"original_price": 62, "original_price": 62,
"original_price_desc": "¥62.00", "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!", "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", "seller": "Armor Games Studios",
"is_free_now": false, "is_free_now": false,
"free_start": "2025/08/21 23:00:00", "free_start": "2025/08/21 23:00:00",
"free_start_at": 1755788400000, "free_start_at": 1755788400000,
"free_end": "2025/08/28 23:00:00", "free_end": "2025/08/28 23:00:00",
"free_end_at": 1756393200000, "free_end_at": 1756393200000,
"link": "https://store.epicgames.com/store/zh-CN/p/kamaeru-0c301e" "link": "https://store.epicgames.com/store/zh-CN/p/kamaeru-0c301e"
}, },
{ {
"id": "0d9a533f0e684cc18620a8f408e8e72c", "id": "0d9a533f0e684cc18620a8f408e8e72c",
"title": "Strange Horticulture", "title": "Strange Horticulture",
"cover": "https://cdn1.epicgames.com/spt-assets/15e8e3eba65a4763a815d6eae1d763b2/strange-horticulture-offer-2wghv.png", "cover": "https://cdn1.epicgames.com/spt-assets/15e8e3eba65a4763a815d6eae1d763b2/strange-horticulture-offer-2wghv.png",
"original_price": 45, "original_price": 45,
"original_price_desc": "¥45.00", "original_price_desc": "¥45.00",
"description": "款神秘学解谜游戏,你将扮演当地植物商店的店主,寻找并识别新的植物,悠闲撸猫,与女巫团体交谈,或加入异教。收集各种强大的植物,用它们来影响故事走向,揭开昂德米尔镇的黑暗谜团。", "description": "款神秘学解谜游戏,你将扮演当地植物商店的店主,寻找并识别新的植物,悠闲撸猫,与女巫团体交谈,或加入异教。收集各种强大的植物,用它们来影响故事走向,揭开昂德米尔镇的黑暗谜团。",
"seller": "Iceberg Interactive", "seller": "Iceberg Interactive",
"is_free_now": false, "is_free_now": false,
"free_start": "2025/08/21 23:00:00", "free_start": "2025/08/21 23:00:00",
"free_start_at": 1755788400000, "free_start_at": 1755788400000,
"free_end": "2025/08/28 23:00:00", "free_end": "2025/08/28 23:00:00",
"free_end_at": 1756393200000, "free_end_at": 1756393200000,
"link": "https://store.epicgames.com/store/zh-CN/p/strange-horticulture-360e80" "link": "https://store.epicgames.com/store/zh-CN/p/strange-horticulture-360e80"
} }
] ]
} }

View File

@@ -1,233 +1,233 @@
/* 动态背景样式 */ /* 动态背景样式 */
body::before { body::before {
content: ''; content: '';
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(135deg, #f0f8e8 0%, #e8f5e8 50%, #d4f4dd 100%); background: linear-gradient(135deg, #f0f8e8 0%, #e8f5e8 50%, #d4f4dd 100%);
z-index: -2; z-index: -2;
} }
body::after { body::after {
content: ''; content: '';
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: background:
radial-gradient(circle at 20% 80%, rgba(144, 238, 144, 0.2) 0%, transparent 50%), radial-gradient(circle at 20% 80%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(173, 255, 173, 0.2) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(152, 251, 152, 0.2) 0%, transparent 50%); radial-gradient(circle at 40% 40%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
z-index: -1; z-index: -1;
animation: backgroundMove 20s ease-in-out infinite; animation: backgroundMove 20s ease-in-out infinite;
} }
@keyframes backgroundMove { @keyframes backgroundMove {
0%, 100% { 0%, 100% {
background: background:
radial-gradient(circle at 20% 80%, rgba(144, 238, 144, 0.2) 0%, transparent 50%), radial-gradient(circle at 20% 80%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(173, 255, 173, 0.2) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(152, 251, 152, 0.2) 0%, transparent 50%); radial-gradient(circle at 40% 40%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
} }
25% { 25% {
background: background:
radial-gradient(circle at 60% 30%, rgba(144, 238, 144, 0.2) 0%, transparent 50%), radial-gradient(circle at 60% 30%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
radial-gradient(circle at 30% 70%, rgba(173, 255, 173, 0.2) 0%, transparent 50%), radial-gradient(circle at 30% 70%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(152, 251, 152, 0.2) 0%, transparent 50%); radial-gradient(circle at 80% 80%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
} }
50% { 50% {
background: background:
radial-gradient(circle at 80% 60%, rgba(144, 238, 144, 0.2) 0%, transparent 50%), radial-gradient(circle at 80% 60%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
radial-gradient(circle at 20% 30%, rgba(173, 255, 173, 0.2) 0%, transparent 50%), radial-gradient(circle at 20% 30%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
radial-gradient(circle at 60% 70%, rgba(152, 251, 152, 0.2) 0%, transparent 50%); radial-gradient(circle at 60% 70%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
} }
75% { 75% {
background: background:
radial-gradient(circle at 40% 90%, rgba(144, 238, 144, 0.2) 0%, transparent 50%), radial-gradient(circle at 40% 90%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
radial-gradient(circle at 70% 10%, rgba(173, 255, 173, 0.2) 0%, transparent 50%), radial-gradient(circle at 70% 10%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
radial-gradient(circle at 20% 60%, rgba(152, 251, 152, 0.2) 0%, transparent 50%); radial-gradient(circle at 20% 60%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
} }
} }
/* 浮动粒子效果 */ /* 浮动粒子效果 */
.particles { .particles {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
z-index: -1; z-index: -1;
} }
.particle { .particle {
position: absolute; position: absolute;
width: 4px; width: 4px;
height: 4px; height: 4px;
background: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.5);
border-radius: 50%; border-radius: 50%;
animation: float 15s infinite linear; animation: float 15s infinite linear;
} }
.particle:nth-child(1) { .particle:nth-child(1) {
left: 10%; left: 10%;
animation-delay: 0s; animation-delay: 0s;
animation-duration: 12s; animation-duration: 12s;
} }
.particle:nth-child(2) { .particle:nth-child(2) {
left: 20%; left: 20%;
animation-delay: 2s; animation-delay: 2s;
animation-duration: 18s; animation-duration: 18s;
} }
.particle:nth-child(3) { .particle:nth-child(3) {
left: 30%; left: 30%;
animation-delay: 4s; animation-delay: 4s;
animation-duration: 15s; animation-duration: 15s;
} }
.particle:nth-child(4) { .particle:nth-child(4) {
left: 40%; left: 40%;
animation-delay: 6s; animation-delay: 6s;
animation-duration: 20s; animation-duration: 20s;
} }
.particle:nth-child(5) { .particle:nth-child(5) {
left: 50%; left: 50%;
animation-delay: 8s; animation-delay: 8s;
animation-duration: 14s; animation-duration: 14s;
} }
.particle:nth-child(6) { .particle:nth-child(6) {
left: 60%; left: 60%;
animation-delay: 10s; animation-delay: 10s;
animation-duration: 16s; animation-duration: 16s;
} }
.particle:nth-child(7) { .particle:nth-child(7) {
left: 70%; left: 70%;
animation-delay: 12s; animation-delay: 12s;
animation-duration: 22s; animation-duration: 22s;
} }
.particle:nth-child(8) { .particle:nth-child(8) {
left: 80%; left: 80%;
animation-delay: 14s; animation-delay: 14s;
animation-duration: 13s; animation-duration: 13s;
} }
.particle:nth-child(9) { .particle:nth-child(9) {
left: 90%; left: 90%;
animation-delay: 16s; animation-delay: 16s;
animation-duration: 19s; animation-duration: 19s;
} }
.particle:nth-child(10) { .particle:nth-child(10) {
left: 15%; left: 15%;
animation-delay: 18s; animation-delay: 18s;
animation-duration: 17s; animation-duration: 17s;
} }
@keyframes float { @keyframes float {
0% { 0% {
transform: translateY(100vh) rotate(0deg); transform: translateY(100vh) rotate(0deg);
opacity: 0; opacity: 0;
} }
10% { 10% {
opacity: 1; opacity: 1;
} }
90% { 90% {
opacity: 1; opacity: 1;
} }
100% { 100% {
transform: translateY(-100px) rotate(360deg); transform: translateY(-100px) rotate(360deg);
opacity: 0; opacity: 0;
} }
} }
/* 网格背景效果 */ /* 网格背景效果 */
.grid-background { .grid-background {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-image: background-image:
linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px), linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px); linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
background-size: 50px 50px; background-size: 50px 50px;
z-index: -1; z-index: -1;
opacity: 0.3; opacity: 0.3;
animation: gridMove 30s linear infinite; animation: gridMove 30s linear infinite;
} }
@keyframes gridMove { @keyframes gridMove {
0% { 0% {
transform: translate(0, 0); transform: translate(0, 0);
} }
100% { 100% {
transform: translate(50px, 50px); transform: translate(50px, 50px);
} }
} }
/* 光晕效果 */ /* 光晕效果 */
.glow-effect { .glow-effect {
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
width: 300px; width: 300px;
height: 300px; height: 300px;
background: radial-gradient(circle, rgba(74, 144, 226, 0.2) 0%, transparent 70%); background: radial-gradient(circle, rgba(74, 144, 226, 0.2) 0%, transparent 70%);
border-radius: 50%; border-radius: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: -1; z-index: -1;
animation: pulse 4s ease-in-out infinite; animation: pulse 4s ease-in-out infinite;
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%, 100% {
transform: translate(-50%, -50%) scale(1); transform: translate(-50%, -50%) scale(1);
opacity: 0.5; opacity: 0.5;
} }
50% { 50% {
transform: translate(-50%, -50%) scale(1.2); transform: translate(-50%, -50%) scale(1.2);
opacity: 0.8; opacity: 0.8;
} }
} }
/* 响应式背景调整 */ /* 响应式背景调整 */
@media (max-width: 768px) { @media (max-width: 768px) {
.grid-background { .grid-background {
background-size: 30px 30px; background-size: 30px 30px;
} }
.glow-effect { .glow-effect {
width: 200px; width: 200px;
height: 200px; height: 200px;
} }
.particle { .particle {
width: 3px; width: 3px;
height: 3px; height: 3px;
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.grid-background { .grid-background {
background-size: 20px 20px; background-size: 20px 20px;
} }
.glow-effect { .glow-effect {
width: 150px; width: 150px;
height: 150px; height: 150px;
} }
.particle { .particle {
width: 2px; width: 2px;
height: 2px; height: 2px;
} }
} }

View File

@@ -1,461 +1,461 @@
/* 全局样式重置 */ /* 全局样式重置 */
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6; line-height: 1.6;
color: #333; color: #333;
min-height: 100vh; min-height: 100vh;
overflow-x: hidden; overflow-x: hidden;
/* 隐藏滚动条但保留滚动功能 */ /* 隐藏滚动条但保留滚动功能 */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none; /* IE and Edge */
} }
/* 隐藏 Webkit 浏览器的滚动条 */ /* 隐藏 Webkit 浏览器的滚动条 */
body::-webkit-scrollbar, body::-webkit-scrollbar,
html::-webkit-scrollbar, html::-webkit-scrollbar,
*::-webkit-scrollbar { *::-webkit-scrollbar {
display: none; display: none;
} }
/* 全局隐藏滚动条但保留滚动功能 */ /* 全局隐藏滚动条但保留滚动功能 */
html { html {
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none; /* IE and Edge */
} }
/* 容器样式 */ /* 容器样式 */
.container { .container {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
/* 头部样式 */ /* 头部样式 */
.header { .header {
text-align: center; text-align: center;
padding: 3rem 2rem 2rem; padding: 3rem 2rem 2rem;
background: linear-gradient(135deg, rgba(144, 238, 144, 0.15), rgba(152, 251, 152, 0.15)); background: linear-gradient(135deg, rgba(144, 238, 144, 0.15), rgba(152, 251, 152, 0.15));
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2); border-bottom: 1px solid rgba(255, 255, 255, 0.2);
} }
.header h1 { .header h1 {
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 700; font-weight: 700;
background: linear-gradient(135deg, #228B22, #32CD32); background: linear-gradient(135deg, #228B22, #32CD32);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.header h1 i { .header h1 i {
margin-right: 0.5rem; margin-right: 0.5rem;
background: linear-gradient(135deg, #228B22, #32CD32); background: linear-gradient(135deg, #228B22, #32CD32);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
} }
.subtitle { .subtitle {
font-size: 1.1rem; font-size: 1.1rem;
color: #666; color: #666;
font-weight: 300; font-weight: 300;
} }
/* 主要内容区域 */ /* 主要内容区域 */
.main-content { .main-content {
flex: 1; flex: 1;
padding: 2rem; padding: 2rem;
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
} }
/* 查询按钮区域 */ /* 查询按钮区域 */
.query-section { .query-section {
text-align: center; text-align: center;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.query-btn { .query-btn {
background: linear-gradient(135deg, #228B22, #32CD32); background: linear-gradient(135deg, #228B22, #32CD32);
color: white; color: white;
border: none; border: none;
padding: 1rem 2rem; padding: 1rem 2rem;
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 600; font-weight: 600;
border-radius: 50px; border-radius: 50px;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(34, 139, 34, 0.3); box-shadow: 0 4px 15px rgba(34, 139, 34, 0.3);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
min-width: 200px; min-width: 200px;
justify-content: center; justify-content: center;
} }
.query-btn:hover { .query-btn:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(34, 139, 34, 0.4); box-shadow: 0 6px 20px rgba(34, 139, 34, 0.4);
background: linear-gradient(135deg, #1e7e1e, #2eb82e); background: linear-gradient(135deg, #1e7e1e, #2eb82e);
} }
.query-btn:active { .query-btn:active {
transform: translateY(0); transform: translateY(0);
} }
.query-btn:disabled { .query-btn:disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
transform: none; transform: none;
} }
/* 加载动画 */ /* 加载动画 */
.loading { .loading {
text-align: center; text-align: center;
padding: 2rem; padding: 2rem;
} }
.spinner { .spinner {
width: 50px; width: 50px;
height: 50px; height: 50px;
border: 4px solid #f3f3f3; border: 4px solid #f3f3f3;
border-top: 4px solid #4a90e2; border-top: 4px solid #4a90e2;
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin: 0 auto 1rem; margin: 0 auto 1rem;
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
.loading p { .loading p {
color: #666; color: #666;
font-size: 1rem; font-size: 1rem;
} }
/* IP信息卡片 */ /* IP信息卡片 */
.ip-info { .ip-info {
animation: fadeInUp 0.6s ease; animation: fadeInUp 0.6s ease;
} }
@keyframes fadeInUp { @keyframes fadeInUp {
from { from {
opacity: 0; opacity: 0;
transform: translateY(30px); transform: translateY(30px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
} }
.ip-card { .ip-card {
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-radius: 20px; border-radius: 20px;
padding: 2rem; padding: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
} }
.ip-header { .ip-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
padding-bottom: 1rem; padding-bottom: 1rem;
border-bottom: 2px solid #f0f0f0; border-bottom: 2px solid #f0f0f0;
} }
.ip-header i { .ip-header i {
font-size: 1.5rem; font-size: 1.5rem;
color: #4a90e2; color: #4a90e2;
} }
.ip-header h2 { .ip-header h2 {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
} }
.ip-display { .ip-display {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 1rem; gap: 1rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
padding: 1.5rem; padding: 1.5rem;
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1), rgba(80, 200, 120, 0.1)); background: linear-gradient(135deg, rgba(74, 144, 226, 0.1), rgba(80, 200, 120, 0.1));
border-radius: 15px; border-radius: 15px;
border: 2px solid rgba(74, 144, 226, 0.2); border: 2px solid rgba(74, 144, 226, 0.2);
} }
.ip-address { .ip-address {
font-size: 2rem; font-size: 2rem;
font-weight: 700; font-weight: 700;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
color: #2c3e50; color: #2c3e50;
background: linear-gradient(135deg, #4a90e2, #50c878); background: linear-gradient(135deg, #4a90e2, #50c878);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.copy-btn { .copy-btn {
background: #4a90e2; background: #4a90e2;
color: white; color: white;
border: none; border: none;
padding: 0.5rem; padding: 0.5rem;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
font-size: 1rem; font-size: 1rem;
width: 40px; width: 40px;
height: 40px; height: 40px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.copy-btn:hover { .copy-btn:hover {
background: #3a7bc8; background: #3a7bc8;
transform: scale(1.1); transform: scale(1.1);
} }
.ip-details { .ip-details {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
} }
.detail-item { .detail-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
padding: 0.75rem; padding: 0.75rem;
background: rgba(248, 249, 250, 0.8); background: rgba(248, 249, 250, 0.8);
border-radius: 10px; border-radius: 10px;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.detail-item:hover { .detail-item:hover {
background: rgba(74, 144, 226, 0.1); background: rgba(74, 144, 226, 0.1);
transform: translateX(5px); transform: translateX(5px);
} }
.detail-item i { .detail-item i {
color: #4a90e2; color: #4a90e2;
width: 20px; width: 20px;
text-align: center; text-align: center;
} }
.detail-item .label { .detail-item .label {
font-weight: 600; font-weight: 600;
color: #555; color: #555;
min-width: 80px; min-width: 80px;
} }
.detail-item .value { .detail-item .value {
color: #333; color: #333;
font-weight: 500; font-weight: 500;
} }
/* IP地址说明区域 */ /* IP地址说明区域 */
.ip-explanation { .ip-explanation {
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-radius: 20px; border-radius: 20px;
padding: 2rem; padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
} }
.ip-explanation h3 { .ip-explanation h3 {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 1.3rem; font-size: 1.3rem;
color: #333; color: #333;
} }
.ip-explanation h3 i { .ip-explanation h3 i {
color: #4a90e2; color: #4a90e2;
} }
.ip-explanation p { .ip-explanation p {
color: #666; color: #666;
line-height: 1.8; line-height: 1.8;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.features { .features {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem; gap: 1rem;
} }
.feature-item { .feature-item {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 1rem; gap: 1rem;
padding: 1rem; padding: 1rem;
background: rgba(248, 249, 250, 0.8); background: rgba(248, 249, 250, 0.8);
border-radius: 12px; border-radius: 12px;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.feature-item:hover { .feature-item:hover {
background: rgba(74, 144, 226, 0.1); background: rgba(74, 144, 226, 0.1);
transform: translateY(-2px); transform: translateY(-2px);
} }
.feature-item i { .feature-item i {
color: #4a90e2; color: #4a90e2;
font-size: 1.5rem; font-size: 1.5rem;
margin-top: 0.2rem; margin-top: 0.2rem;
} }
.feature-item h4 { .feature-item h4 {
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
margin-bottom: 0.3rem; margin-bottom: 0.3rem;
} }
.feature-item p { .feature-item p {
font-size: 0.9rem; font-size: 0.9rem;
color: #666; color: #666;
line-height: 1.5; line-height: 1.5;
margin: 0; margin: 0;
} }
/* 错误信息 */ /* 错误信息 */
.error-message { .error-message {
text-align: center; text-align: center;
padding: 2rem; padding: 2rem;
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-radius: 20px; border-radius: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 99, 99, 0.3); border: 1px solid rgba(255, 99, 99, 0.3);
animation: fadeInUp 0.6s ease; animation: fadeInUp 0.6s ease;
} }
.error-message i { .error-message i {
font-size: 3rem; font-size: 3rem;
color: #ff6b6b; color: #ff6b6b;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.error-message p { .error-message p {
color: #666; color: #666;
font-size: 1.1rem; font-size: 1.1rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.retry-btn { .retry-btn {
background: #ff6b6b; background: #ff6b6b;
color: white; color: white;
border: none; border: none;
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
border-radius: 25px; border-radius: 25px;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.retry-btn:hover { .retry-btn:hover {
background: #ff5252; background: #ff5252;
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3); box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
} }
/* 页脚 */ /* 页脚 */
.footer { .footer {
text-align: center; text-align: center;
padding: 2rem; padding: 2rem;
background: rgba(248, 249, 250, 0.8); background: rgba(248, 249, 250, 0.8);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-top: 1px solid rgba(255, 255, 255, 0.2); border-top: 1px solid rgba(255, 255, 255, 0.2);
color: #666; color: #666;
font-size: 0.9rem; font-size: 0.9rem;
} }
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 768px) { @media (max-width: 768px) {
.header { .header {
padding: 2rem 1rem 1.5rem; padding: 2rem 1rem 1.5rem;
} }
.header h1 { .header h1 {
font-size: 2rem; font-size: 2rem;
} }
.main-content { .main-content {
padding: 1rem; padding: 1rem;
} }
.ip-card, .ip-explanation { .ip-card, .ip-explanation {
padding: 1.5rem; padding: 1.5rem;
} }
.ip-address { .ip-address {
font-size: 1.5rem; font-size: 1.5rem;
} }
.ip-display { .ip-display {
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
} }
.features { .features {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.detail-item { .detail-item {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 0.3rem; gap: 0.3rem;
} }
.detail-item .label { .detail-item .label {
min-width: auto; min-width: auto;
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.header h1 { .header h1 {
font-size: 1.8rem; font-size: 1.8rem;
} }
.query-btn { .query-btn {
padding: 0.875rem 1.5rem; padding: 0.875rem 1.5rem;
font-size: 1rem; font-size: 1rem;
min-width: 180px; min-width: 180px;
} }
.ip-address { .ip-address {
font-size: 1.3rem; font-size: 1.3rem;
} }
.ip-card, .ip-explanation { .ip-card, .ip-explanation {
padding: 1rem; padding: 1rem;
} }
} }

View File

@@ -1,131 +1,131 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>公网IP地址查询</title> <title>公网IP地址查询</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/background.css"> <link rel="stylesheet" href="css/background.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<!-- 头部 --> <!-- 头部 -->
<header class="header"> <header class="header">
<h1><i class="fas fa-globe"></i> 公网IP地址查询</h1> <h1><i class="fas fa-globe"></i> 公网IP地址查询</h1>
<p class="subtitle">快速获取您的公网IP地址信息</p> <p class="subtitle">快速获取您的公网IP地址信息</p>
</header> </header>
<!-- 主要内容区域 --> <!-- 主要内容区域 -->
<main class="main-content"> <main class="main-content">
<!-- 查询按钮区域 --> <!-- 查询按钮区域 -->
<div class="query-section"> <div class="query-section">
<button id="queryBtn" class="query-btn"> <button id="queryBtn" class="query-btn">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
<span>查询我的IP地址</span> <span>查询我的IP地址</span>
</button> </button>
</div> </div>
<!-- 加载动画 --> <!-- 加载动画 -->
<div id="loading" class="loading" style="display: none;"> <div id="loading" class="loading" style="display: none;">
<div class="spinner"></div> <div class="spinner"></div>
<p>正在获取IP地址信息...</p> <p>正在获取IP地址信息...</p>
</div> </div>
<!-- IP信息展示区域 --> <!-- IP信息展示区域 -->
<div id="ip-info" class="ip-info" style="display: none;"> <div id="ip-info" class="ip-info" style="display: none;">
<div class="ip-card"> <div class="ip-card">
<div class="ip-header"> <div class="ip-header">
<i class="fas fa-network-wired"></i> <i class="fas fa-network-wired"></i>
<h2>您的公网IP地址</h2> <h2>您的公网IP地址</h2>
</div> </div>
<div class="ip-display"> <div class="ip-display">
<span id="ip-address" class="ip-address">---.---.---.---</span> <span id="ip-address" class="ip-address">---.---.---.---</span>
<button id="copyBtn" class="copy-btn" title="复制IP地址"> <button id="copyBtn" class="copy-btn" title="复制IP地址">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
</div> </div>
<div class="ip-details"> <div class="ip-details">
<div class="detail-item"> <div class="detail-item">
<i class="fas fa-clock"></i> <i class="fas fa-clock"></i>
<span class="label">查询时间:</span> <span class="label">查询时间:</span>
<span id="query-time" class="value">--</span> <span id="query-time" class="value">--</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<i class="fas fa-map-marker-alt"></i> <i class="fas fa-map-marker-alt"></i>
<span class="label">位置信息:</span> <span class="label">位置信息:</span>
<span id="location" class="value">--</span> <span id="location" class="value">--</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<i class="fas fa-building"></i> <i class="fas fa-building"></i>
<span class="label">网络服务商:</span> <span class="label">网络服务商:</span>
<span id="isp" class="value">--</span> <span id="isp" class="value">--</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<i class="fas fa-flag"></i> <i class="fas fa-flag"></i>
<span class="label">国家:</span> <span class="label">国家:</span>
<span id="country" class="value">--</span> <span id="country" class="value">--</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<i class="fas fa-map"></i> <i class="fas fa-map"></i>
<span class="label">地区:</span> <span class="label">地区:</span>
<span id="region" class="value">--</span> <span id="region" class="value">--</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<i class="fas fa-city"></i> <i class="fas fa-city"></i>
<span class="label">城市:</span> <span class="label">城市:</span>
<span id="city" class="value">--</span> <span id="city" class="value">--</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<i class="fas fa-clock"></i> <i class="fas fa-clock"></i>
<span class="label">时区:</span> <span class="label">时区:</span>
<span id="timezone" class="value">--</span> <span id="timezone" class="value">--</span>
</div> </div>
</div> </div>
</div> </div>
<!-- IP地址信息说明 --> <!-- IP地址信息说明 -->
<div class="ip-explanation"> <div class="ip-explanation">
<h3><i class="fas fa-info-circle"></i> 什么是公网IP地址</h3> <h3><i class="fas fa-info-circle"></i> 什么是公网IP地址</h3>
<p>公网IP地址是您的设备在互联网上的唯一标识符由您的网络服务提供商(ISP)分配。通过这个地址,互联网上的其他设备可以找到并与您的设备通信。</p> <p>公网IP地址是您的设备在互联网上的唯一标识符由您的网络服务提供商(ISP)分配。通过这个地址,互联网上的其他设备可以找到并与您的设备通信。</p>
<div class="features"> <div class="features">
<div class="feature-item"> <div class="feature-item">
<i class="fas fa-shield-alt"></i> <i class="fas fa-shield-alt"></i>
<div> <div>
<h4>隐私保护</h4> <h4>隐私保护</h4>
<p>了解您的IP地址有助于保护网络隐私</p> <p>了解您的IP地址有助于保护网络隐私</p>
</div> </div>
</div> </div>
<div class="feature-item"> <div class="feature-item">
<i class="fas fa-map-marker-alt"></i> <i class="fas fa-map-marker-alt"></i>
<div> <div>
<h4>地理位置</h4> <h4>地理位置</h4>
<p>IP地址可以大致确定您的地理位置</p> <p>IP地址可以大致确定您的地理位置</p>
</div> </div>
</div> </div>
<div class="feature-item"> <div class="feature-item">
<i class="fas fa-cogs"></i> <i class="fas fa-cogs"></i>
<div> <div>
<h4>网络配置</h4> <h4>网络配置</h4>
<p>用于网络故障排除和配置</p> <p>用于网络故障排除和配置</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 错误信息 --> <!-- 错误信息 -->
<div id="error-message" class="error-message" style="display: none;"> <div id="error-message" class="error-message" style="display: none;">
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
<p>获取IP地址失败请检查网络连接或稍后重试</p> <p>获取IP地址失败请检查网络连接或稍后重试</p>
<button id="retryBtn" class="retry-btn">重试</button> <button id="retryBtn" class="retry-btn">重试</button>
</div> </div>
</main> </main>
<!-- 页脚 --> <!-- 页脚 -->
</div> </div>
<script src="js/script.js"></script> <script src="js/script.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,343 +1,343 @@
// 公网IP地址查询应用 // 公网IP地址查询应用
class IPQueryApp { class IPQueryApp {
constructor() { constructor() {
this.apiEndpoint = 'https://60s.api.shumengya.top/v2/ip'; this.apiEndpoint = 'https://60s.api.shumengya.top/v2/ip';
this.init(); this.init();
} }
// 初始化应用 // 初始化应用
init() { init() {
this.bindEvents(); this.bindEvents();
this.createParticles(); this.createParticles();
this.createBackgroundElements(); this.createBackgroundElements();
console.log('IP查询应用初始化完成'); console.log('IP查询应用初始化完成');
} }
// 绑定事件 // 绑定事件
bindEvents() { bindEvents() {
const queryBtn = document.getElementById('queryBtn'); const queryBtn = document.getElementById('queryBtn');
const retryBtn = document.getElementById('retryBtn'); const retryBtn = document.getElementById('retryBtn');
const copyBtn = document.getElementById('copyBtn'); const copyBtn = document.getElementById('copyBtn');
if (queryBtn) { if (queryBtn) {
queryBtn.addEventListener('click', () => this.queryIP()); queryBtn.addEventListener('click', () => this.queryIP());
} }
if (retryBtn) { if (retryBtn) {
retryBtn.addEventListener('click', () => this.queryIP()); retryBtn.addEventListener('click', () => this.queryIP());
} }
if (copyBtn) { if (copyBtn) {
copyBtn.addEventListener('click', () => this.copyIP()); copyBtn.addEventListener('click', () => this.copyIP());
} }
// 页面加载完成后自动查询一次 // 页面加载完成后自动查询一次
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => this.queryIP(), 500); setTimeout(() => this.queryIP(), 500);
}); });
} }
// 创建浮动粒子 // 创建浮动粒子
createParticles() { createParticles() {
const particlesContainer = document.createElement('div'); const particlesContainer = document.createElement('div');
particlesContainer.className = 'particles'; particlesContainer.className = 'particles';
document.body.appendChild(particlesContainer); document.body.appendChild(particlesContainer);
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const particle = document.createElement('div'); const particle = document.createElement('div');
particle.className = 'particle'; particle.className = 'particle';
particlesContainer.appendChild(particle); particlesContainer.appendChild(particle);
} }
} }
// 创建背景元素 // 创建背景元素
createBackgroundElements() { createBackgroundElements() {
// 创建网格背景 // 创建网格背景
const gridBackground = document.createElement('div'); const gridBackground = document.createElement('div');
gridBackground.className = 'grid-background'; gridBackground.className = 'grid-background';
document.body.appendChild(gridBackground); document.body.appendChild(gridBackground);
// 创建光晕效果 // 创建光晕效果
const glowEffect = document.createElement('div'); const glowEffect = document.createElement('div');
glowEffect.className = 'glow-effect'; glowEffect.className = 'glow-effect';
document.body.appendChild(glowEffect); document.body.appendChild(glowEffect);
} }
// 显示加载状态 // 显示加载状态
showLoading() { showLoading() {
const loading = document.getElementById('loading'); const loading = document.getElementById('loading');
const ipInfo = document.getElementById('ipInfo'); const ipInfo = document.getElementById('ipInfo');
const errorMessage = document.getElementById('errorMessage'); const errorMessage = document.getElementById('errorMessage');
const queryBtn = document.getElementById('queryBtn'); const queryBtn = document.getElementById('queryBtn');
if (loading) loading.style.display = 'block'; if (loading) loading.style.display = 'block';
if (ipInfo) ipInfo.style.display = 'none'; if (ipInfo) ipInfo.style.display = 'none';
if (errorMessage) errorMessage.style.display = 'none'; if (errorMessage) errorMessage.style.display = 'none';
if (queryBtn) { if (queryBtn) {
queryBtn.disabled = true; queryBtn.disabled = true;
queryBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 查询中...'; queryBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 查询中...';
} }
} }
// 隐藏加载状态 // 隐藏加载状态
hideLoading() { hideLoading() {
const loading = document.getElementById('loading'); const loading = document.getElementById('loading');
const queryBtn = document.getElementById('queryBtn'); const queryBtn = document.getElementById('queryBtn');
if (loading) loading.style.display = 'none'; if (loading) loading.style.display = 'none';
if (queryBtn) { if (queryBtn) {
queryBtn.disabled = false; queryBtn.disabled = false;
queryBtn.innerHTML = '<i class="fas fa-search"></i> 查询我的IP'; queryBtn.innerHTML = '<i class="fas fa-search"></i> 查询我的IP';
} }
} }
// 显示错误信息 // 显示错误信息
showError(message) { showError(message) {
const errorMessage = document.getElementById('error-message'); const errorMessage = document.getElementById('error-message');
const ipInfo = document.getElementById('ip-info'); const ipInfo = document.getElementById('ip-info');
if (errorMessage) { if (errorMessage) {
errorMessage.style.display = 'block'; errorMessage.style.display = 'block';
const errorText = errorMessage.querySelector('p'); const errorText = errorMessage.querySelector('p');
if (errorText) errorText.textContent = message || '获取IP信息失败请稍后重试'; if (errorText) errorText.textContent = message || '获取IP信息失败请稍后重试';
} }
if (ipInfo) ipInfo.style.display = 'none'; if (ipInfo) ipInfo.style.display = 'none';
this.hideLoading(); this.hideLoading();
} }
// 查询IP地址 // 查询IP地址
async queryIP() { async queryIP() {
try { try {
this.showLoading(); this.showLoading();
console.log('开始查询IP地址...'); console.log('开始查询IP地址...');
const response = await fetch(this.apiEndpoint, { const response = await fetch(this.apiEndpoint, {
method: 'GET', method: 'GET',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}); });
console.log('API响应状态:', response.status); console.log('API响应状态:', response.status);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`); throw new Error(`HTTP错误: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
console.log('API返回数据:', data); console.log('API返回数据:', data);
if (data.code === 200 && data.data) { if (data.code === 200 && data.data) {
this.displayIPInfo(data.data); this.displayIPInfo(data.data);
} else { } else {
throw new Error(data.message || '获取IP信息失败'); throw new Error(data.message || '获取IP信息失败');
} }
} catch (error) { } catch (error) {
console.error('查询IP失败:', error); console.error('查询IP失败:', error);
this.showError(error.message); this.showError(error.message);
} }
} }
// 显示IP信息 // 显示IP信息
displayIPInfo(data) { displayIPInfo(data) {
const ipInfo = document.getElementById('ip-info'); const ipInfo = document.getElementById('ip-info');
const errorMessage = document.getElementById('error-message'); const errorMessage = document.getElementById('error-message');
// 更新IP地址显示 // 更新IP地址显示
const ipAddressElement = document.getElementById('ip-address'); const ipAddressElement = document.getElementById('ip-address');
if (ipAddressElement && data.ip) { if (ipAddressElement && data.ip) {
ipAddressElement.textContent = data.ip; ipAddressElement.textContent = data.ip;
} }
// 更新查询时间 // 更新查询时间
const queryTimeElement = document.getElementById('query-time'); const queryTimeElement = document.getElementById('query-time');
if (queryTimeElement) { if (queryTimeElement) {
const now = new Date(); const now = new Date();
queryTimeElement.textContent = now.toLocaleString('zh-CN'); queryTimeElement.textContent = now.toLocaleString('zh-CN');
} }
// 更新详细信息 - 只显示API提供的数据 // 更新详细信息 - 只显示API提供的数据
if (data.location) this.updateDetailItem('location', data.location); if (data.location) this.updateDetailItem('location', data.location);
else this.hideDetailItem('location'); else this.hideDetailItem('location');
if (data.isp) this.updateDetailItem('isp', data.isp); if (data.isp) this.updateDetailItem('isp', data.isp);
else this.hideDetailItem('isp'); else this.hideDetailItem('isp');
if (data.country) this.updateDetailItem('country', data.country); if (data.country) this.updateDetailItem('country', data.country);
else this.hideDetailItem('country'); else this.hideDetailItem('country');
if (data.region) this.updateDetailItem('region', data.region); if (data.region) this.updateDetailItem('region', data.region);
else this.hideDetailItem('region'); else this.hideDetailItem('region');
if (data.city) this.updateDetailItem('city', data.city); if (data.city) this.updateDetailItem('city', data.city);
else this.hideDetailItem('city'); else this.hideDetailItem('city');
if (data.timezone) this.updateDetailItem('timezone', data.timezone); if (data.timezone) this.updateDetailItem('timezone', data.timezone);
else this.hideDetailItem('timezone'); else this.hideDetailItem('timezone');
// 显示IP信息隐藏错误信息 // 显示IP信息隐藏错误信息
if (ipInfo) ipInfo.style.display = 'block'; if (ipInfo) ipInfo.style.display = 'block';
if (errorMessage) errorMessage.style.display = 'none'; if (errorMessage) errorMessage.style.display = 'none';
this.hideLoading(); this.hideLoading();
console.log('IP信息显示完成'); console.log('IP信息显示完成');
} }
// 更新详细信息项 // 更新详细信息项
updateDetailItem(id, value) { updateDetailItem(id, value) {
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) { if (element) {
element.textContent = value; element.textContent = value;
// 显示对应的详细信息行 // 显示对应的详细信息行
const detailRow = element.closest('.detail-item'); const detailRow = element.closest('.detail-item');
if (detailRow) { if (detailRow) {
detailRow.style.display = 'flex'; detailRow.style.display = 'flex';
} }
} }
} }
// 隐藏详细信息项 // 隐藏详细信息项
hideDetailItem(id) { hideDetailItem(id) {
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) { if (element) {
// 隐藏整个详细信息行 // 隐藏整个详细信息行
const detailRow = element.closest('.detail-item'); const detailRow = element.closest('.detail-item');
if (detailRow) { if (detailRow) {
detailRow.style.display = 'none'; detailRow.style.display = 'none';
} }
} }
} }
// 复制IP地址 // 复制IP地址
async copyIP() { async copyIP() {
const ipAddressElement = document.getElementById('ip-address'); const ipAddressElement = document.getElementById('ip-address');
const copyBtn = document.getElementById('copyBtn'); const copyBtn = document.getElementById('copyBtn');
if (!ipAddressElement || !ipAddressElement.textContent) { if (!ipAddressElement || !ipAddressElement.textContent) {
this.showToast('没有可复制的IP地址', 'error'); this.showToast('没有可复制的IP地址', 'error');
return; return;
} }
try { try {
await navigator.clipboard.writeText(ipAddressElement.textContent); await navigator.clipboard.writeText(ipAddressElement.textContent);
// 更新按钮状态 // 更新按钮状态
if (copyBtn) { if (copyBtn) {
const originalHTML = copyBtn.innerHTML; const originalHTML = copyBtn.innerHTML;
copyBtn.innerHTML = '<i class="fas fa-check"></i>'; copyBtn.innerHTML = '<i class="fas fa-check"></i>';
copyBtn.style.background = '#50c878'; copyBtn.style.background = '#50c878';
setTimeout(() => { setTimeout(() => {
copyBtn.innerHTML = originalHTML; copyBtn.innerHTML = originalHTML;
copyBtn.style.background = '#4a90e2'; copyBtn.style.background = '#4a90e2';
}, 1500); }, 1500);
} }
this.showToast('IP地址已复制到剪贴板', 'success'); this.showToast('IP地址已复制到剪贴板', 'success');
console.log('IP地址已复制:', ipAddressElement.textContent); console.log('IP地址已复制:', ipAddressElement.textContent);
} catch (error) { } catch (error) {
console.error('复制失败:', error); console.error('复制失败:', error);
this.showToast('复制失败,请手动选择复制', 'error'); this.showToast('复制失败,请手动选择复制', 'error');
} }
} }
// 显示提示消息 // 显示提示消息
showToast(message, type = 'info') { showToast(message, type = 'info') {
// 移除已存在的toast // 移除已存在的toast
const existingToast = document.querySelector('.toast'); const existingToast = document.querySelector('.toast');
if (existingToast) { if (existingToast) {
existingToast.remove(); existingToast.remove();
} }
// 创建新的toast // 创建新的toast
const toast = document.createElement('div'); const toast = document.createElement('div');
toast.className = `toast toast-${type}`; toast.className = `toast toast-${type}`;
toast.innerHTML = ` toast.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i> <i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
<span>${message}</span> <span>${message}</span>
`; `;
// 添加toast样式 // 添加toast样式
toast.style.cssText = ` toast.style.cssText = `
position: fixed; position: fixed;
top: 20px; top: 20px;
right: 20px; right: 20px;
background: ${type === 'success' ? '#50c878' : type === 'error' ? '#ff6b6b' : '#4a90e2'}; background: ${type === 'success' ? '#50c878' : type === 'error' ? '#ff6b6b' : '#4a90e2'};
color: white; color: white;
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
z-index: 1000; z-index: 1000;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
font-weight: 500; font-weight: 500;
animation: slideInRight 0.3s ease; animation: slideInRight 0.3s ease;
max-width: 300px; max-width: 300px;
`; `;
// 添加动画样式 // 添加动画样式
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = ` style.textContent = `
@keyframes slideInRight { @keyframes slideInRight {
from { from {
transform: translateX(100%); transform: translateX(100%);
opacity: 0; opacity: 0;
} }
to { to {
transform: translateX(0); transform: translateX(0);
opacity: 1; opacity: 1;
} }
} }
@keyframes slideOutRight { @keyframes slideOutRight {
from { from {
transform: translateX(0); transform: translateX(0);
opacity: 1; opacity: 1;
} }
to { to {
transform: translateX(100%); transform: translateX(100%);
opacity: 0; opacity: 0;
} }
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
document.body.appendChild(toast); document.body.appendChild(toast);
// 3秒后自动移除 // 3秒后自动移除
setTimeout(() => { setTimeout(() => {
toast.style.animation = 'slideOutRight 0.3s ease'; toast.style.animation = 'slideOutRight 0.3s ease';
setTimeout(() => { setTimeout(() => {
if (toast.parentNode) { if (toast.parentNode) {
toast.remove(); toast.remove();
} }
if (style.parentNode) { if (style.parentNode) {
style.remove(); style.remove();
} }
}, 300); }, 300);
}, 3000); }, 3000);
} }
// 格式化时间 // 格式化时间
formatTime(timestamp) { formatTime(timestamp) {
const date = new Date(timestamp); const date = new Date(timestamp);
return date.toLocaleString('zh-CN', { return date.toLocaleString('zh-CN', {
year: 'numeric', year: 'numeric',
month: '2-digit', month: '2-digit',
day: '2-digit', day: '2-digit',
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
second: '2-digit' second: '2-digit'
}); });
} }
} }
// 初始化应用 // 初始化应用
const app = new IPQueryApp(); const app = new IPQueryApp();
// 导出到全局作用域(用于调试) // 导出到全局作用域(用于调试)
window.IPQueryApp = app; window.IPQueryApp = app;

View File

@@ -1,3 +1,3 @@
[ [
"https://60s.api.shumengya.top/v2/ip" "https://60s.api.shumengya.top/v2/ip"
] ]

View File

@@ -1,17 +1,17 @@
{ {
"code": 200, "code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841", "message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": { "data": {
"ip": "2401:b60:16:83::" "ip": "2401:b60:16:83::"
} }
} }
// 注意此API只返回IP地址不包含以下信息 // 注意此API只返回IP地址不包含以下信息
// - location (位置信息) // - location (位置信息)
// - isp (网络服务商) // - isp (网络服务商)
// - country (国家) // - country (国家)
// - region (地区) // - region (地区)
// - city (城市) // - city (城市)
// - timezone (时区) // - timezone (时区)
// //
// 使API // 使API

View File

@@ -1,108 +1,108 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🌙农历信息查询</title> <title>🌙农历信息查询</title>
<link rel="stylesheet" href="css/background.css"> <link rel="stylesheet" href="css/background.css">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
</head> </head>
<body> <body>
<!-- 动态调节遮罩层 --> <!-- 动态调节遮罩层 -->
<div class="adaptive-overlay"></div> <div class="adaptive-overlay"></div>
<!-- 流星效果容器 --> <!-- 流星效果容器 -->
<div class="meteor-container"> <div class="meteor-container">
<div class="meteor"></div> <div class="meteor"></div>
<div class="meteor"></div> <div class="meteor"></div>
<div class="meteor"></div> <div class="meteor"></div>
<div class="meteor"></div> <div class="meteor"></div>
<div class="meteor"></div> <div class="meteor"></div>
<div class="meteor"></div> <div class="meteor"></div>
</div> </div>
<!-- 金色粒子效果容器 --> <!-- 金色粒子效果容器 -->
<div class="golden-particles"> <div class="golden-particles">
<div class="particle"></div> <div class="particle"></div>
<div class="particle"></div> <div class="particle"></div>
<div class="particle"></div> <div class="particle"></div>
<div class="particle"></div> <div class="particle"></div>
<div class="particle"></div> <div class="particle"></div>
<div class="particle"></div> <div class="particle"></div>
<div class="particle"></div> <div class="particle"></div>
<div class="particle"></div> <div class="particle"></div>
<div class="particle"></div> <div class="particle"></div>
</div> </div>
<!-- 麦穗飘舞特效 --> <!-- 麦穗飘舞特效 -->
<div class="wheat-floating"> <div class="wheat-floating">
<div class="wheat-particle"></div> <div class="wheat-particle"></div>
<div class="wheat-particle"></div> <div class="wheat-particle"></div>
<div class="wheat-particle"></div> <div class="wheat-particle"></div>
<div class="wheat-particle"></div> <div class="wheat-particle"></div>
<div class="wheat-particle"></div> <div class="wheat-particle"></div>
<div class="wheat-particle"></div> <div class="wheat-particle"></div>
<div class="wheat-particle"></div> <div class="wheat-particle"></div>
<div class="wheat-particle"></div> <div class="wheat-particle"></div>
</div> </div>
<div class="container"> <div class="container">
<header class="header"> <header class="header">
<h1 class="title">🌙农历信息查询</h1> <h1 class="title">🌙农历信息查询</h1>
<p class="subtitle">传统文化 · 时光转换 · 节气查询</p> <p class="subtitle">传统文化 · 时光转换 · 节气查询</p>
<div class="date-selector"> <div class="date-selector">
<div class="input-group"> <div class="input-group">
<label for="dateInput" class="input-label"> <label for="dateInput" class="input-label">
<span class="label-icon">📅</span> <span class="label-icon">📅</span>
选择日期 选择日期
</label> </label>
<input type="date" id="dateInput" class="date-input" /> <input type="date" id="dateInput" class="date-input" />
</div> </div>
<button id="queryBtn" class="query-btn"> <button id="queryBtn" class="query-btn">
<span class="btn-icon">🔍</span> <span class="btn-icon">🔍</span>
查询农历 查询农历
</button> </button>
</div> </div>
<div class="update-time"> <div class="update-time">
<span class="time-icon"></span> <span class="time-icon"></span>
<span id="updateTime">等待查询...</span> <span id="updateTime">等待查询...</span>
</div> </div>
</header> </header>
<div class="loading" id="loading" style="display: none;"> <div class="loading" id="loading" style="display: none;">
<div class="loading-content"> <div class="loading-content">
<div class="glass-spinner"></div> <div class="glass-spinner"></div>
<div class="loading-text"> <div class="loading-text">
<span class="loading-emoji">🔮</span> <span class="loading-emoji">🔮</span>
<p>正在查询农历信息...</p> <p>正在查询农历信息...</p>
<div class="loading-dots"> <div class="loading-dots">
<span></span> <span></span>
<span></span> <span></span>
<span></span> <span></span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="lunar-info" id="lunarInfo" style="display: none;"> <div class="lunar-info" id="lunarInfo" style="display: none;">
<!-- 农历信息将动态生成 --> <!-- 农历信息将动态生成 -->
</div> </div>
<div class="error-message" id="errorMessage" style="display: none;"> <div class="error-message" id="errorMessage" style="display: none;">
<div class="error-content"> <div class="error-content">
<div class="error-icon">😔</div> <div class="error-icon">😔</div>
<h3>查询失败了</h3> <h3>查询失败了</h3>
<p>无法获取农历信息,请稍后重试</p> <p>无法获取农历信息,请稍后重试</p>
<button onclick="queryLunarInfo()" class="retry-btn"> <button onclick="queryLunarInfo()" class="retry-btn">
<span>🔄</span> <span>🔄</span>
重新查询 重新查询
</button> </button>
</div> </div>
</div> </div>
<script src="js/script.js"></script> <script src="js/script.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,475 +1,475 @@
// API接口列表 // API接口列表
const API_ENDPOINTS = [ const API_ENDPOINTS = [
"https://60s.api.shumengya.top", "https://60s.api.shumengya.top",
]; ];
// 当前使用的API索引 // 当前使用的API索引
let currentApiIndex = 0; let currentApiIndex = 0;
// DOM元素 // DOM元素
const loadingElement = document.getElementById('loading'); const loadingElement = document.getElementById('loading');
const lunarInfoElement = document.getElementById('lunarInfo'); const lunarInfoElement = document.getElementById('lunarInfo');
const errorMessageElement = document.getElementById('errorMessage'); const errorMessageElement = document.getElementById('errorMessage');
const updateTimeElement = document.getElementById('updateTime'); const updateTimeElement = document.getElementById('updateTime');
const dateInput = document.getElementById('dateInput'); const dateInput = document.getElementById('dateInput');
const queryBtn = document.getElementById('queryBtn'); const queryBtn = document.getElementById('queryBtn');
// 页面加载完成后初始化 // 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
initializePage(); initializePage();
}); });
// 初始化页面 // 初始化页面
function initializePage() { function initializePage() {
// 设置默认日期为今天 // 设置默认日期为今天
const today = new Date(); const today = new Date();
const dateString = today.toISOString().split('T')[0]; const dateString = today.toISOString().split('T')[0];
dateInput.value = dateString; dateInput.value = dateString;
// 绑定事件 // 绑定事件
queryBtn.addEventListener('click', queryLunarInfo); queryBtn.addEventListener('click', queryLunarInfo);
dateInput.addEventListener('change', queryLunarInfo); dateInput.addEventListener('change', queryLunarInfo);
// 自动查询当天信息 // 自动查询当天信息
queryLunarInfo(); queryLunarInfo();
} }
// 查询农历信息 // 查询农历信息
async function queryLunarInfo() { async function queryLunarInfo() {
const selectedDate = dateInput.value; const selectedDate = dateInput.value;
if (!selectedDate) { if (!selectedDate) {
showError('请选择查询日期'); showError('请选择查询日期');
return; return;
} }
showLoading(); showLoading();
hideError(); hideError();
hideLunarInfo(); hideLunarInfo();
try { try {
const data = await fetchLunarData(selectedDate); const data = await fetchLunarData(selectedDate);
displayLunarInfo(data.data); displayLunarInfo(data.data);
updateQueryTime(); updateQueryTime();
} catch (error) { } catch (error) {
console.error('查询失败:', error); console.error('查询失败:', error);
showError('查询农历信息失败,请稍后重试'); showError('查询农历信息失败,请稍后重试');
} }
hideLoading(); hideLoading();
} }
// 获取农历数据 // 获取农历数据
async function fetchLunarData(date) { async function fetchLunarData(date) {
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];
try { try {
const response = await fetch(`${apiUrl}/v2/lunar?date=${date}`, { const response = await fetch(`${apiUrl}/v2/lunar?date=${date}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
}, },
timeout: 10000 timeout: 10000
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status}`); throw new Error(`HTTP ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
if (data.code === 200 && data.data) { if (data.code === 200 && data.data) {
return data; return data;
} else { } else {
throw new Error('数据格式错误'); throw new Error('数据格式错误');
} }
} catch (error) { } catch (error) {
console.error(`API ${apiUrl} 请求失败:`, error); console.error(`API ${apiUrl} 请求失败:`, error);
currentApiIndex = (currentApiIndex + 1) % API_ENDPOINTS.length; currentApiIndex = (currentApiIndex + 1) % API_ENDPOINTS.length;
if (i === API_ENDPOINTS.length - 1) { if (i === API_ENDPOINTS.length - 1) {
throw new Error('所有API接口都无法访问'); throw new Error('所有API接口都无法访问');
} }
} }
} }
} }
// 显示农历信息 // 显示农历信息
function displayLunarInfo(lunarData) { function displayLunarInfo(lunarData) {
lunarInfoElement.innerHTML = ` lunarInfoElement.innerHTML = `
<div class="info-card"> <div class="info-card">
<div class="card-header"> <div class="card-header">
<div class="card-icon">📅</div> <div class="card-icon">📅</div>
<div class="card-title">公历信息</div> <div class="card-title">公历信息</div>
</div> </div>
<div class="card-content"> <div class="card-content">
<div class="info-item"> <div class="info-item">
<div class="item-icon">🗓️</div> <div class="item-icon">🗓️</div>
<div class="item-label">公历日期</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 class="item-value">${lunarData.solar.year}${String(lunarData.solar.month).padStart(2, '0')}${String(lunarData.solar.day).padStart(2, '0')}日</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">🌍</div> <div class="item-icon">🌍</div>
<div class="item-label">星期</div> <div class="item-label">星期</div>
<div class="item-value">${lunarData.solar.week_desc}</div> <div class="item-value">${lunarData.solar.week_desc}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">🍂</div> <div class="item-icon">🍂</div>
<div class="item-label">季节</div> <div class="item-label">季节</div>
<div class="item-value">${lunarData.solar.season_name_desc}</div> <div class="item-value">${lunarData.solar.season_name_desc}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">⭐</div> <div class="item-icon">⭐</div>
<div class="item-label">星座</div> <div class="item-label">星座</div>
<div class="item-value">${lunarData.constellation.name}</div> <div class="item-value">${lunarData.constellation.name}</div>
</div> </div>
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card">
<div class="card-header"> <div class="card-header">
<div class="card-icon">🌙</div> <div class="card-icon">🌙</div>
<div class="card-title">农历信息</div> <div class="card-title">农历信息</div>
</div> </div>
<div class="card-content"> <div class="card-content">
<div class="info-item"> <div class="info-item">
<div class="item-icon">🏮</div> <div class="item-icon">🏮</div>
<div class="item-label">农历日期</div> <div class="item-label">农历日期</div>
<div class="item-value">${lunarData.lunar.desc_short}</div> <div class="item-value">${lunarData.lunar.desc_short}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">🐲</div> <div class="item-icon">🐲</div>
<div class="item-label">生肖年</div> <div class="item-label">生肖年</div>
<div class="item-value">${lunarData.zodiac.year}年</div> <div class="item-value">${lunarData.zodiac.year}年</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">☯️</div> <div class="item-icon">☯️</div>
<div class="item-label">天干地支</div> <div class="item-label">天干地支</div>
<div class="item-value">${lunarData.sixty_cycle.year.name}</div> <div class="item-value">${lunarData.sixty_cycle.year.name}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">🌙</div> <div class="item-icon">🌙</div>
<div class="item-label">月相</div> <div class="item-label">月相</div>
<div class="item-value">${lunarData.phase.name}</div> <div class="item-value">${lunarData.phase.name}</div>
</div> </div>
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card">
<div class="card-header"> <div class="card-header">
<div class="card-icon">🌾</div> <div class="card-icon">🌾</div>
<div class="card-title">节气节日</div> <div class="card-title">节气节日</div>
</div> </div>
<div class="card-content"> <div class="card-content">
<div class="info-item"> <div class="info-item">
<div class="item-icon">🌱</div> <div class="item-icon">🌱</div>
<div class="item-label">当前节气</div> <div class="item-label">当前节气</div>
<div class="item-value">${lunarData.term.stage ? lunarData.term.stage.name : '无节气'}</div> <div class="item-value">${lunarData.term.stage ? lunarData.term.stage.name : '无节气'}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">🎉</div> <div class="item-icon">🎉</div>
<div class="item-label">法定假日</div> <div class="item-label">法定假日</div>
<div class="item-value">${lunarData.legal_holiday ? lunarData.legal_holiday.name : '无假日'}</div> <div class="item-value">${lunarData.legal_holiday ? lunarData.legal_holiday.name : '无假日'}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">🎊</div> <div class="item-icon">🎊</div>
<div class="item-label">传统节日</div> <div class="item-label">传统节日</div>
<div class="item-value">${lunarData.festival.both_desc || '无特殊节日'}</div> <div class="item-value">${lunarData.festival.both_desc || '无特殊节日'}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">📊</div> <div class="item-icon">📊</div>
<div class="item-label">一年第几天</div> <div class="item-label">一年第几天</div>
<div class="item-value">第${lunarData.stats.day_of_year}天</div> <div class="item-value">第${lunarData.stats.day_of_year}天</div>
</div> </div>
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card">
<div class="card-header"> <div class="card-header">
<div class="card-icon">⏰</div> <div class="card-icon">⏰</div>
<div class="card-title">时辰干支</div> <div class="card-title">时辰干支</div>
</div> </div>
<div class="card-content"> <div class="card-content">
<div class="info-item"> <div class="info-item">
<div class="item-icon">🕐</div> <div class="item-icon">🕐</div>
<div class="item-label">当前时辰</div> <div class="item-label">当前时辰</div>
<div class="item-value">${lunarData.lunar.hour_desc}</div> <div class="item-value">${lunarData.lunar.hour_desc}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">☯️</div> <div class="item-icon">☯️</div>
<div class="item-label">时辰干支</div> <div class="item-label">时辰干支</div>
<div class="item-value">${lunarData.sixty_cycle.hour.name}</div> <div class="item-value">${lunarData.sixty_cycle.hour.name}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">🐾</div> <div class="item-icon">🐾</div>
<div class="item-label">时辰生肖</div> <div class="item-label">时辰生肖</div>
<div class="item-value">${lunarData.zodiac.hour}</div> <div class="item-value">${lunarData.zodiac.hour}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">🎵</div> <div class="item-icon">🎵</div>
<div class="item-label">纳音</div> <div class="item-label">纳音</div>
<div class="item-value">${lunarData.nayin.hour}</div> <div class="item-value">${lunarData.nayin.hour}</div>
</div> </div>
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card">
<div class="card-header"> <div class="card-header">
<div class="card-icon">📖</div> <div class="card-icon">📖</div>
<div class="card-title">黄历宜忌</div> <div class="card-title">黄历宜忌</div>
</div> </div>
<div class="card-content"> <div class="card-content">
<div class="info-item"> <div class="info-item">
<div class="item-icon">✅</div> <div class="item-icon">✅</div>
<div class="item-label">宜</div> <div class="item-label">宜</div>
<div class="item-value">${formatTabooText(lunarData.taboo.day.recommends)}</div> <div class="item-value">${formatTabooText(lunarData.taboo.day.recommends)}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">❌</div> <div class="item-icon">❌</div>
<div class="item-label">忌</div> <div class="item-label">忌</div>
<div class="item-value">${formatTabooText(lunarData.taboo.day.avoids)}</div> <div class="item-value">${formatTabooText(lunarData.taboo.day.avoids)}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">🕐</div> <div class="item-icon">🕐</div>
<div class="item-label">时辰宜</div> <div class="item-label">时辰宜</div>
<div class="item-value">${formatTabooText(lunarData.taboo.hour.recommends)}</div> <div class="item-value">${formatTabooText(lunarData.taboo.hour.recommends)}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">🚫</div> <div class="item-icon">🚫</div>
<div class="item-label">时辰忌</div> <div class="item-label">时辰忌</div>
<div class="item-value">${formatTabooText(lunarData.taboo.hour.avoids)}</div> <div class="item-value">${formatTabooText(lunarData.taboo.hour.avoids)}</div>
</div> </div>
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card">
<div class="card-header"> <div class="card-header">
<div class="card-icon">🌟</div> <div class="card-icon">🌟</div>
<div class="card-title">运势财运</div> <div class="card-title">运势财运</div>
</div> </div>
<div class="card-content"> <div class="card-content">
<div class="info-item"> <div class="info-item">
<div class="item-icon">🍀</div> <div class="item-icon">🍀</div>
<div class="item-label">今日运势</div> <div class="item-label">今日运势</div>
<div class="item-value">${lunarData.fortune.today_luck}</div> <div class="item-value">${lunarData.fortune.today_luck}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">💼</div> <div class="item-icon">💼</div>
<div class="item-label">事业运</div> <div class="item-label">事业运</div>
<div class="item-value">${lunarData.fortune.career}</div> <div class="item-value">${lunarData.fortune.career}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">💰</div> <div class="item-icon">💰</div>
<div class="item-label">财运</div> <div class="item-label">财运</div>
<div class="item-value">${lunarData.fortune.money}</div> <div class="item-value">${lunarData.fortune.money}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">💖</div> <div class="item-icon">💖</div>
<div class="item-label">感情运</div> <div class="item-label">感情运</div>
<div class="item-value">${lunarData.fortune.love}</div> <div class="item-value">${lunarData.fortune.love}</div>
</div> </div>
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card">
<div class="card-header"> <div class="card-header">
<div class="card-icon">📈</div> <div class="card-icon">📈</div>
<div class="card-title">年度统计</div> <div class="card-title">年度统计</div>
</div> </div>
<div class="card-content"> <div class="card-content">
<div class="info-item"> <div class="info-item">
<div class="item-icon">📊</div> <div class="item-icon">📊</div>
<div class="item-label">年度进度</div> <div class="item-label">年度进度</div>
<div class="item-value">${lunarData.stats.percents_formatted.year}</div> <div class="item-value">${lunarData.stats.percents_formatted.year}</div>
</div> </div>
<div class="info-item"> <div class="info-item">
<div class="item-icon">📅</div> <div class="item-icon">📅</div>
<div class="item-label">本月进度</div> <div class="item-label">本月进度</div>
<div class="item-value">${lunarData.stats.percents_formatted.month}</div> <div class="item-value">${lunarData.stats.percents_formatted.month}</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
${generateHourlyTaboo(lunarData.taboo.hours)} ${generateHourlyTaboo(lunarData.taboo.hours)}
`; `;
showLunarInfo(); showLunarInfo();
} }
// 格式化宜忌文本 // 格式化宜忌文本
function formatTabooText(text) { function formatTabooText(text) {
if (!text) return '无'; if (!text) return '无';
return text.replace(/\./g, '、'); return text.replace(/\./g, '、');
} }
// 生成十二时辰宜忌 // 生成十二时辰宜忌
function generateHourlyTaboo(hours) { function generateHourlyTaboo(hours) {
if (!hours || hours.length === 0) return ''; if (!hours || hours.length === 0) return '';
const hourCards = hours.map(hour => ` const hourCards = hours.map(hour => `
<div class="hour-item"> <div class="hour-item">
<div class="hour-name">${hour.hour}</div> <div class="hour-name">${hour.hour}</div>
<div class="hour-content"> <div class="hour-content">
<div class="hour-recommends"> <div class="hour-recommends">
<span class="hour-label">宜:</span> <span class="hour-label">宜:</span>
<span class="hour-text">${formatTabooText(hour.recommends) || '无'}</span> <span class="hour-text">${formatTabooText(hour.recommends) || '无'}</span>
</div> </div>
<div class="hour-avoids"> <div class="hour-avoids">
<span class="hour-label">忌:</span> <span class="hour-label">忌:</span>
<span class="hour-text">${formatTabooText(hour.avoids) || '无'}</span> <span class="hour-text">${formatTabooText(hour.avoids) || '无'}</span>
</div> </div>
</div> </div>
</div> </div>
`).join(''); `).join('');
return ` return `
<div class="info-card hours-card"> <div class="info-card hours-card">
<div class="card-header"> <div class="card-header">
<div class="card-icon">⏰</div> <div class="card-icon">⏰</div>
<div class="card-title">十二时辰宜忌</div> <div class="card-title">十二时辰宜忌</div>
</div> </div>
<div class="card-content"> <div class="card-content">
<div class="hours-grid"> <div class="hours-grid">
${hourCards} ${hourCards}
</div> </div>
</div> </div>
</div> </div>
`; `;
} }
// 更新查询时间 // 更新查询时间
function updateQueryTime() { function updateQueryTime() {
const now = new Date(); const now = new Date();
const timeStr = now.toLocaleString('zh-CN', { const timeStr = now.toLocaleString('zh-CN', {
year: 'numeric', year: 'numeric',
month: '2-digit', month: '2-digit',
day: '2-digit', day: '2-digit',
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
second: '2-digit' second: '2-digit'
}); });
updateTimeElement.textContent = `查询时间: ${timeStr}`; updateTimeElement.textContent = `查询时间: ${timeStr}`;
// 添加成功提示 // 添加成功提示
showSuccessMessage('🌙 农历信息已更新'); showSuccessMessage('🌙 农历信息已更新');
} }
// 显示成功消息 // 显示成功消息
function showSuccessMessage(message) { function showSuccessMessage(message) {
// 移除之前的提示 // 移除之前的提示
const existingToast = document.querySelector('.success-toast'); const existingToast = document.querySelector('.success-toast');
if (existingToast) { if (existingToast) {
existingToast.remove(); existingToast.remove();
} }
const toast = document.createElement('div'); const toast = document.createElement('div');
toast.className = 'success-toast'; toast.className = 'success-toast';
toast.textContent = message; toast.textContent = message;
toast.style.cssText = ` toast.style.cssText = `
position: fixed; position: fixed;
top: 20px; top: 20px;
right: 20px; right: 20px;
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(15px); backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px);
color: #1a1a1a; color: #1a1a1a;
padding: 12px 20px; padding: 12px 20px;
border-radius: 25px; border-radius: 25px;
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.2); box-shadow: 0 8px 32px rgba(31, 38, 135, 0.2);
z-index: 1000; z-index: 1000;
font-weight: 600; font-weight: 600;
font-size: 0.9em; font-size: 0.9em;
animation: glassToastSlide 0.5s ease-out; animation: glassToastSlide 0.5s ease-out;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
`; `;
document.body.appendChild(toast); document.body.appendChild(toast);
// 3秒后自动移除 // 3秒后自动移除
setTimeout(() => { setTimeout(() => {
toast.style.animation = 'glassToastSlideOut 0.5s ease-in forwards'; toast.style.animation = 'glassToastSlideOut 0.5s ease-in forwards';
setTimeout(() => toast.remove(), 500); setTimeout(() => toast.remove(), 500);
}, 3000); }, 3000);
} }
// 显示加载状态 // 显示加载状态
function showLoading() { function showLoading() {
loadingElement.style.display = 'block'; loadingElement.style.display = 'block';
} }
// 隐藏加载状态 // 隐藏加载状态
function hideLoading() { function hideLoading() {
loadingElement.style.display = 'none'; loadingElement.style.display = 'none';
} }
// 显示农历信息 // 显示农历信息
function showLunarInfo() { function showLunarInfo() {
lunarInfoElement.style.display = 'block'; lunarInfoElement.style.display = 'block';
} }
// 隐藏农历信息 // 隐藏农历信息
function hideLunarInfo() { function hideLunarInfo() {
lunarInfoElement.style.display = 'none'; lunarInfoElement.style.display = 'none';
} }
// 显示错误信息 // 显示错误信息
function showError(message = '查询失败,请稍后重试') { function showError(message = '查询失败,请稍后重试') {
errorMessageElement.style.display = 'block'; errorMessageElement.style.display = 'block';
const errorContent = errorMessageElement.querySelector('.error-content p'); const errorContent = errorMessageElement.querySelector('.error-content p');
if (errorContent) { if (errorContent) {
errorContent.textContent = message; errorContent.textContent = message;
} }
} }
// 隐藏错误信息 // 隐藏错误信息
function hideError() { function hideError() {
errorMessageElement.style.display = 'none'; errorMessageElement.style.display = 'none';
} }
// 添加CSS动画到页面 // 添加CSS动画到页面
if (!document.querySelector('#toast-styles')) { if (!document.querySelector('#toast-styles')) {
const style = document.createElement('style'); const style = document.createElement('style');
style.id = 'toast-styles'; style.id = 'toast-styles';
style.textContent = ` style.textContent = `
@keyframes glassToastSlide { @keyframes glassToastSlide {
from { from {
opacity: 0; opacity: 0;
transform: translateX(100px) scale(0.8); transform: translateX(100px) scale(0.8);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateX(0) scale(1); transform: translateX(0) scale(1);
} }
} }
@keyframes glassToastSlideOut { @keyframes glassToastSlideOut {
from { from {
opacity: 1; opacity: 1;
transform: translateX(0) scale(1); transform: translateX(0) scale(1);
} }
to { to {
opacity: 0; opacity: 0;
transform: translateX(100px) scale(0.8); transform: translateX(100px) scale(0.8);
} }
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
} }
// 键盘快捷键支持 // 键盘快捷键支持
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
queryLunarInfo(); queryLunarInfo();
} }
if (e.key === 'r' && (e.ctrlKey || e.metaKey)) { if (e.key === 'r' && (e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
queryLunarInfo(); queryLunarInfo();
} }
}); });

View File

@@ -1,3 +1,3 @@
[ [
"https://60s.api.shumengya.top" "https://60s.api.shumengya.top"
] ]

View File

@@ -1,212 +1,212 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多功能哈希工具 - Hash Toolkit</title> <title>多功能哈希工具 - Hash Toolkit</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<!-- Header Section --> <!-- Header Section -->
<header class="header"> <header class="header">
<div class="header-content"> <div class="header-content">
<div class="logo"> <div class="logo">
<i class="fas fa-fingerprint"></i> <i class="fas fa-fingerprint"></i>
<h1>Hash Toolkit</h1> <h1>Hash Toolkit</h1>
</div> </div>
<p class="subtitle">多功能哈希、编码与压缩工具</p> <p class="subtitle">多功能哈希、编码与压缩工具</p>
</div> </div>
<div class="header-decoration"> <div class="header-decoration">
<div class="floating-shapes"> <div class="floating-shapes">
<div class="shape shape-1"></div> <div class="shape shape-1"></div>
<div class="shape shape-2"></div> <div class="shape shape-2"></div>
<div class="shape shape-3"></div> <div class="shape shape-3"></div>
</div> </div>
</div> </div>
</header> </header>
<!-- Main Content --> <!-- Main Content -->
<main class="main-content"> <main class="main-content">
<!-- Input Section --> <!-- Input Section -->
<section class="input-section"> <section class="input-section">
<div class="input-card"> <div class="input-card">
<div class="card-header"> <div class="card-header">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
<h2>输入内容</h2> <h2>输入内容</h2>
</div> </div>
<div class="input-wrapper"> <div class="input-wrapper">
<textarea <textarea
id="inputText" id="inputText"
placeholder="请输入要处理的文本内容...\n支持中文、英文、特殊字符等" placeholder="请输入要处理的文本内容...\n支持中文、英文、特殊字符等"
rows="6" rows="6"
></textarea> ></textarea>
<div class="input-actions"> <div class="input-actions">
<button id="clearBtn" class="btn btn-secondary"> <button id="clearBtn" class="btn btn-secondary">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
清空 清空
</button> </button>
<button id="processBtn" class="btn btn-primary"> <button id="processBtn" class="btn btn-primary">
<i class="fas fa-cogs"></i> <i class="fas fa-cogs"></i>
开始处理 开始处理
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- Results Section --> <!-- Results Section -->
<section class="results-section" id="resultsSection"> <section class="results-section" id="resultsSection">
<div class="results-grid"> <div class="results-grid">
<!-- Hash Results --> <!-- Hash Results -->
<div class="result-card hash-card"> <div class="result-card hash-card">
<div class="card-header"> <div class="card-header">
<i class="fas fa-hashtag"></i> <i class="fas fa-hashtag"></i>
<h3>哈希算法</h3> <h3>哈希算法</h3>
</div> </div>
<div class="result-items"> <div class="result-items">
<div class="result-item"> <div class="result-item">
<label>MD5</label> <label>MD5</label>
<div class="result-value" id="md5Result"> <div class="result-value" id="md5Result">
<span class="placeholder">等待处理...</span> <span class="placeholder">等待处理...</span>
<button class="copy-btn" data-target="md5Result"> <button class="copy-btn" data-target="md5Result">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="result-item"> <div class="result-item">
<label>SHA1</label> <label>SHA1</label>
<div class="result-value" id="sha1Result"> <div class="result-value" id="sha1Result">
<span class="placeholder">等待处理...</span> <span class="placeholder">等待处理...</span>
<button class="copy-btn" data-target="sha1Result"> <button class="copy-btn" data-target="sha1Result">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="result-item"> <div class="result-item">
<label>SHA256</label> <label>SHA256</label>
<div class="result-value" id="sha256Result"> <div class="result-value" id="sha256Result">
<span class="placeholder">等待处理...</span> <span class="placeholder">等待处理...</span>
<button class="copy-btn" data-target="sha256Result"> <button class="copy-btn" data-target="sha256Result">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="result-item"> <div class="result-item">
<label>SHA512</label> <label>SHA512</label>
<div class="result-value" id="sha512Result"> <div class="result-value" id="sha512Result">
<span class="placeholder">等待处理...</span> <span class="placeholder">等待处理...</span>
<button class="copy-btn" data-target="sha512Result"> <button class="copy-btn" data-target="sha512Result">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Encoding Results --> <!-- Encoding Results -->
<div class="result-card encoding-card"> <div class="result-card encoding-card">
<div class="card-header"> <div class="card-header">
<i class="fas fa-code"></i> <i class="fas fa-code"></i>
<h3>编码转换</h3> <h3>编码转换</h3>
</div> </div>
<div class="result-items"> <div class="result-items">
<div class="result-item"> <div class="result-item">
<label>Base64 编码</label> <label>Base64 编码</label>
<div class="result-value" id="base64EncodeResult"> <div class="result-value" id="base64EncodeResult">
<span class="placeholder">等待处理...</span> <span class="placeholder">等待处理...</span>
<button class="copy-btn" data-target="base64EncodeResult"> <button class="copy-btn" data-target="base64EncodeResult">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="result-item"> <div class="result-item">
<label>Base64 解码</label> <label>Base64 解码</label>
<div class="result-value" id="base64DecodeResult"> <div class="result-value" id="base64DecodeResult">
<span class="placeholder">等待处理...</span> <span class="placeholder">等待处理...</span>
<button class="copy-btn" data-target="base64DecodeResult"> <button class="copy-btn" data-target="base64DecodeResult">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="result-item"> <div class="result-item">
<label>URL 编码</label> <label>URL 编码</label>
<div class="result-value" id="urlEncodeResult"> <div class="result-value" id="urlEncodeResult">
<span class="placeholder">等待处理...</span> <span class="placeholder">等待处理...</span>
<button class="copy-btn" data-target="urlEncodeResult"> <button class="copy-btn" data-target="urlEncodeResult">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="result-item"> <div class="result-item">
<label>URL 解码</label> <label>URL 解码</label>
<div class="result-value" id="urlDecodeResult"> <div class="result-value" id="urlDecodeResult">
<span class="placeholder">等待处理...</span> <span class="placeholder">等待处理...</span>
<button class="copy-btn" data-target="urlDecodeResult"> <button class="copy-btn" data-target="urlDecodeResult">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Compression Results --> <!-- Compression Results -->
<div class="result-card compression-card"> <div class="result-card compression-card">
<div class="card-header"> <div class="card-header">
<i class="fas fa-compress-alt"></i> <i class="fas fa-compress-alt"></i>
<h3>压缩算法</h3> <h3>压缩算法</h3>
</div> </div>
<div class="result-items"> <div class="result-items">
<div class="result-item"> <div class="result-item">
<label>Gzip 压缩</label> <label>Gzip 压缩</label>
<div class="result-value" id="gzipCompressResult"> <div class="result-value" id="gzipCompressResult">
<span class="placeholder">等待处理...</span> <span class="placeholder">等待处理...</span>
<button class="copy-btn" data-target="gzipCompressResult"> <button class="copy-btn" data-target="gzipCompressResult">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="result-item"> <div class="result-item">
<label>Deflate 压缩</label> <label>Deflate 压缩</label>
<div class="result-value" id="deflateCompressResult"> <div class="result-value" id="deflateCompressResult">
<span class="placeholder">等待处理...</span> <span class="placeholder">等待处理...</span>
<button class="copy-btn" data-target="deflateCompressResult"> <button class="copy-btn" data-target="deflateCompressResult">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="result-item"> <div class="result-item">
<label>Brotli 压缩</label> <label>Brotli 压缩</label>
<div class="result-value" id="brotliCompressResult"> <div class="result-value" id="brotliCompressResult">
<span class="placeholder">等待处理...</span> <span class="placeholder">等待处理...</span>
<button class="copy-btn" data-target="brotliCompressResult"> <button class="copy-btn" data-target="brotliCompressResult">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</main> </main>
<!-- Loading Overlay --> <!-- Loading Overlay -->
<div class="loading-overlay" id="loadingOverlay"> <div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner"> <div class="loading-spinner">
<div class="spinner"></div> <div class="spinner"></div>
<p>正在处理中...</p> <p>正在处理中...</p>
</div> </div>
</div> </div>
<!-- Toast Notification --> <!-- Toast Notification -->
<div class="toast" id="toast"> <div class="toast" id="toast">
<i class="fas fa-check-circle"></i> <i class="fas fa-check-circle"></i>
<span id="toastMessage">复制成功!</span> <span id="toastMessage">复制成功!</span>
</div> </div>
</div> </div>
<script src="js/script.js"></script> <script src="js/script.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,394 +1,394 @@
// API配置 // API配置
const API_BASE_URL = 'https://60s.api.shumengya.top/v2/hash'; const API_BASE_URL = 'https://60s.api.shumengya.top/v2/hash';
// DOM元素 // DOM元素
const elements = { const elements = {
inputText: document.getElementById('inputText'), inputText: document.getElementById('inputText'),
processBtn: document.getElementById('processBtn'), processBtn: document.getElementById('processBtn'),
clearBtn: document.getElementById('clearBtn'), clearBtn: document.getElementById('clearBtn'),
resultsSection: document.getElementById('resultsSection'), resultsSection: document.getElementById('resultsSection'),
loadingOverlay: document.getElementById('loadingOverlay'), loadingOverlay: document.getElementById('loadingOverlay'),
toast: document.getElementById('toast'), toast: document.getElementById('toast'),
toastMessage: document.getElementById('toastMessage') toastMessage: document.getElementById('toastMessage')
}; };
// 结果元素映射 // 结果元素映射
const resultElements = { const resultElements = {
md5: document.getElementById('md5Result'), md5: document.getElementById('md5Result'),
sha1: document.getElementById('sha1Result'), sha1: document.getElementById('sha1Result'),
sha256: document.getElementById('sha256Result'), sha256: document.getElementById('sha256Result'),
sha512: document.getElementById('sha512Result'), sha512: document.getElementById('sha512Result'),
base64Encode: document.getElementById('base64EncodeResult'), base64Encode: document.getElementById('base64EncodeResult'),
base64Decode: document.getElementById('base64DecodeResult'), base64Decode: document.getElementById('base64DecodeResult'),
urlEncode: document.getElementById('urlEncodeResult'), urlEncode: document.getElementById('urlEncodeResult'),
urlDecode: document.getElementById('urlDecodeResult'), urlDecode: document.getElementById('urlDecodeResult'),
gzipCompress: document.getElementById('gzipCompressResult'), gzipCompress: document.getElementById('gzipCompressResult'),
deflateCompress: document.getElementById('deflateCompressResult'), deflateCompress: document.getElementById('deflateCompressResult'),
brotliCompress: document.getElementById('brotliCompressResult') brotliCompress: document.getElementById('brotliCompressResult')
}; };
// 初始化 // 初始化
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
initializeEventListeners(); initializeEventListeners();
addInputAnimation(); addInputAnimation();
}); });
// 事件监听器初始化 // 事件监听器初始化
function initializeEventListeners() { function initializeEventListeners() {
// 处理按钮点击 // 处理按钮点击
elements.processBtn.addEventListener('click', handleProcess); elements.processBtn.addEventListener('click', handleProcess);
// 清空按钮点击 // 清空按钮点击
elements.clearBtn.addEventListener('click', handleClear); elements.clearBtn.addEventListener('click', handleClear);
// 输入框回车键 // 输入框回车键
elements.inputText.addEventListener('keydown', function(e) { elements.inputText.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'Enter') { if (e.ctrlKey && e.key === 'Enter') {
handleProcess(); handleProcess();
} }
}); });
// 复制按钮事件委托 // 复制按钮事件委托
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {
if (e.target.closest('.copy-btn')) { if (e.target.closest('.copy-btn')) {
const copyBtn = e.target.closest('.copy-btn'); const copyBtn = e.target.closest('.copy-btn');
const targetId = copyBtn.getAttribute('data-target'); const targetId = copyBtn.getAttribute('data-target');
const targetElement = document.getElementById(targetId); const targetElement = document.getElementById(targetId);
const textContent = targetElement.textContent.trim(); const textContent = targetElement.textContent.trim();
if (textContent && textContent !== '等待处理...' && textContent !== '处理失败') { if (textContent && textContent !== '等待处理...' && textContent !== '处理失败') {
copyToClipboard(textContent); copyToClipboard(textContent);
} }
} }
}); });
// 输入框实时验证 // 输入框实时验证
elements.inputText.addEventListener('input', function() { elements.inputText.addEventListener('input', function() {
const hasContent = this.value.trim().length > 0; const hasContent = this.value.trim().length > 0;
elements.processBtn.disabled = !hasContent; elements.processBtn.disabled = !hasContent;
if (hasContent) { if (hasContent) {
elements.processBtn.classList.remove('disabled'); elements.processBtn.classList.remove('disabled');
} else { } else {
elements.processBtn.classList.add('disabled'); elements.processBtn.classList.add('disabled');
} }
}); });
} }
// 添加输入动画效果 // 添加输入动画效果
function addInputAnimation() { function addInputAnimation() {
elements.inputText.addEventListener('focus', function() { elements.inputText.addEventListener('focus', function() {
this.parentElement.classList.add('focused'); this.parentElement.classList.add('focused');
}); });
elements.inputText.addEventListener('blur', function() { elements.inputText.addEventListener('blur', function() {
this.parentElement.classList.remove('focused'); this.parentElement.classList.remove('focused');
}); });
} }
// 处理主要功能 // 处理主要功能
async function handleProcess() { async function handleProcess() {
const inputValue = elements.inputText.value.trim(); const inputValue = elements.inputText.value.trim();
if (!inputValue) { if (!inputValue) {
showToast('请输入要处理的内容', 'error'); showToast('请输入要处理的内容', 'error');
return; return;
} }
// 显示加载状态 // 显示加载状态
showLoading(true); showLoading(true);
resetResults(); resetResults();
try { try {
// 调用API // 调用API
const response = await fetch(`${API_BASE_URL}?content=${encodeURIComponent(inputValue)}`); const response = await fetch(`${API_BASE_URL}?content=${encodeURIComponent(inputValue)}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
if (data.code === 200 && data.data) { if (data.code === 200 && data.data) {
displayResults(data.data); displayResults(data.data);
showResultsSection(); showResultsSection();
showToast('处理完成!', 'success'); showToast('处理完成!', 'success');
} else { } else {
throw new Error(data.message || '处理失败'); throw new Error(data.message || '处理失败');
} }
} catch (error) { } catch (error) {
console.error('处理错误:', error); console.error('处理错误:', error);
showToast(`处理失败: ${error.message}`, 'error'); showToast(`处理失败: ${error.message}`, 'error');
displayError(); displayError();
} finally { } finally {
showLoading(false); showLoading(false);
} }
} }
// 显示结果 // 显示结果
function displayResults(data) { function displayResults(data) {
try { try {
// 哈希结果 // 哈希结果
updateResultElement('md5', data.md5 || '不可用'); updateResultElement('md5', data.md5 || '不可用');
// SHA系列 // SHA系列
if (data.sha) { if (data.sha) {
updateResultElement('sha1', data.sha.sha1 || '不可用'); updateResultElement('sha1', data.sha.sha1 || '不可用');
updateResultElement('sha256', data.sha.sha256 || '不可用'); updateResultElement('sha256', data.sha.sha256 || '不可用');
updateResultElement('sha512', data.sha.sha512 || '不可用'); updateResultElement('sha512', data.sha.sha512 || '不可用');
} }
// Base64编码 // Base64编码
if (data.base64) { if (data.base64) {
updateResultElement('base64Encode', data.base64.encoded || '不可用'); updateResultElement('base64Encode', data.base64.encoded || '不可用');
// BASE64解码只有当输入本身是BASE64格式时才显示解码结果 // BASE64解码只有当输入本身是BASE64格式时才显示解码结果
let base64DecodeResult = data.base64.decoded; let base64DecodeResult = data.base64.decoded;
if (!base64DecodeResult) { if (!base64DecodeResult) {
// 检查输入是否为有效的BASE64格式 // 检查输入是否为有效的BASE64格式
const inputValue = elements.inputText.value.trim(); const inputValue = elements.inputText.value.trim();
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/; const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
if (base64Regex.test(inputValue) && inputValue.length % 4 === 0) { if (base64Regex.test(inputValue) && inputValue.length % 4 === 0) {
try { try {
base64DecodeResult = atob(inputValue); base64DecodeResult = atob(inputValue);
} catch (e) { } catch (e) {
base64DecodeResult = '解码失败'; base64DecodeResult = '解码失败';
} }
} else { } else {
base64DecodeResult = '输入非BASE64格式'; base64DecodeResult = '输入非BASE64格式';
} }
} }
updateResultElement('base64Decode', base64DecodeResult || '不可用'); updateResultElement('base64Decode', base64DecodeResult || '不可用');
} }
// URL编码 // URL编码
if (data.url) { if (data.url) {
updateResultElement('urlEncode', data.url.encoded || '不可用'); updateResultElement('urlEncode', data.url.encoded || '不可用');
updateResultElement('urlDecode', data.url.decoded || '不可用'); updateResultElement('urlDecode', data.url.decoded || '不可用');
} }
// 压缩结果(仅显示压缩,不显示解压) // 压缩结果(仅显示压缩,不显示解压)
if (data.gzip) { if (data.gzip) {
updateResultElement('gzipCompress', data.gzip.encoded || '不可用'); updateResultElement('gzipCompress', data.gzip.encoded || '不可用');
} }
if (data.deflate) { if (data.deflate) {
updateResultElement('deflateCompress', data.deflate.encoded || '不可用'); updateResultElement('deflateCompress', data.deflate.encoded || '不可用');
} }
if (data.brotli) { if (data.brotli) {
updateResultElement('brotliCompress', data.brotli.encoded || '不可用'); updateResultElement('brotliCompress', data.brotli.encoded || '不可用');
} }
} catch (error) { } catch (error) {
console.error('显示结果时出错:', error); console.error('显示结果时出错:', error);
showToast('显示结果时出错', 'error'); showToast('显示结果时出错', 'error');
} }
} }
// 更新单个结果元素 // 更新单个结果元素
function updateResultElement(key, value) { function updateResultElement(key, value) {
const element = resultElements[key]; const element = resultElements[key];
if (element) { if (element) {
const textSpan = element.querySelector('span') || element; const textSpan = element.querySelector('span') || element;
textSpan.textContent = value; textSpan.textContent = value;
textSpan.classList.remove('placeholder'); textSpan.classList.remove('placeholder');
// 添加动画效果 // 添加动画效果
element.classList.add('slide-in'); element.classList.add('slide-in');
setTimeout(() => { setTimeout(() => {
element.classList.remove('slide-in'); element.classList.remove('slide-in');
}, 300); }, 300);
} }
} }
// 重置结果 // 重置结果
function resetResults() { function resetResults() {
Object.values(resultElements).forEach(element => { Object.values(resultElements).forEach(element => {
if (element) { if (element) {
const textSpan = element.querySelector('span') || element; const textSpan = element.querySelector('span') || element;
textSpan.textContent = '等待处理...'; textSpan.textContent = '等待处理...';
textSpan.classList.add('placeholder'); textSpan.classList.add('placeholder');
} }
}); });
} }
// 显示错误状态 // 显示错误状态
function displayError() { function displayError() {
Object.values(resultElements).forEach(element => { Object.values(resultElements).forEach(element => {
if (element) { if (element) {
const textSpan = element.querySelector('span') || element; const textSpan = element.querySelector('span') || element;
textSpan.textContent = '处理失败'; textSpan.textContent = '处理失败';
textSpan.classList.add('placeholder'); textSpan.classList.add('placeholder');
} }
}); });
} }
// 显示结果区域 // 显示结果区域
function showResultsSection() { function showResultsSection() {
elements.resultsSection.classList.add('show'); elements.resultsSection.classList.add('show');
// 平滑滚动到结果区域 // 平滑滚动到结果区域
setTimeout(() => { setTimeout(() => {
elements.resultsSection.scrollIntoView({ elements.resultsSection.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'start' block: 'start'
}); });
}, 100); }, 100);
} }
// 清空功能 // 清空功能
function handleClear() { function handleClear() {
elements.inputText.value = ''; elements.inputText.value = '';
elements.inputText.focus(); elements.inputText.focus();
elements.resultsSection.classList.remove('show'); elements.resultsSection.classList.remove('show');
resetResults(); resetResults();
elements.processBtn.disabled = true; elements.processBtn.disabled = true;
elements.processBtn.classList.add('disabled'); elements.processBtn.classList.add('disabled');
showToast('内容已清空', 'info'); showToast('内容已清空', 'info');
} }
// 复制到剪贴板 // 复制到剪贴板
async function copyToClipboard(text) { async function copyToClipboard(text) {
try { try {
if (navigator.clipboard && window.isSecureContext) { if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
} else { } else {
// 降级方案 // 降级方案
const textArea = document.createElement('textarea'); const textArea = document.createElement('textarea');
textArea.value = text; textArea.value = text;
textArea.style.position = 'fixed'; textArea.style.position = 'fixed';
textArea.style.left = '-999999px'; textArea.style.left = '-999999px';
textArea.style.top = '-999999px'; textArea.style.top = '-999999px';
document.body.appendChild(textArea); document.body.appendChild(textArea);
textArea.focus(); textArea.focus();
textArea.select(); textArea.select();
document.execCommand('copy'); document.execCommand('copy');
textArea.remove(); textArea.remove();
} }
showToast('复制成功!', 'success'); showToast('复制成功!', 'success');
} catch (error) { } catch (error) {
console.error('复制失败:', error); console.error('复制失败:', error);
showToast('复制失败,请手动复制', 'error'); showToast('复制失败,请手动复制', 'error');
} }
} }
// 显示/隐藏加载状态 // 显示/隐藏加载状态
function showLoading(show) { function showLoading(show) {
if (show) { if (show) {
elements.loadingOverlay.classList.add('show'); elements.loadingOverlay.classList.add('show');
elements.processBtn.disabled = true; elements.processBtn.disabled = true;
elements.processBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 处理中...'; elements.processBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 处理中...';
} else { } else {
elements.loadingOverlay.classList.remove('show'); elements.loadingOverlay.classList.remove('show');
elements.processBtn.disabled = false; elements.processBtn.disabled = false;
elements.processBtn.innerHTML = '<i class="fas fa-cogs"></i> 开始处理'; elements.processBtn.innerHTML = '<i class="fas fa-cogs"></i> 开始处理';
} }
} }
// 显示提示消息 // 显示提示消息
function showToast(message, type = 'success') { function showToast(message, type = 'success') {
elements.toastMessage.textContent = message; elements.toastMessage.textContent = message;
// 设置图标和样式 // 设置图标和样式
const icon = elements.toast.querySelector('i'); const icon = elements.toast.querySelector('i');
icon.className = getToastIcon(type); icon.className = getToastIcon(type);
elements.toast.className = `toast ${type}`; elements.toast.className = `toast ${type}`;
elements.toast.classList.add('show'); elements.toast.classList.add('show');
// 自动隐藏 // 自动隐藏
setTimeout(() => { setTimeout(() => {
elements.toast.classList.remove('show'); elements.toast.classList.remove('show');
}, 3000); }, 3000);
} }
// 获取提示图标 // 获取提示图标
function getToastIcon(type) { function getToastIcon(type) {
const icons = { const icons = {
success: 'fas fa-check-circle', success: 'fas fa-check-circle',
error: 'fas fa-exclamation-circle', error: 'fas fa-exclamation-circle',
info: 'fas fa-info-circle', info: 'fas fa-info-circle',
warning: 'fas fa-exclamation-triangle' warning: 'fas fa-exclamation-triangle'
}; };
return icons[type] || icons.success; return icons[type] || icons.success;
} }
// 工具函数:防抖 // 工具函数:防抖
function debounce(func, wait) { function debounce(func, wait) {
let timeout; let timeout;
return function executedFunction(...args) { return function executedFunction(...args) {
const later = () => { const later = () => {
clearTimeout(timeout); clearTimeout(timeout);
func(...args); func(...args);
}; };
clearTimeout(timeout); clearTimeout(timeout);
timeout = setTimeout(later, wait); timeout = setTimeout(later, wait);
}; };
} }
// 工具函数:节流 // 工具函数:节流
function throttle(func, limit) { function throttle(func, limit) {
let inThrottle; let inThrottle;
return function() { return function() {
const args = arguments; const args = arguments;
const context = this; const context = this;
if (!inThrottle) { if (!inThrottle) {
func.apply(context, args); func.apply(context, args);
inThrottle = true; inThrottle = true;
setTimeout(() => inThrottle = false, limit); setTimeout(() => inThrottle = false, limit);
} }
} }
} }
// 添加键盘快捷键支持 // 添加键盘快捷键支持
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
// Ctrl+Enter 处理 // Ctrl+Enter 处理
if (e.ctrlKey && e.key === 'Enter') { if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
if (!elements.processBtn.disabled) { if (!elements.processBtn.disabled) {
handleProcess(); handleProcess();
} }
} }
// Escape 清空 // Escape 清空
if (e.key === 'Escape') { if (e.key === 'Escape') {
handleClear(); handleClear();
} }
}); });
// 页面可见性变化处理 // 页面可见性变化处理
document.addEventListener('visibilitychange', function() { document.addEventListener('visibilitychange', function() {
if (document.hidden) { if (document.hidden) {
// 页面隐藏时的处理 // 页面隐藏时的处理
console.log('页面已隐藏'); console.log('页面已隐藏');
} else { } else {
// 页面显示时的处理 // 页面显示时的处理
console.log('页面已显示'); console.log('页面已显示');
} }
}); });
// 错误处理 // 错误处理
window.addEventListener('error', function(e) { window.addEventListener('error', function(e) {
console.error('全局错误:', e.error); console.error('全局错误:', e.error);
showToast('发生未知错误,请刷新页面重试', 'error'); showToast('发生未知错误,请刷新页面重试', 'error');
}); });
// 未处理的Promise拒绝 // 未处理的Promise拒绝
window.addEventListener('unhandledrejection', function(e) { window.addEventListener('unhandledrejection', function(e) {
console.error('未处理的Promise拒绝:', e.reason); console.error('未处理的Promise拒绝:', e.reason);
showToast('网络请求失败,请检查网络连接', 'error'); showToast('网络请求失败,请检查网络连接', 'error');
}); });
// 导出函数供测试使用 // 导出函数供测试使用
if (typeof module !== 'undefined' && module.exports) { if (typeof module !== 'undefined' && module.exports) {
module.exports = { module.exports = {
handleProcess, handleProcess,
copyToClipboard, copyToClipboard,
showToast, showToast,
debounce, debounce,
throttle throttle
}; };
} }

View File

@@ -1,3 +1,3 @@
[ [
"https://60s.api.shumengya.top" "https://60s.api.shumengya.top"
] ]

View File

@@ -1,35 +1,35 @@
{ {
"code": 200, "code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841", "message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": { "data": {
"source": "hello", "source": "hello",
"md5": "5d41402abc4b2a76b9719d911017c592", "md5": "5d41402abc4b2a76b9719d911017c592",
"sha": { "sha": {
"sha1": "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d", "sha1": "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d",
"sha256": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", "sha256": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
"sha512": "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043" "sha512": "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"
}, },
"base64": { "base64": {
"encoded": "aGVsbG8=", "encoded": "aGVsbG8=",
"decoded": "" "decoded": ""
}, },
"url": { "url": {
"encoded": "hello", "encoded": "hello",
"decoded": "hello" "decoded": "hello"
}, },
"gzip": { "gzip": {
"encoded": "1f8b0800000000000003cb48cdc9c9070086a6103605000000", "encoded": "1f8b0800000000000003cb48cdc9c9070086a6103605000000",
"decoded": "" "decoded": ""
}, },
"deflate": { "deflate": {
"encoded": "789ccb48cdc9c90700062c0215", "encoded": "789ccb48cdc9c90700062c0215",
"decoded": "" "decoded": ""
}, },
"brotli": { "brotli": {
"encoded": "0b028068656c6c6f03", "encoded": "0b028068656c6c6f03",
"decoded": "" "decoded": ""
} }
} }
} }
API encoded/decoded encode/decode API encoded/decoded encode/decode

View File

@@ -1,137 +1,137 @@
/* 背景样式文件 */ /* 背景样式文件 */
/* 页面主背景 */ /* 页面主背景 */
body { body {
background: linear-gradient(135deg, #e8f5e8 0%, #f0fdf4 25%, #dcfce7 50%, #f0fdf4 75%, #e8f5e8 100%); background: linear-gradient(135deg, #e8f5e8 0%, #f0fdf4 25%, #dcfce7 50%, #f0fdf4 75%, #e8f5e8 100%);
background-size: 400% 400%; background-size: 400% 400%;
animation: gradientShift 15s ease infinite; animation: gradientShift 15s ease infinite;
background-attachment: fixed; background-attachment: fixed;
} }
/* 背景动画 */ /* 背景动画 */
@keyframes gradientShift { @keyframes gradientShift {
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
} }
/* 容器背景 */ /* 容器背景 */
.container { .container {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-radius: 20px; border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
} }
/* 翻译框背景 */ /* 翻译框背景 */
.translate-box { .translate-box {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
border: 1px solid rgba(116, 198, 157, 0.3); border: 1px solid rgba(116, 198, 157, 0.3);
} }
/* 输入框背景 */ /* 输入框背景 */
#input-text { #input-text {
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
} }
#input-text:focus { #input-text:focus {
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
} }
/* 输出框背景 */ /* 输出框背景 */
.output-text { .output-text {
background: #f8fffe; background: #f8fffe;
} }
/* 按钮背景 */ /* 按钮背景 */
.translate-btn { .translate-btn {
background: linear-gradient(135deg, #74c69d, #52b788); background: linear-gradient(135deg, #74c69d, #52b788);
} }
.translate-btn:hover { .translate-btn:hover {
background: linear-gradient(135deg, #52b788, #40916c); background: linear-gradient(135deg, #52b788, #40916c);
} }
.translate-btn:disabled { .translate-btn:disabled {
background: #b7e4c7; background: #b7e4c7;
} }
.swap-btn { .swap-btn {
background: #74c69d; background: #74c69d;
} }
.swap-btn:hover { .swap-btn:hover {
background: #52b788; background: #52b788;
} }
/* 语言选择器背景 */ /* 语言选择器背景 */
.lang-select { .lang-select {
background: white; background: white;
} }
.lang-select:focus { .lang-select:focus {
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
} }
/* 发音信息背景 */ /* 发音信息背景 */
.pronounce-item { .pronounce-item {
background: rgba(116, 198, 157, 0.1); background: rgba(116, 198, 157, 0.1);
} }
/* 清除和复制按钮背景 */ /* 清除和复制按钮背景 */
.clear-btn:hover, .clear-btn:hover,
.copy-btn:hover { .copy-btn:hover {
background: rgba(116, 198, 157, 0.1); background: rgba(116, 198, 157, 0.1);
} }
/* 提示消息背景 */ /* 提示消息背景 */
.toast { .toast {
background: #52b788; background: #52b788;
} }
.toast.error { .toast.error {
background: #e74c3c; background: #e74c3c;
} }
/* 响应式背景调整 */ /* 响应式背景调整 */
@media (max-width: 767px) { @media (max-width: 767px) {
body { body {
background-size: 200% 200%; background-size: 200% 200%;
animation-duration: 10s; animation-duration: 10s;
} }
.container { .container {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
} }
.translate-box { .translate-box {
background: rgba(255, 255, 255, 0.98); background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(15px); backdrop-filter: blur(15px);
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
body { body {
background-size: 150% 150%; background-size: 150% 150%;
animation-duration: 8s; animation-duration: 8s;
} }
.container { .container {
background: transparent; background: transparent;
backdrop-filter: none; backdrop-filter: none;
border: none; border: none;
} }
.translate-box { .translate-box {
background: rgba(255, 255, 255, 0.99); background: rgba(255, 255, 255, 0.99);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
} }

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