chore: sync local changes (2026-03-12)
This commit is contained in:
@@ -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
5
.gitignore
vendored
@@ -1,5 +0,0 @@
|
|||||||
#项目自忽略
|
|
||||||
.vscode
|
|
||||||
InfoGenie-frontend/node_modules
|
|
||||||
InfoGenie-frontend/build
|
|
||||||
InfoGenie-backend/__pycache__
|
|
||||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"git.ignoreLimitWarning": true,
|
"git.ignoreLimitWarning": true,
|
||||||
"terminal.integrated.defaultProfile.windows": "Command Prompt"
|
"terminal.integrated.defaultProfile.windows": "Command Prompt"
|
||||||
}
|
}
|
||||||
57
Dockerfile
57
Dockerfile
@@ -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"]
|
|
||||||
19
InfoGenie-backend/.dockerignore
Normal file
19
InfoGenie-backend/.dockerignore
Normal 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
3
InfoGenie-backend/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#项目自忽略
|
||||||
|
.vscode
|
||||||
|
__pycache__
|
||||||
30
InfoGenie-backend/Dockerfile
Normal file
30
InfoGenie-backend/Dockerfile
Normal 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"]
|
||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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接口==========================
|
||||||
@@ -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
@@ -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限流
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
@echo off
|
@echo off
|
||||||
python app.py
|
python app.py
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
python3 app.py
|
python3 app.py
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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❌ 所有连接尝试都失败了")
|
|
||||||
@@ -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('✅ 测试完成!')
|
|
||||||
@@ -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()
|
|
||||||
@@ -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测试完成!")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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('✅ 测试完成!')
|
|
||||||
@@ -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使用记录和萌芽币消费情况
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@echo off
|
@echo off
|
||||||
npm run build
|
npm run build
|
||||||
npx serve -s build
|
npx serve -s build
|
||||||
pause
|
pause
|
||||||
@@ -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
|
||||||
41400
InfoGenie-frontend/package-lock.json
generated
41400
InfoGenie-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
|||||||
[
|
[
|
||||||
"https://60s.api.shumengya.top"
|
"https://60s.api.shumengya.top"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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` 实例都需要这个方法。这将浪费大量内存空间,因为它们仍然具有该属性,这将占用每个实例的内存空间。相反,如果我们只将它添加到原型中,那么它只存在于内存中的一个位置,但是所有实例都可以访问它!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
[
|
[
|
||||||
"https://60s.api.shumengya.top"
|
"https://60s.api.shumengya.top"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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 吃疯狂星期四 然后再给我点杯奶茶 再给我两万块钱 懂吗你们"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
[
|
[
|
||||||
"https://60s.api.shumengya.top"
|
"https://60s.api.shumengya.top"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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": "你带上罪恶之冠,即使背负上所有罪恶和孤独,绝不让你受伤"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>© 2024 冷笑话工坊</p>
|
<p>© 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>
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
@@ -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": "这个世界上谁最懂猪?蜘蛛(知猪)人。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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": "我不想读书,主要是因为家里牛啊,猪啊羊啊都没人喂。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
[
|
[
|
||||||
"https://60s.api.shumengya.top"
|
"https://60s.api.shumengya.top"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
@@ -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": "我不想读书,主要是因为家里牛啊,猪啊羊啊都没人喂。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
[
|
[
|
||||||
"https://60s.api.shumengya.top"
|
"https://60s.api.shumengya.top"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
[
|
[
|
||||||
"https://60s.api.shumengya.top"
|
"https://60s.api.shumengya.top"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
[
|
[
|
||||||
"https://60s.api.shumengya.top/v2/ip"
|
"https://60s.api.shumengya.top/v2/ip"
|
||||||
]
|
]
|
||||||
@@ -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服务
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
[
|
[
|
||||||
"https://60s.api.shumengya.top"
|
"https://60s.api.shumengya.top"
|
||||||
]
|
]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
[
|
[
|
||||||
"https://60s.api.shumengya.top"
|
"https://60s.api.shumengya.top"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
Reference in New Issue
Block a user