Compare commits
27 Commits
master
...
7786e5f507
| Author | SHA1 | Date | |
|---|---|---|---|
| 7786e5f507 | |||
| 98c6371c4e | |||
| 887f6fb5d6 | |||
| 5613cdd6c9 | |||
| dd3f702887 | |||
| 174d8a3bc5 | |||
| 17691af8d1 | |||
| 249e434b72 | |||
| dcfa89e63c | |||
| 72084a8782 | |||
| 2b5a6740d4 | |||
| 44a4f1bae1 | |||
| 6889ca37e5 | |||
| 227ccf9c29 | |||
| 261fb6edb3 | |||
| c2b2e84416 | |||
|
|
0a8b20e450 | ||
|
|
bd4c7439be | ||
| 6f850caad1 | |||
|
|
71a648fdf4 | ||
|
|
b8456c437a | ||
|
|
6829a16f96 | ||
| eb13979d4d | |||
|
|
cca1a969c5 | ||
|
|
ebd66145c8 | ||
|
|
e62a2d7127 | ||
|
|
dd43157e09 |
214
.gitignore
vendored
Normal file → Executable file
214
.gitignore
vendored
Normal file → Executable file
@@ -1,209 +1,5 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
#poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
#pdm.lock
|
||||
#pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
#pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Cursor
|
||||
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
frontend/react-app/node_modules/
|
||||
#项目自忽略
|
||||
.vscode
|
||||
InfoGenie-frontend/node_modules
|
||||
InfoGenie-frontend/build
|
||||
InfoGenie-backend/__pycache__
|
||||
3
.vscode/settings.json
vendored
Normal file → Executable file
3
.vscode/settings.json
vendored
Normal file → Executable file
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"git.ignoreLimitWarning": true
|
||||
"git.ignoreLimitWarning": true,
|
||||
"terminal.integrated.defaultProfile.windows": "Command Prompt"
|
||||
}
|
||||
50
InfoGenie-backend/.dockerignore
Normal file
50
InfoGenie-backend/.dockerignore
Normal file
@@ -0,0 +1,50 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
.venv/
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.pytest_cache/
|
||||
|
||||
# 环境变量文件
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
|
||||
# 测试文件(可选,如果不想包含在镜像中)
|
||||
test/
|
||||
|
||||
# 文档文件(可选)
|
||||
*.md
|
||||
LICENSE
|
||||
|
||||
# 启动脚本(Windows)
|
||||
*.bat
|
||||
|
||||
# 其他临时文件
|
||||
*.tmp
|
||||
.cache/
|
||||
16
InfoGenie-backend/.env
Executable file
16
InfoGenie-backend/.env
Executable file
@@ -0,0 +1,16 @@
|
||||
# InfoGenie 环境变量配置文件
|
||||
# 请勿将此文件提交到版本控制系统
|
||||
|
||||
# 邮件配置
|
||||
# 请将下面的邮箱地址替换为您的实际QQ邮箱
|
||||
MAIL_USERNAME=3205788256@qq.com
|
||||
MAIL_PASSWORD=szcaxvbftusqddhi
|
||||
|
||||
# 数据库配置
|
||||
MONGO_URI=mongodb://shumengya:tyh%4019900420@47.108.90.0:27018/InfoGenie?authSource=admin
|
||||
|
||||
# 应用密钥
|
||||
SECRET_KEY=infogenie-secret-key-2025
|
||||
|
||||
# 环境配置
|
||||
FLASK_ENV=development
|
||||
14
InfoGenie-backend/.env.production
Executable file
14
InfoGenie-backend/.env.production
Executable file
@@ -0,0 +1,14 @@
|
||||
# 生产环境配置
|
||||
|
||||
# MongoDB配置
|
||||
MONGO_URI=mongodb://用户名:密码@主机地址:端口/InfoGenie?authSource=admin
|
||||
|
||||
# 邮件配置
|
||||
MAIL_USERNAME=your-email@qq.com
|
||||
MAIL_PASSWORD=your-app-password
|
||||
|
||||
# 应用密钥
|
||||
SECRET_KEY=infogenie-production-secret-key-2025
|
||||
|
||||
# 会话安全配置
|
||||
HWT_SECURE=True
|
||||
32
InfoGenie-backend/Dockerfile
Normal file
32
InfoGenie-backend/Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
# 使用官方Python镜像作为基础镜像
|
||||
FROM python:3.10-slim
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖(如果需要)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制requirements.txt并安装Python依赖
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制应用代码
|
||||
COPY . .
|
||||
|
||||
# 创建非root用户(安全最佳实践)
|
||||
RUN useradd --create-home --shell /bin/bash app \
|
||||
&& chown -R app:app /app
|
||||
USER app
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 5002
|
||||
|
||||
# 设置环境变量
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_ENV=production
|
||||
|
||||
# 启动命令
|
||||
CMD ["python", "app.py"]
|
||||
13
InfoGenie-backend/ai_config.json
Executable file
13
InfoGenie-backend/ai_config.json
Executable file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"deepseek": {
|
||||
"api_key": "sk-832f8e5250464de08a31523c7fd71295",
|
||||
"api_base": "https://api.deepseek.com",
|
||||
"model": ["deepseek-chat","deepseek-reasoner"]
|
||||
},
|
||||
|
||||
"kimi": {
|
||||
"api_key": "sk-zdg9NBpTlhOcDDpoWfaBKu0KNDdGv18SipORnL2utawja0bE",
|
||||
"api_base": "https://api.moonshot.cn",
|
||||
"model": ["kimi-k2-0905-preview","kimi-k2-0711-preview"]
|
||||
}
|
||||
}
|
||||
75
backend/app.py → InfoGenie-backend/app.py
Normal file → Executable file
75
backend/app.py → InfoGenie-backend/app.py
Normal file → Executable file
@@ -2,11 +2,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
InfoGenie 后端主应用入口
|
||||
Created by: 神奇万事通
|
||||
Created by: 万象口袋
|
||||
Date: 2025-09-02
|
||||
"""
|
||||
|
||||
from flask import Flask, jsonify, request, session, send_from_directory
|
||||
from flask import Flask, jsonify, request, send_from_directory
|
||||
from flask_cors import CORS
|
||||
from flask_pymongo import PyMongo
|
||||
import os
|
||||
@@ -16,12 +16,13 @@ import secrets
|
||||
|
||||
# 导入模块
|
||||
from modules.auth import auth_bp
|
||||
from modules.api_60s import api_60s_bp
|
||||
from modules.user_management import user_bp
|
||||
from modules.email_service import init_mail
|
||||
from modules.aimodelapp import aimodelapp_bp
|
||||
|
||||
from config import Config
|
||||
|
||||
# 创建Flask应用
|
||||
def create_app():
|
||||
"""创建Flask应用实例"""
|
||||
app = Flask(__name__)
|
||||
@@ -29,7 +30,7 @@ def create_app():
|
||||
# 加载配置
|
||||
app.config.from_object(Config)
|
||||
|
||||
# 启用CORS跨域支持
|
||||
# 启用CORS跨域支持(允许所有源)
|
||||
CORS(app, supports_credentials=True)
|
||||
|
||||
# 初始化MongoDB
|
||||
@@ -41,21 +42,23 @@ def create_app():
|
||||
|
||||
# 注册蓝图
|
||||
app.register_blueprint(auth_bp, url_prefix='/api/auth')
|
||||
app.register_blueprint(api_60s_bp, url_prefix='/api/60s')
|
||||
app.register_blueprint(user_bp, url_prefix='/api/user')
|
||||
app.register_blueprint(aimodelapp_bp, url_prefix='/api/aimodelapp')
|
||||
|
||||
# 基础路由
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""API根路径"""
|
||||
return jsonify({
|
||||
'message': '✨ 神奇万事通 API 服务运行中 ✨',
|
||||
'message': '✨ 万象口袋 API 服务运行中 ✨',
|
||||
'version': '1.0.0',
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'endpoints': {
|
||||
'auth': '/api/auth',
|
||||
'60s_api': '/api/60s',
|
||||
'user': '/api/user'
|
||||
'user': '/api/user',
|
||||
'smallgame': '/api/smallgame',
|
||||
'aimodelapp': '/api/aimodelapp'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -102,6 +105,60 @@ def create_app():
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'文件服务错误: {str(e)}'}), 500
|
||||
|
||||
# smallgame静态文件服务
|
||||
@app.route('/smallgame/<path:filename>')
|
||||
def serve_smallgame_files(filename):
|
||||
"""提供smallgame目录下的静态文件服务"""
|
||||
try:
|
||||
# 获取项目根目录
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
game_directory = os.path.join(project_root, 'frontend', 'smallgame')
|
||||
|
||||
# 安全检查:确保文件路径在允许的目录内
|
||||
full_path = os.path.join(game_directory, filename)
|
||||
if not os.path.commonpath([game_directory, full_path]) == game_directory:
|
||||
return jsonify({'error': '非法文件路径'}), 403
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(full_path):
|
||||
return jsonify({'error': '文件不存在'}), 404
|
||||
|
||||
# 获取文件目录和文件名
|
||||
directory = os.path.dirname(full_path)
|
||||
file_name = os.path.basename(full_path)
|
||||
|
||||
return send_from_directory(directory, file_name)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'文件服务错误: {str(e)}'}), 500
|
||||
|
||||
# aimodelapp静态文件服务
|
||||
@app.route('/aimodelapp/<path:filename>')
|
||||
def serve_aimodelapp_files(filename):
|
||||
"""提供aimodelapp目录下的静态文件服务"""
|
||||
try:
|
||||
# 获取项目根目录
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
ai_directory = os.path.join(project_root, 'frontend', 'public', 'aimodelapp')
|
||||
|
||||
# 安全检查:确保文件路径在允许的目录内
|
||||
full_path = os.path.join(ai_directory, filename)
|
||||
if not os.path.commonpath([ai_directory, full_path]) == ai_directory:
|
||||
return jsonify({'error': '非法文件路径'}), 403
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(full_path):
|
||||
return jsonify({'error': '文件不存在'}), 404
|
||||
|
||||
# 获取文件目录和文件名
|
||||
directory = os.path.dirname(full_path)
|
||||
file_name = os.path.basename(full_path)
|
||||
|
||||
return send_from_directory(directory, file_name)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'文件服务错误: {str(e)}'}), 500
|
||||
|
||||
# 错误处理
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
@@ -122,6 +179,4 @@ def create_app():
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
print("🚀 启动 InfoGenie 后端服务...")
|
||||
print("📡 API地址: http://localhost:5000")
|
||||
print("📚 文档地址: http://localhost:5000/api/health")
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
app.run(debug=True, host='0.0.0.0', port=5002)
|
||||
118
InfoGenie-backend/build_docker.sh
Executable file
118
InfoGenie-backend/build_docker.sh
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
|
||||
# InfoGenie 后端 Docker 镜像构建脚本
|
||||
# Created by: 万象口袋
|
||||
# Date: 2025-09-16
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 配置
|
||||
IMAGE_NAME="infogenie-backend"
|
||||
IMAGE_TAG="latest"
|
||||
DOCKERFILE_PATH="."
|
||||
|
||||
# 函数:打印信息
|
||||
print_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 检查Docker是否安装
|
||||
check_docker() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
print_error "Docker 未安装,请先安装 Docker"
|
||||
exit 1
|
||||
fi
|
||||
print_info "Docker 版本: $(docker --version)"
|
||||
}
|
||||
|
||||
# 检查Dockerfile是否存在
|
||||
check_dockerfile() {
|
||||
if [ ! -f "Dockerfile" ]; then
|
||||
print_error "Dockerfile 不存在"
|
||||
exit 1
|
||||
fi
|
||||
print_info "找到 Dockerfile"
|
||||
}
|
||||
|
||||
# 构建Docker镜像
|
||||
build_image() {
|
||||
print_info "开始构建 Docker 镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
|
||||
# 构建镜像
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t ${IMAGE_NAME}:${IMAGE_TAG} \
|
||||
-f ${DOCKERFILE_PATH}/Dockerfile \
|
||||
${DOCKERFILE_PATH}
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_info "Docker 镜像构建成功!"
|
||||
print_info "镜像信息:"
|
||||
docker images ${IMAGE_NAME}:${IMAGE_TAG}
|
||||
else
|
||||
print_error "Docker 镜像构建失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 显示使用说明
|
||||
show_usage() {
|
||||
echo ""
|
||||
print_info "构建完成! 使用方法:"
|
||||
echo ""
|
||||
echo "1. 运行容器 (需要MongoDB):"
|
||||
echo " docker run -d \\"
|
||||
echo " --name infogenie-backend \\"
|
||||
echo " -p 5002:5002 \\"
|
||||
echo " -e MONGO_URI=mongodb://host.docker.internal:27017/InfoGenie \\"
|
||||
echo " -e SECRET_KEY=your-secret-key \\"
|
||||
echo " -e MAIL_USERNAME=your-email@qq.com \\"
|
||||
echo " -e MAIL_PASSWORD=your-app-password \\"
|
||||
echo " ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
echo ""
|
||||
echo "2. 使用 Docker Compose (推荐):"
|
||||
echo " 创建 docker-compose.yml 文件并运行:"
|
||||
echo " docker-compose up -d"
|
||||
echo ""
|
||||
echo "3. 查看日志:"
|
||||
echo " docker logs infogenie-backend"
|
||||
echo ""
|
||||
echo "4. 停止容器:"
|
||||
echo " docker stop infogenie-backend"
|
||||
echo " docker rm infogenie-backend"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
print_info "InfoGenie 后端 Docker 镜像构建脚本"
|
||||
print_info "=================================="
|
||||
|
||||
# 检查环境
|
||||
check_docker
|
||||
check_dockerfile
|
||||
|
||||
# 构建镜像
|
||||
build_image
|
||||
|
||||
# 显示使用说明
|
||||
show_usage
|
||||
|
||||
print_info "构建脚本执行完成!"
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main "$@"
|
||||
30
backend/config.py → InfoGenie-backend/config.py
Normal file → Executable file
30
backend/config.py → InfoGenie-backend/config.py
Normal file → Executable file
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
InfoGenie 配置文件
|
||||
Created by: 神奇万事通
|
||||
Created by: 万象口袋
|
||||
Date: 2025-09-02
|
||||
"""
|
||||
|
||||
@@ -22,11 +22,14 @@ class Config:
|
||||
# MongoDB 配置
|
||||
MONGO_URI = os.environ.get('MONGO_URI') or 'mongodb://localhost:27017/InfoGenie'
|
||||
|
||||
# Session 配置
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(days=7) # 会话持续7天
|
||||
SESSION_COOKIE_SECURE = False # 开发环境设为False,生产环境设为True
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
# hwt 配置
|
||||
HWT_LIFETIME = timedelta(days=7) # hwt持续7天
|
||||
HWT_SECURE = False # 开发环境设为False,生产环境设为True
|
||||
HWT_HTTPONLY = True
|
||||
HWT_SAMESITE = 'Lax'
|
||||
HWT_DOMAIN = None # 开发环境设为None,生产环境设为具体域名
|
||||
HWT_PATH = '/'
|
||||
HWT_REFRESH_EACH_REQUEST = True # 每次请求刷新hwt过期时间
|
||||
|
||||
# 邮件配置
|
||||
MAIL_SERVER = 'smtp.qq.com'
|
||||
@@ -35,7 +38,7 @@ class Config:
|
||||
MAIL_USE_TLS = False
|
||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'your-email@qq.com'
|
||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'your-app-password'
|
||||
MAIL_DEFAULT_SENDER = ('InfoGenie 神奇万事通', os.environ.get('MAIL_USERNAME') or 'your-email@qq.com')
|
||||
MAIL_DEFAULT_SENDER = ('InfoGenie 万象口袋', os.environ.get('MAIL_USERNAME') or 'your-email@qq.com')
|
||||
|
||||
# API 配置
|
||||
API_RATE_LIMIT = '100 per hour' # API调用频率限制
|
||||
@@ -43,20 +46,15 @@ class Config:
|
||||
# 外部API配置
|
||||
EXTERNAL_APIS = {
|
||||
'60s': [
|
||||
'https://60s.api.shumengya.top',
|
||||
'https://60s-cf.viki.moe',
|
||||
'https://60s.viki.moe',
|
||||
'https://60s.b23.run',
|
||||
'https://60s.114128.xyz',
|
||||
'https://60s-cf.114128.xyz'
|
||||
'https://60s.api.shumengya.top'
|
||||
]
|
||||
}
|
||||
|
||||
# 应用信息
|
||||
APP_INFO = {
|
||||
'name': '✨ 神奇万事通 ✨',
|
||||
'name': '✨ 万象口袋 ✨',
|
||||
'description': '🎨 一个多功能的聚合软件应用 💬',
|
||||
'author': '👨💻 by-神奇万事通',
|
||||
'author': '👨💻 by-万象口袋',
|
||||
'version': '1.0.0',
|
||||
'icp': '📄 蜀ICP备2025151694号'
|
||||
}
|
||||
@@ -70,7 +68,7 @@ class ProductionConfig(Config):
|
||||
"""生产环境配置"""
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
SESSION_COOKIE_SECURE = True
|
||||
HWT_SECURE = True
|
||||
|
||||
class TestingConfig(Config):
|
||||
"""测试环境配置"""
|
||||
53
InfoGenie-backend/docker-compose.yml
Normal file
53
InfoGenie-backend/docker-compose.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# InfoGenie 后端服务
|
||||
infogenie-backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "5002:5002"
|
||||
environment:
|
||||
- FLASK_ENV=production
|
||||
- SECRET_KEY=${SECRET_KEY:-infogenie-secret-key-2025}
|
||||
- MONGO_URI=mongodb://mongodb:27017/InfoGenie
|
||||
- MAIL_USERNAME=${MAIL_USERNAME:-your-email@qq.com}
|
||||
- MAIL_PASSWORD=${MAIL_PASSWORD:-your-app-password}
|
||||
- HWT_SECURE=false
|
||||
depends_on:
|
||||
- mongodb
|
||||
networks:
|
||||
- infogenie-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5002/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# MongoDB 数据库
|
||||
mongodb:
|
||||
image: mongo:6.0
|
||||
ports:
|
||||
- "27017:27017"
|
||||
environment:
|
||||
- MONGO_INITDB_DATABASE=InfoGenie
|
||||
volumes:
|
||||
- mongodb_data:/data/db
|
||||
- ./mongo-init:/docker-entrypoint-initdb.d
|
||||
networks:
|
||||
- infogenie-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
mongodb_data:
|
||||
|
||||
networks:
|
||||
infogenie-network:
|
||||
driver: bridge
|
||||
BIN
InfoGenie-backend/modules/__pycache__/aimodelapp.cpython-310.pyc
Normal file
BIN
InfoGenie-backend/modules/__pycache__/aimodelapp.cpython-310.pyc
Normal file
Binary file not shown.
BIN
InfoGenie-backend/modules/__pycache__/aimodelapp.cpython-313.pyc
Executable file
BIN
InfoGenie-backend/modules/__pycache__/aimodelapp.cpython-313.pyc
Executable file
Binary file not shown.
BIN
InfoGenie-backend/modules/__pycache__/api_60s.cpython-313.pyc
Executable file
BIN
InfoGenie-backend/modules/__pycache__/api_60s.cpython-313.pyc
Executable file
Binary file not shown.
BIN
InfoGenie-backend/modules/__pycache__/api_scanner.cpython-313.pyc
Executable file
BIN
InfoGenie-backend/modules/__pycache__/api_scanner.cpython-313.pyc
Executable file
Binary file not shown.
BIN
InfoGenie-backend/modules/__pycache__/auth.cpython-310.pyc
Normal file
BIN
InfoGenie-backend/modules/__pycache__/auth.cpython-310.pyc
Normal file
Binary file not shown.
BIN
InfoGenie-backend/modules/__pycache__/auth.cpython-313.pyc
Executable file
BIN
InfoGenie-backend/modules/__pycache__/auth.cpython-313.pyc
Executable file
Binary file not shown.
Binary file not shown.
BIN
InfoGenie-backend/modules/__pycache__/email_service.cpython-313.pyc
Executable file
BIN
InfoGenie-backend/modules/__pycache__/email_service.cpython-313.pyc
Executable file
Binary file not shown.
BIN
InfoGenie-backend/modules/__pycache__/smallgame.cpython-313.pyc
Executable file
BIN
InfoGenie-backend/modules/__pycache__/smallgame.cpython-313.pyc
Executable file
Binary file not shown.
Binary file not shown.
BIN
InfoGenie-backend/modules/__pycache__/user_management.cpython-313.pyc
Executable file
BIN
InfoGenie-backend/modules/__pycache__/user_management.cpython-313.pyc
Executable file
Binary file not shown.
954
InfoGenie-backend/modules/aimodelapp.py
Executable file
954
InfoGenie-backend/modules/aimodelapp.py
Executable file
@@ -0,0 +1,954 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
AI模型应用服务模块
|
||||
Created by: 万象口袋
|
||||
Date: 2025-01-15
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from bson import ObjectId
|
||||
from functools import wraps
|
||||
|
||||
# 创建蓝图
|
||||
aimodelapp_bp = Blueprint('aimodelapp', __name__)
|
||||
|
||||
# AI功能萌芽币消耗配置
|
||||
AI_COST = 100 # 每次调用AI功能消耗的萌芽币数量
|
||||
|
||||
# 验证用户萌芽币余额装饰器
|
||||
def verify_user_coins(f):
|
||||
"""验证用户萌芽币余额并在调用AI功能后扣除相应数量的萌芽币"""
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
try:
|
||||
# 获取用户认证信息
|
||||
token = request.headers.get('Authorization')
|
||||
if not token:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '未提供认证信息',
|
||||
'error_code': 'auth_required'
|
||||
}), 401
|
||||
|
||||
if token.startswith('Bearer '):
|
||||
token = token[7:]
|
||||
|
||||
# 解析JWT token
|
||||
import jwt
|
||||
try:
|
||||
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
|
||||
user_id = payload['user_id']
|
||||
except Exception as jwt_error:
|
||||
print(f"JWT解析错误: {str(jwt_error)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '无效的认证信息',
|
||||
'error_code': 'invalid_token'
|
||||
}), 401
|
||||
|
||||
# 查询用户萌芽币余额
|
||||
users_collection = current_app.mongo.db.userdata
|
||||
user = users_collection.find_one({'_id': ObjectId(user_id)})
|
||||
|
||||
if not user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '用户不存在',
|
||||
'error_code': 'user_not_found'
|
||||
}), 404
|
||||
|
||||
# 检查萌芽币余额
|
||||
current_coins = user.get('萌芽币', 0)
|
||||
if current_coins < AI_COST:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'萌芽币余额不足!当前余额: {current_coins}, 需要: {AI_COST}',
|
||||
'error_code': 'insufficient_coins',
|
||||
'current_coins': current_coins,
|
||||
'required_coins': AI_COST
|
||||
}), 402
|
||||
|
||||
# 先扣除萌芽币,确保无论服务是否成功都会扣费
|
||||
deduct_result = users_collection.update_one(
|
||||
{'_id': ObjectId(user_id)},
|
||||
{'$inc': {'萌芽币': -AI_COST}}
|
||||
)
|
||||
|
||||
if deduct_result.modified_count < 1:
|
||||
print(f"警告: 用户 {user_id} 萌芽币扣除失败")
|
||||
|
||||
# 为请求添加用户信息,以便在函数内部使用
|
||||
request.current_user = {
|
||||
'user_id': user_id,
|
||||
'username': user.get('用户名', ''),
|
||||
'email': user.get('邮箱', '')
|
||||
}
|
||||
|
||||
# 保存API调用类型
|
||||
api_type = request.path.split('/')[-1]
|
||||
|
||||
# 添加使用记录
|
||||
usage_record = {
|
||||
'api_type': api_type,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'cost': AI_COST
|
||||
}
|
||||
|
||||
# 更新用户的AI使用历史记录
|
||||
users_collection.update_one(
|
||||
{'_id': ObjectId(user_id)},
|
||||
{'$push': {'ai_usage_history': usage_record}}
|
||||
)
|
||||
|
||||
# 调用原函数
|
||||
result = f(*args, **kwargs)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"验证萌芽币时发生错误: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '处理请求时出错',
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
return decorated
|
||||
|
||||
#加载AI配置文件
|
||||
def load_ai_config():
|
||||
"""加载AI配置文件"""
|
||||
try:
|
||||
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'ai_config.json')
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"加载AI配置失败: {e}")
|
||||
return None
|
||||
|
||||
#调用DeepSeek API,带重试机制
|
||||
def call_deepseek_api(messages, model="deepseek-chat", max_retries=3):
|
||||
"""调用DeepSeek API,带重试机制"""
|
||||
config = load_ai_config()
|
||||
if not config or 'deepseek' not in config:
|
||||
return None, "AI配置加载失败"
|
||||
|
||||
deepseek_config = config['deepseek']
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {deepseek_config["api_key"]}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
data = {
|
||||
'model': model,
|
||||
'messages': messages,
|
||||
'temperature': 0.7,
|
||||
'max_tokens': 2000
|
||||
}
|
||||
|
||||
import time
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
# 增加超时时间到90秒
|
||||
response = requests.post(
|
||||
f"{deepseek_config['api_base']}/chat/completions",
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=90
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return result['choices'][0]['message']['content'], None
|
||||
else:
|
||||
error_msg = f"API调用失败: {response.status_code} - {response.text}"
|
||||
if attempt < max_retries - 1:
|
||||
print(f"第{attempt + 1}次尝试失败,等待重试: {error_msg}")
|
||||
time.sleep(2 ** attempt) # 指数退避
|
||||
continue
|
||||
return None, error_msg
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
error_msg = "API请求超时"
|
||||
if attempt < max_retries - 1:
|
||||
print(f"第{attempt + 1}次尝试超时,等待重试")
|
||||
time.sleep(2 ** attempt) # 指数退避
|
||||
continue
|
||||
return None, f"{error_msg}(已重试{max_retries}次)"
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"API调用异常: {str(e)}"
|
||||
if attempt < max_retries - 1:
|
||||
print(f"第{attempt + 1}次尝试异常,等待重试: {error_msg}")
|
||||
time.sleep(2 ** attempt) # 指数退避
|
||||
continue
|
||||
return None, f"{error_msg}(已重试{max_retries}次)"
|
||||
|
||||
#调用Kimi API
|
||||
def call_kimi_api(messages, model="kimi-k2-0905-preview"):
|
||||
"""调用Kimi API"""
|
||||
config = load_ai_config()
|
||||
if not config or 'kimi' not in config:
|
||||
return None, "AI配置加载失败"
|
||||
|
||||
kimi_config = config['kimi']
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {kimi_config["api_key"]}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
data = {
|
||||
'model': model,
|
||||
'messages': messages,
|
||||
'temperature': 0.7,
|
||||
'max_tokens': 2000
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{kimi_config['api_base']}/v1/chat/completions",
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return result['choices'][0]['message']['content'], None
|
||||
else:
|
||||
return None, f"API调用失败: {response.status_code} - {response.text}"
|
||||
|
||||
except Exception as e:
|
||||
return None, f"API调用异常: {str(e)}"
|
||||
|
||||
#统一的AI聊天接口
|
||||
@aimodelapp_bp.route('/chat', methods=['POST'])
|
||||
@verify_user_coins
|
||||
def ai_chat():
|
||||
"""统一的AI聊天接口"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return jsonify({'error': '请求数据为空'}), 400
|
||||
|
||||
# 获取请求参数
|
||||
messages = data.get('messages', [])
|
||||
model_provider = data.get('provider', 'deepseek') # 默认使用deepseek
|
||||
model_name = data.get('model', 'deepseek-chat') # 默认模型
|
||||
|
||||
if not messages:
|
||||
return jsonify({'error': '消息内容不能为空'}), 400
|
||||
|
||||
# 根据提供商调用对应的API
|
||||
if model_provider == 'deepseek':
|
||||
content, error = call_deepseek_api(messages, model_name)
|
||||
elif model_provider == 'kimi':
|
||||
content, error = call_kimi_api(messages, model_name)
|
||||
else:
|
||||
return jsonify({'error': f'不支持的AI提供商: {model_provider}'}), 400
|
||||
|
||||
if error:
|
||||
return jsonify({'error': error}), 500
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'content': content,
|
||||
'provider': model_provider,
|
||||
'model': model_name,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'服务器错误: {str(e)}'}), 500
|
||||
|
||||
#姓名分析专用接口
|
||||
@aimodelapp_bp.route('/name-analysis', methods=['POST'])
|
||||
@verify_user_coins
|
||||
def name_analysis():
|
||||
"""姓名分析专用接口"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
name = data.get('name', '').strip()
|
||||
|
||||
if not name:
|
||||
return jsonify({'error': '姓名不能为空'}), 400
|
||||
|
||||
# 构建姓名分析的专业提示词
|
||||
prompt = f"""你是一位专业的姓名学专家和语言学家,请对输入的姓名进行全面分析。请直接输出分析结果,不要包含任何思考过程或<think>标签。
|
||||
|
||||
姓名:{name}
|
||||
|
||||
请按照以下格式严格输出分析结果:
|
||||
|
||||
【稀有度评分】
|
||||
评分:X%
|
||||
评价:[对稀有度的详细说明,包括姓氏和名字的常见程度分析]
|
||||
|
||||
【音韵评价】
|
||||
评分:X%
|
||||
评价:[对音韵美感的分析,包括声调搭配、读音流畅度、音律和谐度等]
|
||||
|
||||
【含义解读】
|
||||
[详细分析姓名的寓意内涵,包括:
|
||||
1. 姓氏的历史渊源和文化背景
|
||||
2. 名字各字的含义和象征
|
||||
3. 整体姓名的寓意组合
|
||||
4. 可能体现的父母期望或文化内涵
|
||||
5. 与传统文化、诗词典故的关联等]
|
||||
|
||||
要求:
|
||||
1. 评分必须是1-100的整数百分比,要有明显区分度,避免雷同
|
||||
2. 分析要专业、客观、有依据,评分要根据实际情况有所差异
|
||||
3. 含义解读要详细深入,至少150字
|
||||
4. 严格按照上述格式输出,不要添加思考过程、<think>标签或其他内容
|
||||
5. 如果是生僻字或罕见姓名,要特别说明
|
||||
6. 直接输出最终结果,不要显示推理过程"""
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
# 使用DeepSeek进行分析
|
||||
content, error = call_deepseek_api(messages)
|
||||
|
||||
if error:
|
||||
return jsonify({'error': error}), 500
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'analysis': content,
|
||||
'name': name,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'姓名分析失败: {str(e)}'}), 500
|
||||
|
||||
#变量命名助手接口
|
||||
@aimodelapp_bp.route('/variable-naming', methods=['POST'])
|
||||
@verify_user_coins
|
||||
def variable_naming():
|
||||
"""变量命名助手接口"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
description = data.get('description', '').strip()
|
||||
language = data.get('language', 'javascript').lower()
|
||||
|
||||
if not description:
|
||||
return jsonify({'error': '变量描述不能为空'}), 400
|
||||
|
||||
# 构建变量命名的提示词
|
||||
prompt = f"""你是一个专业的变量命名助手。请根据以下描述为变量生成合适的名称:
|
||||
|
||||
描述:{description}
|
||||
|
||||
请为每种命名规范生成3个变量名建议:
|
||||
1. camelCase (驼峰命名法)
|
||||
2. PascalCase (帕斯卡命名法)
|
||||
3. snake_case (下划线命名法)
|
||||
4. kebab-case (短横线命名法)
|
||||
5. CONSTANT_CASE (常量命名法)
|
||||
|
||||
要求:
|
||||
- 变量名要准确反映功能和用途
|
||||
- 严格遵循各自的命名规范
|
||||
- 避免使用缩写,除非是广泛认知的缩写
|
||||
- 名称要简洁但具有描述性
|
||||
- 考虑代码的可读性和维护性
|
||||
|
||||
请按以下JSON格式返回:
|
||||
{{
|
||||
"suggestions": {{
|
||||
"camelCase": [
|
||||
{{"name": "变量名1", "description": "解释说明1"}},
|
||||
{{"name": "变量名2", "description": "解释说明2"}},
|
||||
{{"name": "变量名3", "description": "解释说明3"}}
|
||||
],
|
||||
"PascalCase": [
|
||||
{{"name": "变量名1", "description": "解释说明1"}},
|
||||
{{"name": "变量名2", "description": "解释说明2"}},
|
||||
{{"name": "变量名3", "description": "解释说明3"}}
|
||||
],
|
||||
"snake_case": [
|
||||
{{"name": "变量名1", "description": "解释说明1"}},
|
||||
{{"name": "变量名2", "description": "解释说明2"}},
|
||||
{{"name": "变量名3", "description": "解释说明3"}}
|
||||
],
|
||||
"kebab-case": [
|
||||
{{"name": "变量名1", "description": "解释说明1"}},
|
||||
{{"name": "变量名2", "description": "解释说明2"}},
|
||||
{{"name": "变量名3", "description": "解释说明3"}}
|
||||
],
|
||||
"CONSTANT_CASE": [
|
||||
{{"name": "变量名1", "description": "解释说明1"}},
|
||||
{{"name": "变量名2", "description": "解释说明2"}},
|
||||
{{"name": "变量名3", "description": "解释说明3"}}
|
||||
]
|
||||
}}
|
||||
}}
|
||||
|
||||
只返回JSON格式的结果,不要包含其他文字。"""
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
# 使用DeepSeek进行分析
|
||||
content, error = call_deepseek_api(messages)
|
||||
|
||||
if error:
|
||||
return jsonify({'error': error}), 500
|
||||
|
||||
# 解析AI返回的JSON格式数据
|
||||
try:
|
||||
# 尝试直接解析JSON
|
||||
ai_response = json.loads(content)
|
||||
suggestions = ai_response.get('suggestions', {})
|
||||
except json.JSONDecodeError:
|
||||
# 如果直接解析失败,尝试提取JSON部分
|
||||
import re
|
||||
json_match = re.search(r'\{[\s\S]*\}', content)
|
||||
if json_match:
|
||||
try:
|
||||
ai_response = json.loads(json_match.group())
|
||||
suggestions = ai_response.get('suggestions', {})
|
||||
except json.JSONDecodeError:
|
||||
return jsonify({'error': 'AI返回的数据格式无法解析'}), 500
|
||||
else:
|
||||
return jsonify({'error': 'AI返回的数据中未找到有效的JSON格式'}), 500
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'suggestions': suggestions,
|
||||
'description': description,
|
||||
'language': language,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'变量命名失败: {str(e)}'}), 500
|
||||
|
||||
#AI写诗助手接口
|
||||
@aimodelapp_bp.route('/poetry', methods=['POST'])
|
||||
@verify_user_coins
|
||||
def poetry_assistant():
|
||||
"""AI写诗助手接口"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
theme = data.get('theme', '').strip()
|
||||
style = data.get('style', '现代诗').strip()
|
||||
mood = data.get('mood', '').strip()
|
||||
|
||||
if not theme:
|
||||
return jsonify({'error': '诗歌主题不能为空'}), 400
|
||||
|
||||
# 构建写诗的提示词
|
||||
prompt = f"""你是一位才华横溢的诗人,请根据以下要求创作一首诗歌。
|
||||
|
||||
主题:{theme}
|
||||
风格:{style}
|
||||
情感基调:{mood if mood else '自由发挥'}
|
||||
|
||||
创作要求:
|
||||
1. 紧扣主题,情感真挚
|
||||
2. 语言优美,意境深远
|
||||
3. 符合指定的诗歌风格
|
||||
4. 长度适中,朗朗上口
|
||||
5. 如果是古体诗,注意平仄和韵律
|
||||
|
||||
请直接输出诗歌作品,不需要额外的解释或分析。"""
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
# 使用DeepSeek进行创作
|
||||
content, error = call_deepseek_api(messages)
|
||||
|
||||
if error:
|
||||
return jsonify({'error': error}), 500
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'poem': content,
|
||||
'theme': theme,
|
||||
'style': style,
|
||||
'mood': mood,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'诗歌创作失败: {str(e)}'}), 500
|
||||
|
||||
#AI语言翻译接口
|
||||
@aimodelapp_bp.route('/translation', methods=['POST'])
|
||||
@verify_user_coins
|
||||
def translation():
|
||||
"""AI语言翻译接口"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
source_text = data.get('source_text', '').strip()
|
||||
target_language = data.get('target_language', 'zh-CN').strip()
|
||||
|
||||
if not source_text:
|
||||
return jsonify({'error': '翻译内容不能为空'}), 400
|
||||
|
||||
# 语言映射
|
||||
language_map = {
|
||||
'zh-CN': '中文(简体)',
|
||||
'zh-TW': '中文(繁体)',
|
||||
'en': '英语',
|
||||
'ja': '日语',
|
||||
'ko': '韩语',
|
||||
'fr': '法语',
|
||||
'de': '德语',
|
||||
'es': '西班牙语',
|
||||
'it': '意大利语',
|
||||
'pt': '葡萄牙语',
|
||||
'ru': '俄语',
|
||||
'ar': '阿拉伯语',
|
||||
'hi': '印地语',
|
||||
'th': '泰语',
|
||||
'vi': '越南语'
|
||||
}
|
||||
|
||||
target_language_name = language_map.get(target_language, target_language)
|
||||
|
||||
# 构建翻译的专业提示词
|
||||
prompt = f"""你是一位专业的翻译专家,精通多种语言的翻译工作。请将以下文本翻译成{target_language_name}。
|
||||
|
||||
原文:{source_text}
|
||||
|
||||
翻译要求:
|
||||
1. 【信】- 忠实原文,准确传达原意,不遗漏、不添加、不歪曲
|
||||
2. 【达】- 译文通顺流畅,符合目标语言的表达习惯和语法规范
|
||||
3. 【雅】- 用词优美得体,风格与原文相符,具有良好的可读性
|
||||
|
||||
特别注意:
|
||||
- 自动检测源语言,无需用户指定
|
||||
- 保持原文的语气、情感色彩和文体风格
|
||||
- 对于专业术语,提供准确的对应翻译
|
||||
- 对于文化特色词汇,在保持原意的基础上进行适当的本土化处理
|
||||
- 如果是单词或短语,提供多个常用含义的翻译
|
||||
- 如果是句子,确保语法正确、表达自然
|
||||
|
||||
请按以下JSON格式返回翻译结果:
|
||||
{{
|
||||
"detected_language": "检测到的源语言名称",
|
||||
"target_language": "{target_language_name}",
|
||||
"translation": "翻译结果",
|
||||
"alternative_translations": [
|
||||
"备选翻译1",
|
||||
"备选翻译2",
|
||||
"备选翻译3"
|
||||
],
|
||||
"explanation": "翻译说明(包括语境、用法、注意事项等)",
|
||||
"pronunciation": "目标语言的发音指导(如适用)"
|
||||
}}
|
||||
|
||||
只返回JSON格式的结果,不要包含其他文字。"""
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
# 使用DeepSeek进行翻译
|
||||
content, error = call_deepseek_api(messages)
|
||||
|
||||
if error:
|
||||
return jsonify({'error': error}), 500
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'translation_result': content,
|
||||
'source_text': source_text,
|
||||
'target_language': target_language,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'翻译失败: {str(e)}'}), 500
|
||||
|
||||
#现代文转文言文接口
|
||||
@aimodelapp_bp.route('/classical_conversion', methods=['POST'])
|
||||
@verify_user_coins
|
||||
def classical_conversion():
|
||||
"""现代文转文言文接口"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
modern_text = data.get('modern_text', '').strip()
|
||||
style = data.get('style', '古雅').strip()
|
||||
article_type = data.get('article_type', '散文').strip()
|
||||
|
||||
if not modern_text:
|
||||
return jsonify({'error': '现代文内容不能为空'}), 400
|
||||
|
||||
# 构建文言文转换的专业提示词
|
||||
prompt = f"""你是一位精通古代文言文的文学大师,擅长将现代文转换为优美的文言文。请将以下现代文转换为文言文。
|
||||
|
||||
现代文:{modern_text}
|
||||
|
||||
转换要求:
|
||||
1. 风格:{style}
|
||||
2. 文体:{article_type}
|
||||
3. 保持原文的核心意思和情感色彩
|
||||
4. 使用恰当的文言文语法和词汇
|
||||
5. 注重音韵美感和文字的雅致
|
||||
6. 根据不同风格调整用词和句式
|
||||
|
||||
风格说明:
|
||||
- 古雅:典雅庄重,用词考究,句式工整
|
||||
- 简洁:言简意赅,删繁就简,朴实无华
|
||||
- 华丽:辞藻华美,对仗工整,音韵和谐
|
||||
- 朴实:平实自然,通俗易懂,贴近生活
|
||||
|
||||
文体特点:
|
||||
- 散文:行文自由,情理并茂
|
||||
- 诗歌:讲究韵律,意境深远
|
||||
- 议论文:逻辑严密,论证有力
|
||||
- 记叙文:叙事生动,描写细腻
|
||||
- 书信:格式规范,情真意切
|
||||
- 公文:庄重严肃,用词准确
|
||||
|
||||
请按以下JSON格式返回转换结果:
|
||||
{{
|
||||
"classical_text": "转换后的文言文",
|
||||
"translation_notes": "转换说明,包括重要词汇的选择理由和语法特点",
|
||||
"style_analysis": "风格分析,说明如何体现所选风格特点",
|
||||
"difficulty_level": "难度等级(初级/中级/高级)",
|
||||
"key_phrases": [
|
||||
{{
|
||||
"modern": "现代词汇",
|
||||
"classical": "对应文言文词汇",
|
||||
"explanation": "转换说明"
|
||||
}}
|
||||
],
|
||||
"cultural_elements": "文化内涵说明,包含的典故、意象等"
|
||||
}}
|
||||
|
||||
只返回JSON格式的结果,不要包含其他文字。"""
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
# 使用DeepSeek进行文言文转换
|
||||
content, error = call_deepseek_api(messages)
|
||||
|
||||
if error:
|
||||
return jsonify({'error': error}), 500
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'conversion_result': content,
|
||||
'modern_text': modern_text,
|
||||
'style': style,
|
||||
'article_type': article_type,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'文言文转换失败: {str(e)}'}), 500
|
||||
|
||||
#AI表情制作器接口
|
||||
@aimodelapp_bp.route('/expression-maker', methods=['POST'])
|
||||
@verify_user_coins
|
||||
def expression_maker():
|
||||
"""AI表情制作器接口"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
text = data.get('text', '').strip()
|
||||
style = data.get('style', 'mixed').strip()
|
||||
|
||||
if not text:
|
||||
return jsonify({'error': '文字内容不能为空'}), 400
|
||||
|
||||
# 风格映射
|
||||
style_prompts = {
|
||||
'mixed': '混合使用Emoji表情和颜文字',
|
||||
'emoji': '仅使用Emoji表情符号',
|
||||
'kaomoji': '仅使用颜文字(日式表情符号)',
|
||||
'cute': '使用可爱风格的表情符号',
|
||||
'cool': '使用酷炫风格的表情符号'
|
||||
}
|
||||
|
||||
style_description = style_prompts.get(style, style_prompts['mixed'])
|
||||
|
||||
# 构建表情制作的提示词
|
||||
prompt = f"""你是一个专业的表情符号专家,擅长为文字内容生成合适的表情符号。请根据以下文字内容生成相应的表情符号:
|
||||
|
||||
文字内容:{text}
|
||||
表情风格:{style_description}
|
||||
|
||||
请为这个文字内容生成表情符号,要求:
|
||||
1. 准确表达文字的情感和含义
|
||||
2. 符合指定的表情风格
|
||||
3. 提供多样化的选择
|
||||
4. 包含使用场景说明
|
||||
|
||||
请按以下分类生成表情符号:
|
||||
1. Emoji表情(使用Unicode表情符号)
|
||||
2. 颜文字(使用ASCII字符组成的表情)
|
||||
3. 组合表情(多个符号组合使用)
|
||||
|
||||
每个分类提供5个不同的表情选项,每个选项包含:
|
||||
- 表情符号本身
|
||||
- 适用场景说明
|
||||
- 情感强度(轻微/中等/强烈)
|
||||
|
||||
请按以下JSON格式返回:
|
||||
{{
|
||||
"expressions": {{
|
||||
"emoji": [
|
||||
{{
|
||||
"symbol": "😊",
|
||||
"description": "适用场景和情感说明",
|
||||
"intensity": "中等",
|
||||
"usage": "使用建议"
|
||||
}}
|
||||
],
|
||||
"kaomoji": [
|
||||
{{
|
||||
"symbol": "(^_^)",
|
||||
"description": "适用场景和情感说明",
|
||||
"intensity": "轻微",
|
||||
"usage": "使用建议"
|
||||
}}
|
||||
],
|
||||
"combination": [
|
||||
{{
|
||||
"symbol": "🎉✨",
|
||||
"description": "适用场景和情感说明",
|
||||
"intensity": "强烈",
|
||||
"usage": "使用建议"
|
||||
}}
|
||||
]
|
||||
}},
|
||||
"summary": {{
|
||||
"emotion_analysis": "对输入文字的情感分析",
|
||||
"recommended_usage": "推荐的使用场景",
|
||||
"style_notes": "风格特点说明"
|
||||
}}
|
||||
}}
|
||||
|
||||
只返回JSON格式的结果,不要包含其他文字。"""
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
# 使用DeepSeek进行分析
|
||||
content, error = call_deepseek_api(messages)
|
||||
|
||||
if error:
|
||||
return jsonify({'error': error}), 500
|
||||
|
||||
# 解析AI返回的JSON格式数据
|
||||
try:
|
||||
# 尝试直接解析JSON
|
||||
ai_response = json.loads(content)
|
||||
expressions = ai_response.get('expressions', {})
|
||||
summary = ai_response.get('summary', {})
|
||||
except json.JSONDecodeError:
|
||||
# 如果直接解析失败,尝试提取JSON部分
|
||||
import re
|
||||
json_match = re.search(r'\{[\s\S]*\}', content)
|
||||
if json_match:
|
||||
try:
|
||||
ai_response = json.loads(json_match.group())
|
||||
expressions = ai_response.get('expressions', {})
|
||||
summary = ai_response.get('summary', {})
|
||||
except json.JSONDecodeError:
|
||||
return jsonify({'error': 'AI返回的数据格式无法解析'}), 500
|
||||
else:
|
||||
return jsonify({'error': 'AI返回的数据中未找到有效的JSON格式'}), 500
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'expressions': expressions,
|
||||
'summary': summary,
|
||||
'text': text,
|
||||
'style': style,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'表情制作失败: {str(e)}'}), 500
|
||||
|
||||
#Linux命令生成接口
|
||||
@aimodelapp_bp.route('/linux-command', methods=['POST'])
|
||||
@verify_user_coins
|
||||
def linux_command_generator():
|
||||
"""Linux命令生成接口"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
task_description = data.get('task_description', '').strip()
|
||||
difficulty_level = data.get('difficulty_level', 'beginner').strip()
|
||||
|
||||
if not task_description:
|
||||
return jsonify({'error': '任务描述不能为空'}), 400
|
||||
|
||||
# 构建Linux命令生成的专业提示词
|
||||
prompt = f"""你是一位Linux系统专家,请根据用户的任务描述生成相应的Linux命令。
|
||||
|
||||
任务描述:{task_description}
|
||||
用户水平:{difficulty_level}
|
||||
|
||||
请为这个任务生成合适的Linux命令,要求:
|
||||
1. 命令准确可用,符合Linux标准
|
||||
2. 根据用户水平提供适当的复杂度
|
||||
3. 提供多种实现方式(如果有的话)
|
||||
4. 包含安全提示和注意事项
|
||||
5. 解释每个命令的作用和参数
|
||||
|
||||
用户水平说明:
|
||||
- beginner(初学者):提供基础命令,详细解释
|
||||
- intermediate(中级):提供常用命令和选项
|
||||
- advanced(高级):提供高效命令和高级用法
|
||||
|
||||
请按以下JSON格式返回:
|
||||
{{
|
||||
"commands": [
|
||||
{{
|
||||
"command": "具体的Linux命令",
|
||||
"description": "命令的详细说明",
|
||||
"safety_level": "safe/caution/dangerous",
|
||||
"explanation": "命令各部分的解释",
|
||||
"example_output": "预期的命令输出示例",
|
||||
"alternatives": ["替代命令1", "替代命令2"]
|
||||
}}
|
||||
],
|
||||
"safety_warnings": ["安全提示1", "安全提示2"],
|
||||
"prerequisites": ["前置条件1", "前置条件2"],
|
||||
"related_concepts": ["相关概念1", "相关概念2"]
|
||||
}}
|
||||
|
||||
只返回JSON格式的结果,不要包含其他文字。"""
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
# 使用DeepSeek进行命令生成
|
||||
content, error = call_deepseek_api(messages)
|
||||
|
||||
if error:
|
||||
return jsonify({'error': error}), 500
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'command_result': content,
|
||||
'task_description': task_description,
|
||||
'difficulty_level': difficulty_level,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Linux命令生成失败: {str(e)}'}), 500
|
||||
|
||||
#获取用户萌芽币余额
|
||||
@aimodelapp_bp.route('/coins', methods=['GET'])
|
||||
def get_user_coins():
|
||||
"""获取用户萌芽币余额"""
|
||||
try:
|
||||
# 获取用户认证信息
|
||||
token = request.headers.get('Authorization')
|
||||
if not token:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '未提供认证信息',
|
||||
'error_code': 'auth_required'
|
||||
}), 401
|
||||
|
||||
if token.startswith('Bearer '):
|
||||
token = token[7:]
|
||||
|
||||
# 解析JWT token
|
||||
import jwt
|
||||
try:
|
||||
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
|
||||
user_id = payload['user_id']
|
||||
except jwt.ExpiredSignatureError:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Token已过期,请重新登录',
|
||||
'error_code': 'token_expired'
|
||||
}), 401
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'无效的认证信息: {str(e)}',
|
||||
'error_code': 'invalid_token'
|
||||
}), 401
|
||||
|
||||
# 查询用户萌芽币余额
|
||||
users_collection = current_app.mongo.db.userdata
|
||||
user = users_collection.find_one({'_id': ObjectId(user_id)})
|
||||
|
||||
if not user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '用户不存在',
|
||||
'error_code': 'user_not_found'
|
||||
}), 404
|
||||
|
||||
# 返回萌芽币信息
|
||||
current_coins = user.get('萌芽币', 0)
|
||||
username = user.get('用户名', '用户')
|
||||
|
||||
# 增加额外有用信息
|
||||
ai_usage_history = []
|
||||
if 'ai_usage_history' in user:
|
||||
ai_usage_history = user['ai_usage_history'][-5:] # 最近5条使用记录
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'coins': current_coins,
|
||||
'ai_cost': AI_COST,
|
||||
'can_use_ai': current_coins >= AI_COST,
|
||||
'username': username,
|
||||
'usage_count': len(ai_usage_history),
|
||||
'recent_usage': ai_usage_history
|
||||
},
|
||||
'message': f'当前萌芽币余额: {current_coins}'
|
||||
}), 200
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '处理请求时出错',
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
#获取可用的AI模型列表
|
||||
@aimodelapp_bp.route('/models', methods=['GET'])
|
||||
def get_available_models():
|
||||
"""获取可用的AI模型列表"""
|
||||
try:
|
||||
config = load_ai_config()
|
||||
if not config:
|
||||
return jsonify({'error': 'AI配置加载失败'}), 500
|
||||
|
||||
models = {}
|
||||
for provider, provider_config in config.items():
|
||||
if 'model' in provider_config:
|
||||
models[provider] = provider_config['model']
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'models': models,
|
||||
'default_provider': 'deepseek',
|
||||
'default_model': 'deepseek-chat'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'获取模型列表失败: {str(e)}'}), 500
|
||||
152
backend/modules/auth.py → InfoGenie-backend/modules/auth.py
Normal file → Executable file
152
backend/modules/auth.py → InfoGenie-backend/modules/auth.py
Normal file → Executable file
@@ -2,27 +2,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
用户认证模块
|
||||
Created by: 神奇万事通
|
||||
Created by: 万象口袋
|
||||
Date: 2025-09-02
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify, session, current_app
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import hashlib
|
||||
import re
|
||||
from datetime import datetime
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
from .email_service import send_verification_email, verify_code, is_qq_email, get_qq_avatar_url
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
#生成JWT token
|
||||
def generate_token(user_data):
|
||||
"""生成JWT token"""
|
||||
payload = {
|
||||
'user_id': user_data['user_id'],
|
||||
'email': user_data['email'],
|
||||
'username': user_data['username'],
|
||||
'exp': datetime.utcnow() + timedelta(days=7), # 7天过期
|
||||
'iat': datetime.utcnow()
|
||||
}
|
||||
return jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
|
||||
|
||||
#验证JWT token
|
||||
def verify_token(token):
|
||||
"""验证JWT token"""
|
||||
try:
|
||||
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
|
||||
return {'success': True, 'data': payload}
|
||||
except jwt.ExpiredSignatureError:
|
||||
return {'success': False, 'message': 'Token已过期'}
|
||||
except jwt.InvalidTokenError:
|
||||
return {'success': False, 'message': 'Token无效'}
|
||||
|
||||
#JWT token验证装饰器
|
||||
def token_required(f):
|
||||
"""JWT token验证装饰器"""
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
token = request.headers.get('Authorization')
|
||||
if not token:
|
||||
return jsonify({'success': False, 'message': '缺少认证token'}), 401
|
||||
|
||||
if token.startswith('Bearer '):
|
||||
token = token[7:]
|
||||
|
||||
result = verify_token(token)
|
||||
if not result['success']:
|
||||
return jsonify({'success': False, 'message': result['message']}), 401
|
||||
|
||||
request.current_user = result['data']
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
|
||||
#验证QQ邮箱格式
|
||||
def validate_qq_email(email):
|
||||
"""验证QQ邮箱格式"""
|
||||
return is_qq_email(email)
|
||||
|
||||
#验证密码格式
|
||||
def validate_password(password):
|
||||
"""验证密码格式(6-20位)"""
|
||||
return 6 <= len(password) <= 20
|
||||
|
||||
#发送验证码邮件
|
||||
@auth_bp.route('/send-verification', methods=['POST'])
|
||||
def send_verification():
|
||||
"""发送验证码邮件"""
|
||||
@@ -78,6 +126,7 @@ def send_verification():
|
||||
'message': '发送失败,请稍后重试'
|
||||
}), 500
|
||||
|
||||
#验证验证码
|
||||
@auth_bp.route('/verify-code', methods=['POST'])
|
||||
def verify_verification_code():
|
||||
"""验证验证码"""
|
||||
@@ -108,6 +157,7 @@ def verify_verification_code():
|
||||
'message': '验证失败,请稍后重试'
|
||||
}), 500
|
||||
|
||||
#用户注册
|
||||
@auth_bp.route('/register', methods=['POST'])
|
||||
def register():
|
||||
"""用户注册(需要先验证邮箱)"""
|
||||
@@ -176,7 +226,15 @@ def register():
|
||||
'注册时间': datetime.now().isoformat(),
|
||||
'最后登录': None,
|
||||
'登录次数': 0,
|
||||
'用户状态': 'active'
|
||||
'用户状态': 'active',
|
||||
'等级': 0,
|
||||
'经验': 0,
|
||||
'萌芽币': 0,
|
||||
'签到系统': {
|
||||
'连续签到天数': 0,
|
||||
'今日是否已签到': False,
|
||||
'签到时间': '2025-01-01'
|
||||
}
|
||||
}
|
||||
|
||||
result = users_collection.insert_one(user_data)
|
||||
@@ -204,42 +262,7 @@ def register():
|
||||
'message': '注册失败,请稍后重试'
|
||||
}), 500
|
||||
|
||||
if existing_user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '该账号已被注册'
|
||||
}), 409
|
||||
|
||||
# 创建新用户
|
||||
password_hash = generate_password_hash(password)
|
||||
user_data = {
|
||||
'账号': account,
|
||||
'密码': password_hash,
|
||||
'注册时间': datetime.now().isoformat(),
|
||||
'最后登录': None,
|
||||
'登录次数': 0,
|
||||
'用户状态': 'active'
|
||||
}
|
||||
|
||||
result = users_collection.insert_one(user_data)
|
||||
|
||||
if result.inserted_id:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '注册成功!'
|
||||
}), 201
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '注册失败,请稍后重试'
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
#用户登录
|
||||
@auth_bp.route('/login', methods=['POST'])
|
||||
def login():
|
||||
"""用户登录(支持邮箱+验证码或邮箱+密码)"""
|
||||
@@ -313,15 +336,18 @@ def login():
|
||||
}
|
||||
)
|
||||
|
||||
# 设置会话
|
||||
session['user_id'] = str(user['_id'])
|
||||
session['email'] = email
|
||||
session['username'] = user.get('用户名', '')
|
||||
session.permanent = True
|
||||
# 生成JWT token
|
||||
user_data = {
|
||||
'user_id': str(user['_id']),
|
||||
'email': email,
|
||||
'username': user.get('用户名', '')
|
||||
}
|
||||
token = generate_token(user_data)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '登录成功!',
|
||||
'token': token,
|
||||
'user': {
|
||||
'id': str(user['_id']),
|
||||
'email': email,
|
||||
@@ -339,9 +365,10 @@ def login():
|
||||
}), 500
|
||||
|
||||
# 登录成功,创建会话
|
||||
session['user_id'] = str(user['_id'])
|
||||
session['account'] = user['账号']
|
||||
session['logged_in'] = True
|
||||
hwt = getattr(request, 'hwt', {})
|
||||
hwt['user_id'] = str(user['_id'])
|
||||
hwt['account'] = user['账号']
|
||||
hwt['logged_in'] = True
|
||||
|
||||
# 更新登录信息
|
||||
users_collection.update_one(
|
||||
@@ -368,21 +395,16 @@ def login():
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
#用户登出
|
||||
@auth_bp.route('/logout', methods=['POST'])
|
||||
def logout():
|
||||
"""用户登出"""
|
||||
try:
|
||||
if 'logged_in' in session:
|
||||
session.clear()
|
||||
# JWT是无状态的,客户端删除token即可
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '已成功登出'
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '用户未登录'
|
||||
}), 401
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
@@ -390,17 +412,31 @@ def logout():
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
#检查登录状态
|
||||
@auth_bp.route('/check', methods=['GET'])
|
||||
def check_login():
|
||||
"""检查登录状态"""
|
||||
try:
|
||||
if session.get('logged_in'):
|
||||
token = request.headers.get('Authorization')
|
||||
if not token:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'logged_in': False
|
||||
}), 200
|
||||
|
||||
if token.startswith('Bearer '):
|
||||
token = token[7:]
|
||||
|
||||
result = verify_token(token)
|
||||
if result['success']:
|
||||
user_data = result['data']
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'logged_in': True,
|
||||
'user': {
|
||||
'account': session.get('account'),
|
||||
'user_id': session.get('user_id')
|
||||
'id': user_data['user_id'],
|
||||
'email': user_data['email'],
|
||||
'username': user_data['username']
|
||||
}
|
||||
}), 200
|
||||
else:
|
||||
11
backend/modules/email_service.py → InfoGenie-backend/modules/email_service.py
Normal file → Executable file
11
backend/modules/email_service.py → InfoGenie-backend/modules/email_service.py
Normal file → Executable file
@@ -18,15 +18,18 @@ import os
|
||||
# 验证码存储(生产环境建议使用Redis)
|
||||
verification_codes = {}
|
||||
|
||||
# 初始化日志
|
||||
def init_mail(app):
|
||||
"""初始化邮件配置"""
|
||||
# 使用smtplib直接发送,不需要Flask-Mail
|
||||
pass
|
||||
|
||||
# 生成验证码
|
||||
def generate_verification_code(length=6):
|
||||
"""生成验证码"""
|
||||
return ''.join(random.choices(string.digits, k=length))
|
||||
|
||||
# 发送验证邮件
|
||||
def send_verification_email(email, verification_type='register'):
|
||||
"""
|
||||
发送验证邮件
|
||||
@@ -69,7 +72,7 @@ def send_verification_email(email, verification_type='register'):
|
||||
<body>
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1 style="color: #66bb6a; margin: 0;">InfoGenie 神奇万事通</h1>
|
||||
<h1 style="color: #66bb6a; margin: 0;">InfoGenie 万象口袋</h1>
|
||||
<p style="color: #666; font-size: 14px; margin: 5px 0;">欢迎注册InfoGenie</p>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +101,7 @@ def send_verification_email(email, verification_type='register'):
|
||||
<body>
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1 style="color: #66bb6a; margin: 0;">InfoGenie 神奇万事通</h1>
|
||||
<h1 style="color: #66bb6a; margin: 0;">InfoGenie 万象口袋</h1>
|
||||
<p style="color: #666; font-size: 14px; margin: 5px 0;">安全登录验证</p>
|
||||
</div>
|
||||
|
||||
@@ -168,6 +171,7 @@ def send_verification_email(email, verification_type='register'):
|
||||
'message': '邮件发送失败,请稍后重试'
|
||||
}
|
||||
|
||||
# 验证验证码
|
||||
def verify_code(email, code):
|
||||
"""
|
||||
验证验证码
|
||||
@@ -221,6 +225,7 @@ def verify_code(email, code):
|
||||
'type': verification_type
|
||||
}
|
||||
|
||||
# 验证QQ邮箱格式
|
||||
def is_qq_email(email):
|
||||
"""
|
||||
验证是否为QQ邮箱
|
||||
@@ -239,6 +244,7 @@ def is_qq_email(email):
|
||||
|
||||
return domain in qq_domains
|
||||
|
||||
# 获取QQ头像URL
|
||||
def get_qq_avatar_url(email):
|
||||
"""
|
||||
根据QQ邮箱获取QQ头像URL
|
||||
@@ -262,6 +268,7 @@ def get_qq_avatar_url(email):
|
||||
# 返回QQ头像API URL
|
||||
return f"http://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100"
|
||||
|
||||
# 清理过期验证码
|
||||
def cleanup_expired_codes():
|
||||
"""清理过期的验证码"""
|
||||
current_time = datetime.now()
|
||||
408
InfoGenie-backend/modules/user_management.py
Executable file
408
InfoGenie-backend/modules/user_management.py
Executable file
@@ -0,0 +1,408 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
用户管理模块
|
||||
Created by: 万象口袋
|
||||
Date: 2025-09-02
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from datetime import datetime
|
||||
from bson import ObjectId
|
||||
import jwt
|
||||
from functools import wraps
|
||||
|
||||
user_bp = Blueprint('user', __name__)
|
||||
|
||||
# 验证JWT token
|
||||
def verify_token(token):
|
||||
"""验证JWT token"""
|
||||
try:
|
||||
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
|
||||
return {'success': True, 'data': payload}
|
||||
except jwt.ExpiredSignatureError:
|
||||
return {'success': False, 'message': 'Token已过期'}
|
||||
except jwt.InvalidTokenError:
|
||||
return {'success': False, 'message': 'Token无效'}
|
||||
|
||||
# 登录验证装饰器(支持JWT token和hwt)
|
||||
def login_required(f):
|
||||
"""登录验证装饰器(支持JWT token和hwt)"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# 优先检查JWT token
|
||||
token = request.headers.get('Authorization')
|
||||
if token:
|
||||
if token.startswith('Bearer '):
|
||||
token = token[7:]
|
||||
|
||||
result = verify_token(token)
|
||||
if result['success']:
|
||||
request.current_user = result['data']
|
||||
return f(*args, **kwargs)
|
||||
# 回退到hwt验证
|
||||
hwt = getattr(request, 'hwt', {})
|
||||
if not hwt.get('logged_in'):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请先登录'
|
||||
}), 401
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorated_function
|
||||
|
||||
# 获取用户资料
|
||||
@user_bp.route('/profile', methods=['GET'])
|
||||
@login_required
|
||||
def get_profile():
|
||||
"""获取用户资料"""
|
||||
try:
|
||||
hwt = getattr(request, 'hwt', {})
|
||||
user_id = hwt.get('user_id')
|
||||
users_collection = current_app.mongo.db.userdata
|
||||
user = users_collection.find_one({'_id': ObjectId(user_id)})
|
||||
if not user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '用户不存在'
|
||||
}), 404
|
||||
# 返回用户信息(不包含密码)
|
||||
profile = {
|
||||
'account': user['账号'],
|
||||
'register_time': user.get('注册时间'),
|
||||
'last_login': user.get('最后登录'),
|
||||
'login_count': user.get('登录次数', 0),
|
||||
'status': user.get('用户状态', 'active')
|
||||
}
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': profile
|
||||
}), 200
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 修改密码
|
||||
@user_bp.route('/change-password', methods=['POST'])
|
||||
@login_required
|
||||
def change_password():
|
||||
"""修改密码"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
old_password = data.get('old_password', '').strip()
|
||||
new_password = data.get('new_password', '').strip()
|
||||
|
||||
if not old_password or not new_password:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '旧密码和新密码不能为空'
|
||||
}), 400
|
||||
|
||||
if len(new_password) < 6 or len(new_password) > 20:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '新密码长度必须在6-20位之间'
|
||||
}), 400
|
||||
|
||||
hwt = getattr(request, 'hwt', {})
|
||||
user_id = hwt.get('user_id')
|
||||
users_collection = current_app.mongo.db.userdata
|
||||
user = users_collection.find_one({'_id': ObjectId(user_id)})
|
||||
if not user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '用户不存在'
|
||||
}), 404
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
# 验证旧密码
|
||||
if not check_password_hash(user['密码'], old_password):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '原密码错误'
|
||||
}), 401
|
||||
# 更新密码
|
||||
new_password_hash = generate_password_hash(new_password)
|
||||
result = users_collection.update_one(
|
||||
{'_id': ObjectId(user_id)},
|
||||
{'$set': {'密码': new_password_hash}}
|
||||
)
|
||||
if result.modified_count > 0:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '密码修改成功'
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '密码修改失败'
|
||||
}), 500
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 获取用户统计信息
|
||||
@user_bp.route('/stats', methods=['GET'])
|
||||
@login_required
|
||||
def get_user_stats():
|
||||
"""获取用户统计信息"""
|
||||
try:
|
||||
hwt = getattr(request, 'hwt', {})
|
||||
user_id = hwt.get('user_id')
|
||||
# 这里可以添加更多统计信息,比如API调用次数等
|
||||
stats = {
|
||||
'login_today': 1, # 今日登录次数
|
||||
'api_calls_today': 0, # 今日API调用次数
|
||||
'total_api_calls': 0, # 总API调用次数
|
||||
'join_days': 1, # 加入天数
|
||||
'last_activity': datetime.now().isoformat()
|
||||
}
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': stats
|
||||
}), 200
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 获取用户游戏数据
|
||||
@user_bp.route('/game-data', methods=['GET'])
|
||||
@login_required
|
||||
def get_user_game_data():
|
||||
"""获取用户游戏数据"""
|
||||
try:
|
||||
# 优先从JWT token获取用户ID
|
||||
if hasattr(request, 'current_user'):
|
||||
user_id = request.current_user['user_id']
|
||||
else:
|
||||
hwt = getattr(request, 'hwt', {})
|
||||
user_id = hwt.get('user_id')
|
||||
|
||||
users_collection = current_app.mongo.db.userdata
|
||||
|
||||
user = users_collection.find_one({'_id': ObjectId(user_id)})
|
||||
|
||||
if not user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '用户不存在'
|
||||
}), 404
|
||||
|
||||
# 返回用户游戏数据
|
||||
game_data = {
|
||||
'level': user.get('等级', 0),
|
||||
'experience': user.get('经验', 0),
|
||||
'coins': user.get('萌芽币', 0),
|
||||
'checkin_system': user.get('签到系统', {
|
||||
'连续签到天数': 0,
|
||||
'今日是否已签到': False,
|
||||
'签到时间': '2025-01-01'
|
||||
})
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': game_data
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 每日签到
|
||||
@user_bp.route('/checkin', methods=['POST'])
|
||||
@login_required
|
||||
def daily_checkin():
|
||||
"""每日签到"""
|
||||
try:
|
||||
# 优先从JWT token获取用户ID
|
||||
if hasattr(request, 'current_user'):
|
||||
user_id = request.current_user['user_id']
|
||||
else:
|
||||
hwt = getattr(request, 'hwt', {})
|
||||
user_id = hwt.get('user_id')
|
||||
|
||||
users_collection = current_app.mongo.db.userdata
|
||||
|
||||
user = users_collection.find_one({'_id': ObjectId(user_id)})
|
||||
|
||||
if not user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '用户不存在'
|
||||
}), 404
|
||||
|
||||
# 获取当前日期
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
# 获取签到系统数据
|
||||
checkin_system = user.get('签到系统', {
|
||||
'连续签到天数': 0,
|
||||
'今日是否已签到': False,
|
||||
'签到时间': '2025-01-01'
|
||||
})
|
||||
|
||||
# 检查今日是否已签到
|
||||
if checkin_system.get('今日是否已签到', False) and checkin_system.get('签到时间') == today:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '今日已签到,请明天再来!'
|
||||
}), 400
|
||||
|
||||
# 计算连续签到天数
|
||||
last_checkin_date = checkin_system.get('签到时间', '2025-01-01')
|
||||
consecutive_days = checkin_system.get('连续签到天数', 0)
|
||||
|
||||
# 检查是否连续签到
|
||||
if last_checkin_date:
|
||||
try:
|
||||
last_date = datetime.strptime(last_checkin_date, '%Y-%m-%d')
|
||||
today_date = datetime.strptime(today, '%Y-%m-%d')
|
||||
days_diff = (today_date - last_date).days
|
||||
|
||||
if days_diff == 1:
|
||||
# 连续签到
|
||||
consecutive_days += 1
|
||||
elif days_diff > 1:
|
||||
# 断签,重新开始
|
||||
consecutive_days = 1
|
||||
else:
|
||||
# 同一天,不应该发生
|
||||
consecutive_days = consecutive_days
|
||||
except:
|
||||
consecutive_days = 1
|
||||
else:
|
||||
consecutive_days = 1
|
||||
|
||||
# 签到奖励
|
||||
coin_reward = 300
|
||||
exp_reward = 200
|
||||
|
||||
# 获取当前用户数据
|
||||
current_coins = user.get('萌芽币', 0)
|
||||
current_exp = user.get('经验', 0)
|
||||
current_level = user.get('等级', 0)
|
||||
|
||||
# 计算新的经验和等级
|
||||
new_exp = current_exp + exp_reward
|
||||
new_level = current_level
|
||||
|
||||
# 等级升级逻辑:100 × 1.2^(等级)
|
||||
while True:
|
||||
exp_needed = int(100 * (1.2 ** new_level))
|
||||
if new_exp >= exp_needed:
|
||||
new_exp -= exp_needed
|
||||
new_level += 1
|
||||
else:
|
||||
break
|
||||
|
||||
# 更新用户数据
|
||||
update_data = {
|
||||
'萌芽币': current_coins + coin_reward,
|
||||
'经验': new_exp,
|
||||
'等级': new_level,
|
||||
'签到系统': {
|
||||
'连续签到天数': consecutive_days,
|
||||
'今日是否已签到': True,
|
||||
'签到时间': today
|
||||
}
|
||||
}
|
||||
|
||||
result = users_collection.update_one(
|
||||
{'_id': ObjectId(user_id)},
|
||||
{'$set': update_data}
|
||||
)
|
||||
|
||||
if result.modified_count > 0:
|
||||
level_up = new_level > current_level
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '签到成功!',
|
||||
'data': {
|
||||
'coin_reward': coin_reward,
|
||||
'exp_reward': exp_reward,
|
||||
'consecutive_days': consecutive_days,
|
||||
'level_up': level_up,
|
||||
'new_level': new_level,
|
||||
'new_coins': current_coins + coin_reward,
|
||||
'new_exp': new_exp
|
||||
}
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '签到失败,请稍后重试'
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 删除账户
|
||||
@user_bp.route('/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_account():
|
||||
"""删除账户"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
password = data.get('password', '').strip()
|
||||
|
||||
if not password:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请输入密码确认删除'
|
||||
}), 400
|
||||
|
||||
hwt = getattr(request, 'hwt', {})
|
||||
user_id = hwt.get('user_id')
|
||||
users_collection = current_app.mongo.db.userdata
|
||||
|
||||
user = users_collection.find_one({'_id': ObjectId(user_id)})
|
||||
|
||||
if not user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '用户不存在'
|
||||
}), 404
|
||||
|
||||
from werkzeug.security import check_password_hash
|
||||
|
||||
# 验证密码
|
||||
if not check_password_hash(user['密码'], password):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '密码错误'
|
||||
}), 401
|
||||
|
||||
# 删除用户
|
||||
result = users_collection.delete_one({'_id': ObjectId(user_id)})
|
||||
|
||||
if result.deleted_count > 0:
|
||||
# 清除会话
|
||||
hwt = getattr(request, 'hwt', {})
|
||||
hwt.clear()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '账户已成功删除'
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '删除失败'
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
3
backend/requirements.txt → InfoGenie-backend/requirements.txt
Normal file → Executable file
3
backend/requirements.txt → InfoGenie-backend/requirements.txt
Normal file → Executable file
@@ -10,6 +10,9 @@ pymongo==4.5.0
|
||||
# 密码加密
|
||||
Werkzeug==2.3.7
|
||||
|
||||
# JWT认证
|
||||
PyJWT==2.8.0
|
||||
|
||||
# HTTP请求
|
||||
requests==2.31.0
|
||||
|
||||
2
InfoGenie-backend/start_backend.bat
Executable file
2
InfoGenie-backend/start_backend.bat
Executable file
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
python app.py
|
||||
2
InfoGenie-backend/start_backend.sh
Executable file
2
InfoGenie-backend/start_backend.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
python3 app.py
|
||||
0
backend/test/email_test.py → InfoGenie-backend/test/email_test.py
Normal file → Executable file
0
backend/test/email_test.py → InfoGenie-backend/test/email_test.py
Normal file → Executable file
0
backend/test/mongo_test.py → InfoGenie-backend/test/mongo_test.py
Normal file → Executable file
0
backend/test/mongo_test.py → InfoGenie-backend/test/mongo_test.py
Normal file → Executable file
0
backend/test/test_email.py → InfoGenie-backend/test/test_email.py
Normal file → Executable file
0
backend/test/test_email.py → InfoGenie-backend/test/test_email.py
Normal file → Executable file
0
backend/test/test_email_fix.py → InfoGenie-backend/test/test_email_fix.py
Normal file → Executable file
0
backend/test/test_email_fix.py → InfoGenie-backend/test/test_email_fix.py
Normal file → Executable file
0
backend/test/test_mongo.py → InfoGenie-backend/test/test_mongo.py
Normal file → Executable file
0
backend/test/test_mongo.py → InfoGenie-backend/test/test_mongo.py
Normal file → Executable file
166
InfoGenie-backend/后端架构文档.md
Executable file
166
InfoGenie-backend/后端架构文档.md
Executable file
@@ -0,0 +1,166 @@
|
||||
# InfoGenie后端项目专业技术总结
|
||||
|
||||
## 项目架构概述
|
||||
|
||||
InfoGenie后端采用了**模块化、松耦合**的设计理念,基于Flask框架构建RESTful API服务,实现了前后端完全分离的现代Web应用架构。整体架构遵循了**单一职责原则**和**关注点分离原则**,各模块独立封装,通过清晰定义的API接口进行交互。
|
||||
|
||||
## 核心技术栈
|
||||
|
||||
### 基础框架
|
||||
- **Web框架**: Flask 2.3.3(轻量、灵活、可扩展)
|
||||
- **API设计**: RESTful架构(资源导向、无状态通信)
|
||||
- **数据库**: MongoDB(适用于文档型数据存储,通过Flask-PyMongo 2.3.0集成)
|
||||
- **认证机制**: JWT Token(PyJWT 2.8.0,支持7天有效期)
|
||||
|
||||
### 中间件与辅助工具
|
||||
- **CORS支持**: Flask-CORS 4.0.0(解决跨域资源共享问题)
|
||||
- **密码安全**: Werkzeug 2.3.7(提供高强度密码哈希功能)
|
||||
- **邮件服务**: 基于SMTP协议的邮件发送(使用smtplib直接实现,无依赖Flask-Mail)
|
||||
- **环境配置**: python-dotenv 1.0.0(分离配置与代码,增强安全性)
|
||||
- **API限流**: Flask-Limiter 3.5.0(防止API滥用,提高系统稳定性)
|
||||
|
||||
## 架构设计亮点
|
||||
|
||||
### 1. 应用工厂模式
|
||||
项目采用**应用工厂模式**(Factory Pattern)创建Flask应用实例,便于测试和多环境部署:
|
||||
```python
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
# 初始化各种扩展和注册蓝图
|
||||
return app
|
||||
```
|
||||
|
||||
### 2. 蓝图模块化设计
|
||||
采用Flask蓝图(Blueprint)实现功能模块化,提高代码复用性和可维护性:
|
||||
- `auth_bp`: 用户认证模块
|
||||
- `user_bp`: 用户管理模块
|
||||
- `aimodelapp_bp`: AI模型应用模块
|
||||
|
||||
### 3. 装饰器模式
|
||||
大量使用装饰器模式实现横切关注点(Cross-cutting Concerns)如认证、权限验证、萌芽币消费等:
|
||||
```python
|
||||
@verify_user_coins
|
||||
def ai_function_endpoint():
|
||||
# 业务逻辑
|
||||
```
|
||||
|
||||
### 4. 统一响应格式
|
||||
实现了一致的API响应格式,便于前端处理:
|
||||
```json
|
||||
{
|
||||
"success": true|false,
|
||||
"data": {},
|
||||
"message": "操作信息",
|
||||
"timestamp": "ISO格式时间戳"
|
||||
}
|
||||
```
|
||||
|
||||
## 安全设计分析
|
||||
|
||||
### 1. 多层次认证体系
|
||||
- **JWT Token认证**: 无状态认证机制,适合分布式部署
|
||||
- **验证码邮箱认证**: 双因素认证提高安全性
|
||||
- **QQ邮箱格式验证**: 限制注册邮箱类型,减少垃圾注册
|
||||
|
||||
### 2. 数据安全措施
|
||||
- **密码哈希存储**: 使用Werkzeug提供的高强度哈希算法
|
||||
- **敏感配置外部化**: 通过环境变量注入敏感配置
|
||||
- **路径遍历防护**: 静态文件服务实现了路径限制检查
|
||||
```python
|
||||
if not os.path.commonpath([base_directory, full_path]) == base_directory:
|
||||
return jsonify({'error': '非法文件路径'}), 403
|
||||
```
|
||||
|
||||
### 3. 请求安全控制
|
||||
- **API限流**: 防止暴力攻击和资源耗尽
|
||||
- **CORS限制**: 生产环境可配置严格的跨域策略
|
||||
- **请求参数验证**: 严格验证所有客户端输入
|
||||
|
||||
## 业务模块分析
|
||||
|
||||
### 1. 认证模块(auth.py)
|
||||
实现了基于JWT的无状态认证系统,通过邮箱验证码进行用户身份确认,支持注册、登录和会话管理。设计重点包括:
|
||||
- 验证码5分钟有效期机制
|
||||
- JWT token 7天有效期管理
|
||||
- 认证装饰器实现代码复用
|
||||
|
||||
### 2. 用户管理模块(user_management.py)
|
||||
负责用户资料、签到系统、萌芽币管理等核心业务功能,实现了:
|
||||
- 用户资料CRUD操作
|
||||
- 每日签到奖励系统(经验值和萌芽币)
|
||||
- 用户等级动态计算逻辑
|
||||
|
||||
### 3. AI模型应用模块(aimodelapp.py)
|
||||
集成多种AI服务(DeepSeek、Kimi)并实现统一接口调用,特点:
|
||||
- 萌芽币消费装饰器模式(每次调用消耗100萌芽币)
|
||||
- AI调用带重试机制(提高系统稳定性)
|
||||
- 多模型提供商支持(提高可用性和容错性)
|
||||
|
||||
### 4. 邮件服务模块(email_service.py)
|
||||
负责验证码邮件发送、QQ邮箱格式验证等功能,特点:
|
||||
- 直接使用smtplib实现,减少依赖
|
||||
- HTML格式邮件模板支持
|
||||
- 验证码管理机制(内存存储,生产环境建议使用Redis)
|
||||
|
||||
## 数据库设计
|
||||
|
||||
采用MongoDB文档型数据库,主要集合为`userdata`,存储用户相关所有数据。MongoDB的选择优势:
|
||||
- **灵活的数据结构**: 适合存储复杂且不断演化的用户数据
|
||||
- **文档自包含**: 减少关联查询,提高读取性能
|
||||
- **水平扩展能力**: 支持未来系统规模扩展需求
|
||||
|
||||
用户数据模型设计合理,包含核心字段:
|
||||
```json
|
||||
{
|
||||
"邮箱": "user@qq.com",
|
||||
"用户名": "用户名",
|
||||
"密码": "哈希密码",
|
||||
"头像": "QQ头像URL",
|
||||
"注册时间": "ISO时间格式",
|
||||
"萌芽币": 1500,
|
||||
"签到系统": {
|
||||
"连续签到天数": 7,
|
||||
"今日是否已签到": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 部署与运维
|
||||
|
||||
### 多环境配置支持
|
||||
实现了开发、测试和生产环境的配置分离:
|
||||
```python
|
||||
config = {
|
||||
'development': DevelopmentConfig,
|
||||
'production': ProductionConfig,
|
||||
'testing': TestingConfig,
|
||||
'default': DevelopmentConfig
|
||||
}
|
||||
```
|
||||
|
||||
### Docker化部署
|
||||
提供了完整的Docker化部署方案:
|
||||
- Dockerfile定义应用容器
|
||||
- docker-compose.yml配置多容器协作
|
||||
- 支持环境变量注入敏感配置
|
||||
|
||||
## 技术亮点与优化空间
|
||||
|
||||
### 亮点
|
||||
1. **模块化设计**: 通过Flask蓝图实现功能解耦
|
||||
2. **装饰器封装**: 横切关注点(cross-cutting concerns)集中处理
|
||||
3. **统一错误处理**: 全局一致的错误响应机制
|
||||
4. **AI服务抽象**: 屏蔽不同AI提供商的实现差异
|
||||
|
||||
### 优化空间
|
||||
1. **缓存机制**: 可引入Redis缓存验证码、热点数据等
|
||||
2. **异步处理**: 邮件发送、AI调用等耗时操作可改为异步执行
|
||||
3. **日志系统**: 增强日志记录和监控能力
|
||||
4. **单元测试**: 增加自动化测试覆盖率
|
||||
|
||||
## 结论
|
||||
|
||||
InfoGenie后端项目展现了良好的软件工程实践,采用模块化设计、RESTful API架构和多层次安全控制,构建了一个可扩展、可维护的后端系统。该项目不仅满足了当前的业务需求,还为未来功能扩展和性能优化预留了空间。
|
||||
|
||||
特别是在AI功能集成方面,通过抽象接口和装饰器模式,实现了业务逻辑与技术实现的分离,体现了良好的软件设计原则。萌芽币消费系统的实现也展示了面向业务模型的领域设计能力。
|
||||
13
InfoGenie-backend/用户数据模板.json
Executable file
13
InfoGenie-backend/用户数据模板.json
Executable file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"账号":"3205788256",
|
||||
"邮箱":"3205788256@qq.com",
|
||||
"密码":"0123456789",
|
||||
"等级":0,
|
||||
"经验":0,
|
||||
"萌芽币":0,
|
||||
"签到系统":{
|
||||
"连续签到天数":0,
|
||||
"今日是否已签到":false,
|
||||
"签到时间":"2025-01-01"
|
||||
}
|
||||
}
|
||||
4
InfoGenie-frontend/build_frontend.bat
Executable file
4
InfoGenie-frontend/build_frontend.bat
Executable file
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
npm run build
|
||||
npx serve -s build
|
||||
pause
|
||||
5
InfoGenie-frontend/env.backup
Executable file
5
InfoGenie-frontend/env.backup
Executable file
@@ -0,0 +1,5 @@
|
||||
# 生产环境API配置
|
||||
REACT_APP_API_URL=https://infogenie.api.shumengya.top
|
||||
|
||||
# 生产环境API配置
|
||||
REACT_APP_API_URL=http://127.0.0.1:5002
|
||||
0
frontend/react-app/package-lock.json → InfoGenie-frontend/package-lock.json
generated
Normal file → Executable file
0
frontend/react-app/package-lock.json → InfoGenie-frontend/package-lock.json
generated
Normal file → Executable file
8
frontend/react-app/package.json → InfoGenie-frontend/package.json
Normal file → Executable file
8
frontend/react-app/package.json → InfoGenie-frontend/package.json
Normal file → Executable file
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "infogenie-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "✨ 神奇万事通 - 前端React应用",
|
||||
"description": "✨ 万象口袋 - 前端React应用",
|
||||
"keywords": ["react", "api", "mobile-first", "responsive"],
|
||||
"author": "神奇万事通",
|
||||
"author": "万象口袋",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"homepage": "/",
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
@@ -44,6 +45,5 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:5000"
|
||||
}
|
||||
}
|
||||
0
frontend/60sapi/娱乐消遣/随机JavaScript趣味题/css/style.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机JavaScript趣味题/css/style.css
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机JavaScript趣味题/css/style.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机JavaScript趣味题/css/style.css
Normal file → Executable file
@@ -4,11 +4,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>随机JavaScript趣味题</title>
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
||||
<link rel="dns-prefetch" href="https://60s.api.shumengya.top">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/background.css">
|
||||
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
|
||||
<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/languages/javascript.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
29
frontend/60sapi/娱乐消遣/随机JavaScript趣味题/js/script.js → InfoGenie-frontend/public/60sapi/娱乐消遣/随机JavaScript趣味题/js/script.js
Normal file → Executable file
29
frontend/60sapi/娱乐消遣/随机JavaScript趣味题/js/script.js → InfoGenie-frontend/public/60sapi/娱乐消遣/随机JavaScript趣味题/js/script.js
Normal file → Executable file
@@ -2,19 +2,17 @@
|
||||
class JSQuizApp {
|
||||
constructor() {
|
||||
this.apiEndpoints = [
|
||||
'https://60s-cf.viki.moe',
|
||||
'https://60s.viki.moe',
|
||||
'https://60s.b23.run',
|
||||
'https://60s.114128.xyz',
|
||||
'https://60s-cf.114128.xyz'
|
||||
'https://60s.api.shumengya.top',
|
||||
];
|
||||
this.currentApiIndex = 0;
|
||||
this.currentQuestion = null;
|
||||
this.selectedOption = null;
|
||||
this.isAnswered = false;
|
||||
this.loadStartTime = null;
|
||||
|
||||
this.initElements();
|
||||
this.bindEvents();
|
||||
this.preloadResources();
|
||||
this.loadQuestion();
|
||||
}
|
||||
|
||||
@@ -41,6 +39,17 @@ class JSQuizApp {
|
||||
};
|
||||
}
|
||||
|
||||
// 预加载资源
|
||||
preloadResources() {
|
||||
// 预连接API服务器
|
||||
this.apiEndpoints.forEach(endpoint => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'preconnect';
|
||||
link.href = endpoint;
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvents() {
|
||||
this.elements.submitBtn.addEventListener('click', () => this.submitAnswer());
|
||||
@@ -84,6 +93,7 @@ class JSQuizApp {
|
||||
|
||||
// 加载题目
|
||||
async loadQuestion() {
|
||||
this.loadStartTime = Date.now();
|
||||
this.showLoading();
|
||||
this.resetQuestion();
|
||||
|
||||
@@ -92,15 +102,20 @@ class JSQuizApp {
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const response = await fetch(this.getCurrentApiUrl(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 10000
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
@@ -109,6 +124,8 @@ class JSQuizApp {
|
||||
|
||||
if (data.code === 200 && data.data) {
|
||||
this.currentQuestion = data.data;
|
||||
const loadTime = Date.now() - this.loadStartTime;
|
||||
console.log(`题目加载完成,耗时: ${loadTime}ms`);
|
||||
this.displayQuestion();
|
||||
return;
|
||||
} else {
|
||||
0
frontend/60sapi/娱乐消遣/随机JavaScript趣味题/接口集合.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机JavaScript趣味题/接口集合.json
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机JavaScript趣味题/接口集合.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机JavaScript趣味题/接口集合.json
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机JavaScript趣味题/返回接口.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机JavaScript趣味题/返回接口.json
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机JavaScript趣味题/返回接口.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机JavaScript趣味题/返回接口.json
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机KFC文案/css/background.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机KFC文案/css/background.css
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机KFC文案/css/background.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机KFC文案/css/background.css
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机KFC文案/css/style.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机KFC文案/css/style.css
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机KFC文案/css/style.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机KFC文案/css/style.css
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机KFC文案/index.html → InfoGenie-frontend/public/60sapi/娱乐消遣/随机KFC文案/index.html
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机KFC文案/index.html → InfoGenie-frontend/public/60sapi/娱乐消遣/随机KFC文案/index.html
Normal file → Executable file
4
frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/js/main.js → InfoGenie-frontend/public/60sapi/娱乐消遣/随机KFC文案/js/main.js
Normal file → Executable file
4
frontend/react-app/public/60sapi/娱乐消遣/随机KFC文案/js/main.js → InfoGenie-frontend/public/60sapi/娱乐消遣/随机KFC文案/js/main.js
Normal file → Executable file
@@ -17,8 +17,8 @@ class KFCGenerator {
|
||||
// 加载API接口列表
|
||||
async loadApiEndpoints() {
|
||||
try {
|
||||
const response = await fetch('./接口集合.json');
|
||||
this.apiEndpoints = await response.json();
|
||||
// 直接硬编码API端点,避免CORS问题
|
||||
this.apiEndpoints = ["https://60s.api.shumengya.top"];
|
||||
} catch (error) {
|
||||
console.error('加载API接口列表失败:', error);
|
||||
this.showToast('加载接口配置失败', 'error');
|
||||
0
frontend/60sapi/娱乐消遣/随机KFC文案/接口集合.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机KFC文案/接口集合.json
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机KFC文案/接口集合.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机KFC文案/接口集合.json
Normal file → Executable file
8
InfoGenie-frontend/public/60sapi/娱乐消遣/随机KFC文案/返回接口.json
Executable file
8
InfoGenie-frontend/public/60sapi/娱乐消遣/随机KFC文案/返回接口.json
Executable file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||
"data": {
|
||||
"index": 78,
|
||||
"kfc": "我叫夯大力 立冬给我准备了糖炒栗子了没有 没准备的自动绝交 再 v 我 50 吃疯狂星期四 然后再给我点杯奶茶 再给我两万块钱 懂吗你们"
|
||||
}
|
||||
}
|
||||
0
frontend/60sapi/娱乐消遣/随机一言/css/background.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/css/background.css
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机一言/css/background.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/css/background.css
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机一言/css/style.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/css/style.css
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机一言/css/style.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/css/style.css
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机一言/index.html → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/index.html
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机一言/index.html → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/index.html
Normal file → Executable file
9
frontend/react-app/public/60sapi/娱乐消遣/随机一言/js/script.js → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/js/script.js
Normal file → Executable file
9
frontend/react-app/public/60sapi/娱乐消遣/随机一言/js/script.js → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/js/script.js
Normal file → Executable file
@@ -4,11 +4,7 @@ class HitokotoApp {
|
||||
constructor() {
|
||||
// API接口列表
|
||||
this.apiEndpoints = [
|
||||
"https://60s-cf.viki.moe",
|
||||
"https://60s.viki.moe",
|
||||
"https://60s.b23.run",
|
||||
"https://60s.114128.xyz",
|
||||
"https://60s-cf.114128.xyz"
|
||||
"https://60s.api.shumengya.top"
|
||||
];
|
||||
|
||||
this.currentEndpointIndex = 0;
|
||||
@@ -117,7 +113,8 @@ class HitokotoApp {
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
|
||||
|
||||
try {
|
||||
const response = await fetch(`${endpoint}/v2/hitokoto?encoding=text`, {
|
||||
// 移除URL中的encoding=text参数,确保返回JSON格式
|
||||
const response = await fetch(`${endpoint}/v2/hitokoto`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
0
frontend/60sapi/娱乐消遣/随机一言/接口集合.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/接口集合.json
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机一言/接口集合.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/接口集合.json
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机一言/返回接口.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/返回接口.json
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机一言/返回接口.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/返回接口.json
Normal file → Executable file
16
InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/css/Untitled-1.html
Executable file
16
InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/css/Untitled-1.html
Executable file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>每日笑话</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<p id="joke">加载中...</p>
|
||||
<button id="next">换一个</button>
|
||||
</div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
107
InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/css/background.css
Executable file
107
InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/css/background.css
Executable file
@@ -0,0 +1,107 @@
|
||||
/* background.css - 动态渐变背景 */
|
||||
body {
|
||||
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
:root {
|
||||
--bg-yellow: #FFFDE7; /* 浅黄 */
|
||||
--bg-blue: #E3F2FD; /* 淡蓝 */
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(180deg, var(--bg-yellow) 0%, var(--bg-blue) 100%);
|
||||
background-attachment: fixed; /* 固定背景,滚动时不移动 */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
/* Light Theme (Default) */
|
||||
[data-theme="light"] {
|
||||
background: linear-gradient(to bottom, #87CEEB, #B0E0E6);
|
||||
}
|
||||
|
||||
/* Dark Theme */
|
||||
[data-theme="dark"] {
|
||||
background: linear-gradient(to bottom, #232526, #414345);
|
||||
}
|
||||
[data-theme="dark"] .snowflake {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Winter Theme */
|
||||
[data-theme="winter"] {
|
||||
background: linear-gradient(to bottom, #a1c4fd, #c2e9fb);
|
||||
}
|
||||
[data-theme="winter"] .background-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background: linear-gradient(to top, white, rgba(255, 255, 255, 0));
|
||||
z-index: -1;
|
||||
border-radius: 50% 50% 0 0 / 20px;
|
||||
box-shadow: 0 -10px 20px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
|
||||
#snowflake-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.snowflake {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
user-select: none;
|
||||
animation: fall linear infinite;
|
||||
}
|
||||
|
||||
@keyframes fall {
|
||||
to {
|
||||
transform: translateY(105vh) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#frost-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('https://www.transparenttextures.com/patterns/ice-age.png') repeat;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#frost-overlay.is-frosted {
|
||||
opacity: 0.3;
|
||||
}
|
||||
217
InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/css/style.css
Executable file
217
InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/css/style.css
Executable file
@@ -0,0 +1,217 @@
|
||||
:root {
|
||||
--primary-color-light: #4A90E2;
|
||||
--text-color-light: #333;
|
||||
--card-bg-light: rgba(255, 255, 255, 0.85);
|
||||
|
||||
--primary-color-dark: #5271C4;
|
||||
--text-color-dark: #E0E0E0;
|
||||
--card-bg-dark: rgba(40, 40, 40, 0.85);
|
||||
|
||||
--primary-color-winter: #6A82FB;
|
||||
--text-color-winter: #2c3e50;
|
||||
--card-bg-winter: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-nav {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
padding: 5px;
|
||||
border-radius: 50px;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.theme-switcher {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.theme-btn {
|
||||
background: transparent;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, border-color 0.2s;
|
||||
}
|
||||
.theme-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.theme-btn.active {
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: 'ZCOOL KuaiLe', cursive;
|
||||
font-size: 3em;
|
||||
margin-bottom: 20px;
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
|
||||
.joke-stream {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.joke-card {
|
||||
border-radius: 20px;
|
||||
padding: 30px 40px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: background-color 0.5s ease, border-color 0.5s ease;
|
||||
}
|
||||
|
||||
#joke-text {
|
||||
font-size: 1.5em;
|
||||
line-height: 1.6;
|
||||
transition: opacity 0.3s, color 0.5s ease;
|
||||
}
|
||||
|
||||
/* --- Theming --- */
|
||||
|
||||
/* Light Theme */
|
||||
[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-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"] footer { color: rgba(255, 255, 255, 0.8); }
|
||||
|
||||
/* Dark Theme */
|
||||
[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-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"] footer { color: rgba(200, 200, 200, 0.7); }
|
||||
|
||||
/* Winter Theme */
|
||||
[data-theme="winter"] .title { color: #1e3a5f; text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.7); }
|
||||
[data-theme="winter"] .joke-card {
|
||||
background-color: var(--card-bg-winter);
|
||||
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);
|
||||
}
|
||||
[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"] footer { color: #1e3a5f; }
|
||||
|
||||
|
||||
.controls {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
#new-joke-btn {
|
||||
color: white;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
padding: 15px 35px;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.5s ease;
|
||||
}
|
||||
|
||||
#new-joke-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
#new-joke-btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.interactions {
|
||||
margin-top: 25px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.interaction-btn {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, background-color 0.2s;
|
||||
}
|
||||
|
||||
.interaction-btn:hover {
|
||||
transform: scale(1.1);
|
||||
background: white;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
font-size: 0.9em;
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
|
||||
/* Loader */
|
||||
#loader {
|
||||
position: absolute;
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
[data-theme="light"] #loader { color: var(--primary-color-light); }
|
||||
[data-theme="dark"] #loader { color: var(--primary-color-dark); }
|
||||
[data-theme="winter"] #loader { color: var(--primary-color-winter); }
|
||||
|
||||
.snowflake-loader {
|
||||
font-size: 40px;
|
||||
display: inline-block;
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
.snowflake-loader::before {
|
||||
content: '❄';
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 600px) {
|
||||
.title {
|
||||
font-size: 2.5em;
|
||||
}
|
||||
.joke-card {
|
||||
padding: 25px;
|
||||
}
|
||||
#joke-text {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.top-nav {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
58
InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/index.html
Executable file
58
InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/index.html
Executable file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>随机冷笑话</title>
|
||||
<link 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/style.css">
|
||||
</head>
|
||||
<body data-theme="light">
|
||||
<div id="snowflake-container"></div>
|
||||
<div id="frost-overlay"></div>
|
||||
<div class="background-bottom"></div>
|
||||
|
||||
<nav class="top-nav">
|
||||
<div class="theme-switcher">
|
||||
<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="winter" title="冰雪风">❄️</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1 class="title">冷笑话生成器</h1>
|
||||
</header>
|
||||
|
||||
<main class="joke-card">
|
||||
<div id="loader" class="hidden">
|
||||
<div class="snowflake-loader"></div>
|
||||
<p>思考中...</p>
|
||||
</div>
|
||||
<p id="joke-text">点击下面的按钮,来点冷笑话吧!</p>
|
||||
</main>
|
||||
|
||||
<div class="controls">
|
||||
<button id="new-joke-btn">再来一个</button>
|
||||
</div>
|
||||
|
||||
<div class="interactions">
|
||||
<button class="interaction-btn" id="like-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="share-btn" title="分享">🔗</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>© 2024 冷笑话工坊</p>
|
||||
</footer>
|
||||
|
||||
<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>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
117
InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/js/script.js
Executable file
117
InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/js/script.js
Executable file
@@ -0,0 +1,117 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const jokeTextElem = document.getElementById('joke-text');
|
||||
const newJokeBtn = document.getElementById('new-joke-btn');
|
||||
const snowflakeContainer = document.getElementById('snowflake-container');
|
||||
const frostOverlay = document.getElementById('frost-overlay');
|
||||
const windSound = document.getElementById('wind-sound');
|
||||
const snowSound = document.getElementById('snow-sound');
|
||||
const loader = document.getElementById('loader');
|
||||
const themeBtns = document.querySelectorAll('.theme-btn');
|
||||
|
||||
const apiEndpoints = [
|
||||
'https://60s.api.shumengya.top/v2/dad-joke',
|
||||
];
|
||||
let currentApiIndex = 0;
|
||||
|
||||
async function fetchJoke() {
|
||||
jokeTextElem.classList.add('hidden');
|
||||
loader.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(apiEndpoints[currentApiIndex]);
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
|
||||
const data = await response.json();
|
||||
if (data.code === 200 && data.data.content) {
|
||||
updateJokeText(data.data.content);
|
||||
if (document.body.dataset.theme === 'winter' && Math.random() < 0.3) {
|
||||
triggerFrostEffect();
|
||||
}
|
||||
} else {
|
||||
throw new Error('API returned invalid data');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
currentApiIndex = (currentApiIndex + 1) % apiEndpoints.length;
|
||||
if (currentApiIndex !== 0) {
|
||||
fetchJoke();
|
||||
} else {
|
||||
jokeTextElem.textContent = '冰箱坏了,暂时没有冷笑话...';
|
||||
}
|
||||
} finally {
|
||||
loader.classList.add('hidden');
|
||||
jokeTextElem.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function updateJokeText(text) {
|
||||
jokeTextElem.textContent = '';
|
||||
let i = 0;
|
||||
const typing = setInterval(() => {
|
||||
if (i < text.length) {
|
||||
jokeTextElem.textContent += text.charAt(i);
|
||||
i++;
|
||||
} else {
|
||||
clearInterval(typing);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function createSnowflakes() {
|
||||
const snowflakeCount = document.body.dataset.theme === 'dark' ? 50 : 30;
|
||||
snowflakeContainer.innerHTML = '';
|
||||
for (let i = 0; i < snowflakeCount; i++) {
|
||||
const snowflake = document.createElement('div');
|
||||
snowflake.className = 'snowflake';
|
||||
snowflake.textContent = '❄️';
|
||||
|
||||
snowflake.style.left = `${Math.random() * 100}vw`;
|
||||
snowflake.style.fontSize = `${Math.random() * 15 + 10}px`;
|
||||
snowflake.style.opacity = Math.random() * 0.5 + 0.3;
|
||||
|
||||
const duration = Math.random() * 10 + 8;
|
||||
const delay = Math.random() * 10;
|
||||
|
||||
snowflake.style.animation = `fall ${duration}s linear ${delay}s infinite`;
|
||||
|
||||
snowflakeContainer.appendChild(snowflake);
|
||||
}
|
||||
}
|
||||
|
||||
function triggerFrostEffect() {
|
||||
frostOverlay.classList.add('is-frosted');
|
||||
windSound.play().catch(e => console.error("Audio play failed:", e));
|
||||
setTimeout(() => {
|
||||
frostOverlay.classList.remove('is-frosted');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
document.body.dataset.theme = theme;
|
||||
localStorage.setItem('joke-theme', theme);
|
||||
|
||||
themeBtns.forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.themeTarget === theme);
|
||||
});
|
||||
|
||||
if (theme === 'winter') {
|
||||
snowSound.play().catch(e => console.error("Audio play failed:", e));
|
||||
}
|
||||
|
||||
// Recreate snowflakes for theme-specific density
|
||||
createSnowflakes();
|
||||
}
|
||||
|
||||
themeBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
setTheme(btn.dataset.themeTarget);
|
||||
});
|
||||
});
|
||||
|
||||
newJokeBtn.addEventListener('click', fetchJoke);
|
||||
|
||||
// Initial setup
|
||||
const savedTheme = localStorage.getItem('joke-theme') || 'light';
|
||||
setTheme(savedTheme);
|
||||
fetchJoke();
|
||||
});
|
||||
1
InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/接口集合.json
Executable file
1
InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/接口集合.json
Executable file
@@ -0,0 +1 @@
|
||||
["https://60s.api.shumengya.top"]
|
||||
4
frontend/react-app/public/60sapi/娱乐消遣/随机一言/返回接口.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/返回接口.json
Normal file → Executable file
4
frontend/react-app/public/60sapi/娱乐消遣/随机一言/返回接口.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机冷笑话/返回接口.json
Normal file → Executable file
@@ -2,7 +2,7 @@
|
||||
"code": 200,
|
||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||
"data": {
|
||||
"index": 2862,
|
||||
"hitokoto": "你带上罪恶之冠,即使背负上所有罪恶和孤独,绝不让你受伤"
|
||||
"index": 121,
|
||||
"content": "这个世界上谁最懂猪?蜘蛛(知猪)人。"
|
||||
}
|
||||
}
|
||||
90
InfoGenie-frontend/public/60sapi/娱乐消遣/随机发病文学/css/background.css
Executable file
90
InfoGenie-frontend/public/60sapi/娱乐消遣/随机发病文学/css/background.css
Executable file
@@ -0,0 +1,90 @@
|
||||
body {
|
||||
background-color: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#bg-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -2;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease-out;
|
||||
}
|
||||
|
||||
.floating-emoji {
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
opacity: 0;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
@keyframes float-top-to-bottom {
|
||||
0% { transform: translateY(-10vh) rotate(0deg); opacity: 0; }
|
||||
10%, 90% { opacity: 0.7; }
|
||||
100% { transform: translateY(110vh) rotate(360deg); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes float-bottom-to-top {
|
||||
0% { transform: translateY(110vh) rotate(0deg); opacity: 0; }
|
||||
10%, 90% { opacity: 0.7; }
|
||||
100% { transform: translateY(-10vh) rotate(360deg); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes float-left-to-right {
|
||||
0% { transform: translateX(-10vw) rotate(0deg); opacity: 0; }
|
||||
10%, 90% { opacity: 0.7; }
|
||||
100% { transform: translateX(110vw) rotate(360deg); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes float-right-to-left {
|
||||
0% { transform: translateX(110vw) rotate(0deg); opacity: 0; }
|
||||
10%, 90% { opacity: 0.7; }
|
||||
100% { transform: translateX(-10vw) rotate(360deg); opacity: 0; }
|
||||
}
|
||||
|
||||
.text-fragment {
|
||||
position: absolute;
|
||||
font-size: 24px;
|
||||
color: rgba(255, 0, 255, 0.4);
|
||||
opacity: 0;
|
||||
animation: float-fragment 15s linear infinite, fade-in-out 15s linear infinite;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@keyframes float-fragment {
|
||||
0% { transform: translate(0, 0) rotate(0deg); }
|
||||
25% { transform: translate(20px, 40px) rotate(15deg); }
|
||||
50% { transform: translate(-30px, -10px) rotate(-10deg); }
|
||||
75% { transform: translate(10px, -30px) rotate(5deg); }
|
||||
100% { transform: translate(0, 0) rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes fade-in-out {
|
||||
0%, 100% { opacity: 0; }
|
||||
10%, 90% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
.screen-crack {
|
||||
position: absolute;
|
||||
width: 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>');
|
||||
opacity: 0;
|
||||
animation: flicker-crack 25s steps(1, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes flicker-crack {
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: 0.3; }
|
||||
51% { opacity: 0; }
|
||||
75% { opacity: 0.2; }
|
||||
76% { opacity: 0; }
|
||||
}
|
||||
235
InfoGenie-frontend/public/60sapi/娱乐消遣/随机发病文学/css/style.css
Executable file
235
InfoGenie-frontend/public/60sapi/娱乐消遣/随机发病文学/css/style.css
Executable file
@@ -0,0 +1,235 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
background: rgba(20, 20, 20, 0.7);
|
||||
border: none;
|
||||
padding: 40px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
backdrop-filter: blur(5px);
|
||||
position: relative;
|
||||
clip-path: polygon(2% 5%, 97% 0%, 100% 95%, 0% 100%);
|
||||
}
|
||||
|
||||
.body-animated .content-card {
|
||||
animation: tremble 0.4s infinite, glitch-shadow 1.5s steps(1, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes tremble {
|
||||
0% { clip-path: polygon(2% 5%, 97% 0%, 100% 95%, 0% 100%); }
|
||||
25% { clip-path: polygon(2% 5%, 98% 2%, 99% 100%, 1% 98%); }
|
||||
50% { clip-path: polygon(3% 4%, 96% 1%, 100% 96%, 2% 100%); }
|
||||
75% { clip-path: polygon(1% 6%, 97% 3%, 98% 95%, 0% 99%); }
|
||||
100% { clip-path: polygon(2% 5%, 97% 0%, 100% 95%, 0% 100%); }
|
||||
}
|
||||
|
||||
@keyframes glitch-shadow {
|
||||
0% {
|
||||
box-shadow:
|
||||
0 0 8px rgba(255, 0, 255, 0.5),
|
||||
inset 0 0 8px rgba(255, 0, 255, 0.4);
|
||||
}
|
||||
33% {
|
||||
box-shadow:
|
||||
0 0 8px rgba(0, 255, 255, 0.5),
|
||||
inset 0 0 8px rgba(0, 255, 255, 0.4);
|
||||
}
|
||||
66% {
|
||||
box-shadow:
|
||||
0 0 8px rgba(0, 255, 0, 0.5),
|
||||
inset 0 0 8px rgba(0, 255, 0, 0.4);
|
||||
}
|
||||
100% {
|
||||
box-shadow:
|
||||
0 0 8px rgba(255, 0, 255, 0.5),
|
||||
inset 0 0 8px rgba(255, 0, 255, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
#literature-text {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.8;
|
||||
min-height: 100px;
|
||||
color: #e0e0e0;
|
||||
text-shadow: 0 0 5px rgba(0, 255, 135, 0.5);
|
||||
animation: text-flicker 15s linear infinite;
|
||||
}
|
||||
|
||||
.body-animated #literature-text {
|
||||
animation: text-flicker 15s linear infinite, text-shadow-glitch 2s steps(1, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes text-flicker {
|
||||
0%, 100% { opacity: 1; }
|
||||
50.0% { opacity: 0.95; }
|
||||
50.5% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes text-shadow-glitch {
|
||||
0% {
|
||||
text-shadow:
|
||||
1px 0 0 rgba(255,0,255,0.5),
|
||||
-1px 0 0 rgba(0,255,255,0.5);
|
||||
}
|
||||
10% {
|
||||
text-shadow:
|
||||
-1px 0 0 rgba(255,0,255,0.5),
|
||||
1px 0 0 rgba(0,255,255,0.5);
|
||||
}
|
||||
11%, 100% {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
#new-literature-btn {
|
||||
background-color: #ff00ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 12px 25px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 0 10px #ff00ff, 0 0 20px #ff00ff;
|
||||
}
|
||||
|
||||
#new-literature-btn:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 15px #ff00ff, 0 0 30px #ff00ff;
|
||||
}
|
||||
|
||||
#new-literature-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Animation Toggle Switch */
|
||||
.switch-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #ff00ff;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Glitch Overlay & Animations */
|
||||
#glitch-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.body-animated #glitch-overlay {
|
||||
animation: color-shift 15s steps(1, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes color-shift {
|
||||
0%, 100% { background: transparent; }
|
||||
10% { background: rgba(255, 0, 0, 0.05); }
|
||||
10.1% { background: transparent; }
|
||||
20% { background: rgba(0, 255, 0, 0.05); }
|
||||
20.1% { background: transparent; }
|
||||
30% { background: rgba(0, 0, 255, 0.05); }
|
||||
30.1% { background: transparent; }
|
||||
}
|
||||
|
||||
.flicker-block {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.body-animated .flicker-block {
|
||||
animation: flicker 3s infinite;
|
||||
}
|
||||
|
||||
@keyframes flicker {
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.content-card {
|
||||
padding: 20px;
|
||||
}
|
||||
#literature-text {
|
||||
font-size: 1em;
|
||||
}
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
34
InfoGenie-frontend/public/60sapi/娱乐消遣/随机发病文学/index.html
Executable file
34
InfoGenie-frontend/public/60sapi/娱乐消遣/随机发病文学/index.html
Executable file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>随机发病文学</title>
|
||||
<link rel="stylesheet" href="css/background.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="bg-container">
|
||||
</div>
|
||||
<div id="glitch-overlay"></div>
|
||||
|
||||
<div class="container">
|
||||
<div class="content-card">
|
||||
<p id="literature-text">正在加载发病文学...</p>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="new-literature-btn">再疯一次</button>
|
||||
<div class="switch-container">
|
||||
<label for="animation-toggle">关闭动画</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="animation-toggle" checked>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
147
InfoGenie-frontend/public/60sapi/娱乐消遣/随机发病文学/js/script.js
Executable file
147
InfoGenie-frontend/public/60sapi/娱乐消遣/随机发病文学/js/script.js
Executable file
@@ -0,0 +1,147 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const literatureTextElem = document.getElementById('literature-text');
|
||||
const newLiteratureBtn = document.getElementById('new-literature-btn');
|
||||
const animationToggle = document.getElementById('animation-toggle');
|
||||
const bgContainer = document.getElementById('bg-container');
|
||||
const body = document.body;
|
||||
|
||||
const apiEndpoints = [
|
||||
'https://60s.api.shumengya.top/v2/fabing',
|
||||
// Add fallback APIs here if available
|
||||
];
|
||||
|
||||
let currentApiIndex = 0;
|
||||
|
||||
async function fetchLiterature() {
|
||||
literatureTextElem.textContent = '正在卖力发疯中...';
|
||||
literatureTextElem.style.opacity = '0.5';
|
||||
|
||||
try {
|
||||
const response = await fetch(apiEndpoints[currentApiIndex]);
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data.code === 200) {
|
||||
literatureTextElem.textContent = data.data.saying;
|
||||
} else {
|
||||
throw new Error('API returned an error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
currentApiIndex = (currentApiIndex + 1) % apiEndpoints.length;
|
||||
if (currentApiIndex !== 0) {
|
||||
fetchLiterature(); // Retry with the next API
|
||||
} else {
|
||||
literatureTextElem.textContent = '疯不起来了,请稍后再试。';
|
||||
}
|
||||
} finally {
|
||||
literatureTextElem.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
|
||||
function createFloatingEmojis() {
|
||||
const existingEmojis = bgContainer.querySelectorAll('.floating-emoji');
|
||||
existingEmojis.forEach(e => e.remove());
|
||||
|
||||
const emojis = ['🤯', '😵', '🤪', '🥴', '🤡', '👹', '👻', '💀', '💥', '🔥', '🌪️', '😵💫'];
|
||||
const animationNames = ['float-top-to-bottom', 'float-bottom-to-top', 'float-left-to-right', 'float-right-to-left'];
|
||||
const emojiCount = 25;
|
||||
|
||||
for (let i = 0; i < emojiCount; i++) {
|
||||
const emojiEl = document.createElement('div');
|
||||
emojiEl.className = 'floating-emoji';
|
||||
emojiEl.textContent = emojis[Math.floor(Math.random() * emojis.length)];
|
||||
|
||||
const animationName = animationNames[Math.floor(Math.random() * animationNames.length)];
|
||||
emojiEl.style.animationName = animationName;
|
||||
emojiEl.style.animationDuration = `${Math.random() * 10 + 15}s`; // 15-25 seconds
|
||||
emojiEl.style.animationDelay = `${Math.random() * 20}s`;
|
||||
emojiEl.style.fontSize = `${Math.random() * 20 + 20}px`;
|
||||
|
||||
// Set initial position based on animation direction
|
||||
if (animationName.includes('top') || animationName.includes('bottom')) { // Vertical movement
|
||||
emojiEl.style.left = `${Math.random() * 100}vw`;
|
||||
} else { // Horizontal movement
|
||||
emojiEl.style.top = `${Math.random() * 100}vh`;
|
||||
}
|
||||
|
||||
bgContainer.appendChild(emojiEl);
|
||||
}
|
||||
}
|
||||
|
||||
function createFragments() {
|
||||
const existingFragments = bgContainer.querySelectorAll('.text-fragment');
|
||||
existingFragments.forEach(f => f.remove());
|
||||
|
||||
const fragments = ['我', '疯', '了', '?', '!', '…', '救命', '为什么', '好烦', '啊啊啊'];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const frag = document.createElement('div');
|
||||
frag.className = 'text-fragment';
|
||||
frag.textContent = fragments[Math.floor(Math.random() * fragments.length)];
|
||||
frag.style.top = `${Math.random() * 100}%`;
|
||||
frag.style.left = `${Math.random() * 100}%`;
|
||||
frag.style.animationDelay = `${Math.random() * 15}s`;
|
||||
frag.style.fontSize = `${Math.random() * 12 + 12}px`;
|
||||
bgContainer.appendChild(frag);
|
||||
}
|
||||
}
|
||||
|
||||
function createCracks() {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const crack = document.createElement('div');
|
||||
crack.className = 'screen-crack';
|
||||
crack.style.top = `${Math.random() * 80}%`;
|
||||
crack.style.left = `${Math.random() * 80}%`;
|
||||
crack.style.transform = `rotate(${Math.random() * 360}deg)`;
|
||||
crack.style.animationDelay = `${Math.random() * 25}s`;
|
||||
bgContainer.appendChild(crack);
|
||||
}
|
||||
}
|
||||
|
||||
function createFlickerBlocks() {
|
||||
const glitchOverlay = document.getElementById('glitch-overlay');
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const block = document.createElement('div');
|
||||
block.className = 'flicker-block';
|
||||
block.style.width = `${Math.random() * 100 + 50}px`;
|
||||
block.style.height = `${Math.random() * 100 + 50}px`;
|
||||
block.style.top = `${Math.random() * 90}%`;
|
||||
block.style.left = `${Math.random() * 90}%`;
|
||||
block.style.animationDuration = `${Math.random() * 2 + 2}s`;
|
||||
block.style.animationDelay = `${Math.random() * 3}s`;
|
||||
glitchOverlay.appendChild(block);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAnimations() {
|
||||
if (animationToggle.checked) {
|
||||
body.classList.add('body-animated');
|
||||
} else {
|
||||
body.classList.remove('body-animated');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!animationToggle.checked) return;
|
||||
const x = (window.innerWidth / 2) - e.pageX;
|
||||
const y = (window.innerHeight / 2) - e.pageY;
|
||||
bgContainer.style.transform = `translateX(${x / 50}px) translateY(${y / 50}px)`;
|
||||
});
|
||||
|
||||
newLiteratureBtn.addEventListener('click', fetchLiterature);
|
||||
animationToggle.addEventListener('change', toggleAnimations);
|
||||
|
||||
// Initial setup
|
||||
createFloatingEmojis();
|
||||
createFragments();
|
||||
createCracks();
|
||||
createFlickerBlocks();
|
||||
toggleAnimations();
|
||||
fetchLiterature();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
createFloatingEmojis();
|
||||
createFragments();
|
||||
});
|
||||
});
|
||||
1
InfoGenie-frontend/public/60sapi/娱乐消遣/随机发病文学/接口集合.json
Executable file
1
InfoGenie-frontend/public/60sapi/娱乐消遣/随机发病文学/接口集合.json
Executable file
@@ -0,0 +1 @@
|
||||
["https://60s.api.shumengya.top"]
|
||||
8
InfoGenie-frontend/public/60sapi/娱乐消遣/随机发病文学/返回接口.json
Executable file
8
InfoGenie-frontend/public/60sapi/娱乐消遣/随机发病文学/返回接口.json
Executable file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||
"data": {
|
||||
"index": 347,
|
||||
"duanzi": "我不想读书,主要是因为家里牛啊,猪啊羊啊都没人喂。"
|
||||
}
|
||||
}
|
||||
0
frontend/60sapi/娱乐消遣/随机唱歌音频/css/style.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机唱歌音频/css/style.css
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机唱歌音频/css/style.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机唱歌音频/css/style.css
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机唱歌音频/index.html → InfoGenie-frontend/public/60sapi/娱乐消遣/随机唱歌音频/index.html
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机唱歌音频/index.html → InfoGenie-frontend/public/60sapi/娱乐消遣/随机唱歌音频/index.html
Normal file → Executable file
2
frontend/60sapi/娱乐消遣/随机唱歌音频/js/script.js → InfoGenie-frontend/public/60sapi/娱乐消遣/随机唱歌音频/js/script.js
Normal file → Executable file
2
frontend/60sapi/娱乐消遣/随机唱歌音频/js/script.js → InfoGenie-frontend/public/60sapi/娱乐消遣/随机唱歌音频/js/script.js
Normal file → Executable file
@@ -17,7 +17,7 @@
|
||||
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/changya`);
|
||||
} catch (e) {
|
||||
// 如果无法加载接口集合,使用默认接口
|
||||
this.endpoints = ['https://60s.viki.moe/v2/changya'];
|
||||
this.endpoints = ['https://60s.api.shumengya.top/v2/changya'];
|
||||
}
|
||||
},
|
||||
// 获取当前接口URL
|
||||
0
frontend/60sapi/娱乐消遣/随机唱歌音频/接口集合.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机唱歌音频/接口集合.json
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机唱歌音频/接口集合.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机唱歌音频/接口集合.json
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机唱歌音频/返回接口.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机唱歌音频/返回接口.json
Normal file → Executable file
0
frontend/60sapi/娱乐消遣/随机唱歌音频/返回接口.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机唱歌音频/返回接口.json
Normal file → Executable file
36
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/css/background.css
Executable file
36
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/css/background.css
Executable file
@@ -0,0 +1,36 @@
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
transition: background 0.5s ease;
|
||||
}
|
||||
|
||||
/* Hand-drawn Comic Theme Background - NEW VIBRANT VERSION */
|
||||
body.theme-comic {
|
||||
background: linear-gradient(-45deg, #ff7e5f, #feb47b, #ffcc80, #ffecb3);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientBG 15s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradientBG {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Placeholder for Emoji Theme Background */
|
||||
body.theme-emoji {
|
||||
background-color: #fffde7;
|
||||
}
|
||||
|
||||
/* Placeholder for Retro TV Theme Background */
|
||||
body.theme-retro {
|
||||
background-color: #3d2b1f;
|
||||
}
|
||||
199
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/css/style.css
Executable file
199
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/css/style.css
Executable file
@@ -0,0 +1,199 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Zhi+Mang+Xing&display=swap');
|
||||
|
||||
/* --- General & Theme Switcher --- */
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.theme-switcher {
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
padding: 5px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
.theme-icon.active {
|
||||
border-color: #ff7043;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* --- Comic Theme Styles --- */
|
||||
.theme-comic header h1 {
|
||||
font-family: 'Zhi Mang Xing', cursive;
|
||||
font-size: 4em;
|
||||
color: #d84315; /* Deep Orange */
|
||||
text-shadow: 2px 2px 0 #fff;
|
||||
margin: 0.2em 0;
|
||||
}
|
||||
|
||||
.theme-comic .divider {
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #ffca28, #ff7043, #29b6f6, #66bb6a);
|
||||
border-radius: 3px;
|
||||
margin: 20px auto;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.theme-comic .joke-card {
|
||||
background: rgba(255, 255, 255, 0.85); /* White with transparency */
|
||||
backdrop-filter: blur(5px);
|
||||
border-radius: 15px;
|
||||
padding: 40px;
|
||||
min-height: 200px;
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
transform: rotate(-1deg);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.theme-comic .joke-card:hover {
|
||||
transform: rotate(1deg) scale(1.02);
|
||||
}
|
||||
|
||||
.theme-comic .joke-text {
|
||||
font-family: 'Zhi Mang Xing', cursive;
|
||||
font-size: 2em;
|
||||
line-height: 1.6;
|
||||
color: #5d4037;
|
||||
}
|
||||
|
||||
.theme-comic .new-joke-btn {
|
||||
background: #1e88e5; /* Vibrant Blue */
|
||||
color: white;
|
||||
font-family: 'Zhi Mang Xing', cursive;
|
||||
font-size: 2.5em;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
padding: 10px 30px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 5px 0 #1565c0; /* Darker Blue */
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
.theme-comic .new-joke-btn:active {
|
||||
transform: translateY(5px);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* --- Loading Animation --- */
|
||||
.loading-container { display: none; }
|
||||
.loading-container.visible { display: block; }
|
||||
.loading-anim {
|
||||
height: 60px;
|
||||
width: 80px;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
.book {
|
||||
transform-style: preserve-3d;
|
||||
transform: rotateY(-30deg);
|
||||
animation: flip 3s infinite;
|
||||
}
|
||||
.book, .book-page {
|
||||
width: 40px;
|
||||
height: 55px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-left: -20px;
|
||||
margin-top: -27.5px;
|
||||
}
|
||||
.book-page {
|
||||
background: #ffca28;
|
||||
border: 1px solid #ff7043;
|
||||
border-radius: 3px;
|
||||
transform-origin: left;
|
||||
}
|
||||
.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(3) { animation: flip-page 3s -2s infinite; }
|
||||
|
||||
@keyframes flip { 50% { transform: rotateY(30deg); } }
|
||||
@keyframes flip-page { 30%, 100% { transform: rotateY(180deg); } }
|
||||
|
||||
/* --- Feedback Buttons & Animations --- */
|
||||
.feedback-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.feedback-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2em;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.feedback-btn:hover { transform: scale(1.2); }
|
||||
|
||||
#animation-container {
|
||||
position: fixed;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
pointer-events: none; z-index: 999;
|
||||
}
|
||||
.confetti, .snowflake {
|
||||
position: absolute;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
.confetti {
|
||||
width: 10px; height: 10px;
|
||||
animation-name: fall;
|
||||
}
|
||||
.snowflake {
|
||||
font-size: 20px; color: #fff;
|
||||
animation-name: fall;
|
||||
}
|
||||
@keyframes fall {
|
||||
from { transform: translateY(-10vh) rotate(0deg); }
|
||||
to { transform: translateY(110vh) rotate(360deg); }
|
||||
}
|
||||
|
||||
.joke-card.absurd {
|
||||
animation: absurd-flash 0.5s 2;
|
||||
}
|
||||
@keyframes absurd-flash {
|
||||
0%, 100% { border: 2px solid transparent; }
|
||||
50% { border: 5px solid red; }
|
||||
}
|
||||
|
||||
/* --- General Joke Text Visibility --- */
|
||||
.joke-text {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
transition: opacity 0.4s ease, transform 0.4s ease;
|
||||
}
|
||||
.joke-text.visible {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* --- Responsive --- */
|
||||
@media (max-width: 600px) {
|
||||
.theme-comic header h1 { font-size: 3em; }
|
||||
.theme-comic .joke-card { padding: 25px; transform: rotate(0); }
|
||||
.theme-comic .joke-card:hover { transform: rotate(0); }
|
||||
.theme-comic .joke-text { font-size: 1.5em; }
|
||||
}
|
||||
59
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/index.html
Executable file
59
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/index.html
Executable file
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>段子游乐场</title>
|
||||
<link rel="stylesheet" href="css/background.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body class="theme-comic"> <!-- Default Theme -->
|
||||
|
||||
<div class="theme-switcher">
|
||||
<div class="theme-icon" data-theme="theme-comic" title="手绘漫画">✏️</div>
|
||||
<div class="theme-icon" data-theme="theme-emoji" title="表情包狂欢">😂</div>
|
||||
<div class="theme-icon" data-theme="theme-retro" title="复古电视">📺</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>段子游乐场</h1>
|
||||
<div class="divider"></div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div id="joke-card" class="joke-card">
|
||||
<div class="loading-container">
|
||||
<div class="loading-anim">
|
||||
<div class="book">
|
||||
<div class="book-page"></div>
|
||||
<div class="book-page"></div>
|
||||
<div class="book-page"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p>段子菌正在翻笑话库...</p>
|
||||
</div>
|
||||
<p id="joke-text" class="joke-text"></p>
|
||||
</div>
|
||||
|
||||
<div class="feedback-buttons">
|
||||
<button id="btn-lol" 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-absurd" class="feedback-btn" title="离谱但好笑">🤯</button>
|
||||
</div>
|
||||
|
||||
<button id="new-joke-btn" class="new-joke-btn">
|
||||
<span class="btn-text">再来一个!</span>
|
||||
</button>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Animation & Sound Containers -->
|
||||
<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-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>
|
||||
</body>
|
||||
</html>
|
||||
122
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/js/script.js
Executable file
122
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/js/script.js
Executable file
@@ -0,0 +1,122 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Elements
|
||||
const body = document.body;
|
||||
const jokeTextElem = document.getElementById('joke-text');
|
||||
const newJokeBtn = document.getElementById('new-joke-btn');
|
||||
const loadingContainer = document.querySelector('.loading-container');
|
||||
const animationContainer = document.getElementById('animation-container');
|
||||
const jokeCard = document.getElementById('joke-card');
|
||||
|
||||
// API
|
||||
const apiBaseUrls = ["https://60s.api.shumengya.top"];
|
||||
const apiPath = "/v2/duanzi";
|
||||
let currentApiIndex = 0;
|
||||
|
||||
// --- Core Functions ---
|
||||
const showLoading = (isLoading) => {
|
||||
loadingContainer.classList.toggle('visible', isLoading);
|
||||
if (isLoading) jokeTextElem.classList.remove('visible');
|
||||
};
|
||||
|
||||
const displayJoke = (joke) => {
|
||||
jokeTextElem.textContent = joke;
|
||||
showLoading(false);
|
||||
setTimeout(() => jokeTextElem.classList.add('visible'), 50);
|
||||
};
|
||||
|
||||
const fetchJoke = async () => {
|
||||
showLoading(true);
|
||||
try {
|
||||
const url = apiBaseUrls[currentApiIndex] + apiPath;
|
||||
const response = await fetch(url, { timeout: 5000 });
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
|
||||
const data = await response.json();
|
||||
if (data.code === 200 && data.data && data.data.duanzi) {
|
||||
displayJoke(data.data.duanzi);
|
||||
} else {
|
||||
throw new Error('Invalid data format');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`API error with ${apiBaseUrls[currentApiIndex]}:`, error);
|
||||
currentApiIndex = (currentApiIndex + 1) % apiBaseUrls.length;
|
||||
if (currentApiIndex !== 0) {
|
||||
fetchJoke(); // Try next API
|
||||
} else {
|
||||
displayJoke('段子菌迷路了!点击‘再来一个’让它重新找路~');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- Theme Switcher ---
|
||||
const themeSwitcher = document.querySelector('.theme-switcher');
|
||||
themeSwitcher.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('theme-icon')) {
|
||||
const theme = e.target.dataset.theme;
|
||||
body.className = theme; // Set body class to the selected theme
|
||||
|
||||
// Update active icon
|
||||
themeSwitcher.querySelectorAll('.theme-icon').forEach(icon => icon.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
|
||||
alert(`主题已切换!部分主题(如表情包、复古电视)将在后续阶段实现。`);
|
||||
}
|
||||
});
|
||||
// Set initial active theme icon
|
||||
themeSwitcher.querySelector(`[data-theme="${body.className}"]`).classList.add('active');
|
||||
|
||||
|
||||
// --- Feedback Buttons & Animations ---
|
||||
const btnLol = document.getElementById('btn-lol');
|
||||
const btnCold = document.getElementById('btn-cold');
|
||||
const btnSeen = document.getElementById('btn-seen');
|
||||
const btnAbsurd = document.getElementById('btn-absurd');
|
||||
const soundLol = document.getElementById('sound-lol');
|
||||
const soundCold = document.getElementById('sound-cold');
|
||||
|
||||
btnLol.addEventListener('click', () => {
|
||||
soundLol.play();
|
||||
createParticles(20, 'confetti');
|
||||
});
|
||||
|
||||
btnCold.addEventListener('click', () => {
|
||||
soundCold.play();
|
||||
createParticles(15, 'snowflake');
|
||||
});
|
||||
|
||||
btnSeen.addEventListener('click', () => {
|
||||
displayJoke("原来你也听过!那再给你换个新鲜的~");
|
||||
setTimeout(fetchJoke, 1500);
|
||||
});
|
||||
|
||||
btnAbsurd.addEventListener('click', () => {
|
||||
jokeCard.classList.add('absurd');
|
||||
setTimeout(() => jokeCard.classList.remove('absurd'), 1000);
|
||||
});
|
||||
|
||||
function createParticles(count, type) {
|
||||
animationContainer.innerHTML = ''; // Clear previous
|
||||
const colors = ['#ffca28', '#ff7043', '#29b6f6', '#66bb6a'];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const particle = document.createElement('div');
|
||||
particle.classList.add(type);
|
||||
if (type === 'confetti') {
|
||||
particle.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
} else {
|
||||
particle.textContent = '❄️';
|
||||
}
|
||||
particle.style.left = `${Math.random() * 100}vw`;
|
||||
const duration = Math.random() * 3 + 2; // 2-5 seconds
|
||||
const delay = Math.random() * -duration; // Start at different times
|
||||
particle.style.animationDuration = `${duration}s`;
|
||||
particle.style.animationDelay = `${delay}s`;
|
||||
animationContainer.appendChild(particle);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Event Listeners ---
|
||||
newJokeBtn.addEventListener('click', fetchJoke);
|
||||
|
||||
// --- Initial Load ---
|
||||
fetchJoke();
|
||||
});
|
||||
1
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/接口集合.json
Executable file
1
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/接口集合.json
Executable file
@@ -0,0 +1 @@
|
||||
["https://60s.api.shumengya.top"]
|
||||
8
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/返回接口.json
Executable file
8
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/返回接口.json
Executable file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||
"data": {
|
||||
"index": 347,
|
||||
"duanzi": "我不想读书,主要是因为家里牛啊,猪啊羊啊都没人喂。"
|
||||
}
|
||||
}
|
||||
26
InfoGenie-frontend/public/60sapi/娱乐消遣/随机运势/css/background.css
Executable file
26
InfoGenie-frontend/public/60sapi/娱乐消遣/随机运势/css/background.css
Executable file
@@ -0,0 +1,26 @@
|
||||
body {
|
||||
background: linear-gradient(-45deg, #0a021a, #2a0d3f, #4a1a6c, #7b2f8f);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientBG 20s ease infinite;
|
||||
color: #ffffff;
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@keyframes gradientBG {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
342
InfoGenie-frontend/public/60sapi/娱乐消遣/随机运势/css/style.css
Executable file
342
InfoGenie-frontend/public/60sapi/娱乐消遣/随机运势/css/style.css
Executable file
@@ -0,0 +1,342 @@
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.8em;
|
||||
color: #f0e6ff;
|
||||
text-shadow: 0 0 10px #d1a9ff, 0 0 20px #d1a9ff;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.2em;
|
||||
color: #e0c8ff;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.crystal-ball-container {
|
||||
perspective: 1000px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.crystal-ball {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.6), rgba(200, 180, 255, 0.1));
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
box-shadow: 0 0 30px #c390ff, 0 0 60px #a060e0, inset 0 0 20px rgba(255, 220, 255, 0.3);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.reflection {
|
||||
width: 80px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 40px;
|
||||
transform: rotate(-30deg);
|
||||
filter: blur(5px);
|
||||
}
|
||||
|
||||
.swirl {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 120%;
|
||||
height: 120%;
|
||||
background: linear-gradient(45deg, rgba(255, 192, 203, 0.1), rgba(128, 0, 128, 0.2));
|
||||
border-radius: 50%;
|
||||
animation: swirl 10s linear infinite;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-20px); }
|
||||
}
|
||||
|
||||
@keyframes swirl {
|
||||
from { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
.fortune-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 15px;
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.fortune-content {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.fortune-content.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#luck-desc {
|
||||
font-size: 2em;
|
||||
color: #ffc0cb;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
#luck-tip {
|
||||
font-size: 1.1em;
|
||||
color: #e0e0e0;
|
||||
margin: 0;
|
||||
padding-bottom: 20px; /* Add some space before the new details */
|
||||
}
|
||||
|
||||
.fortune-details {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail-item h3 {
|
||||
font-size: 0.9em;
|
||||
color: #ffc0cb;
|
||||
margin: 0 0 5px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.detail-item p {
|
||||
font-size: 1.2em;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#lucky-color {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
|
||||
/* Remove the text content */
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
/* Tarot Card Styles */
|
||||
.tarot-container {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.tarot-container h2 {
|
||||
font-size: 1.5em;
|
||||
color: #f0e6ff;
|
||||
text-shadow: 0 0 8px #d1a9ff;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tarot-card-container {
|
||||
width: 180px;
|
||||
height: 280px;
|
||||
perspective: 1000px;
|
||||
margin: 0 auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tarot-card-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.8s;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.tarot-card-container.flipped .tarot-card-inner {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.tarot-card-front,
|
||||
.tarot-card-back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.tarot-card-back {
|
||||
background: linear-gradient(135deg, #4a1a6c, #2a0d3f);
|
||||
border: 2px solid #d1a9ff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 3em;
|
||||
color: #d1a9ff;
|
||||
}
|
||||
|
||||
.tarot-card-back::after {
|
||||
content: '✧'; /* A simple star symbol */
|
||||
text-shadow: 0 0 10px #f0e6ff;
|
||||
}
|
||||
|
||||
.tarot-card-front {
|
||||
background: linear-gradient(135deg, #3e165b, #592883);
|
||||
border: 2px solid #d1a9ff;
|
||||
color: white;
|
||||
transform: rotateY(180deg);
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#tarot-name {
|
||||
font-size: 1.4em;
|
||||
color: #ffc0cb;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
#tarot-interpretation {
|
||||
font-size: 0.9em;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Side Decorations */
|
||||
.side-decor {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 15vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.left-decor {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.right-decor {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.decor-symbol {
|
||||
position: absolute;
|
||||
color: rgba(209, 169, 255, 0.5);
|
||||
text-shadow: 0 0 10px rgba(240, 230, 255, 0.7);
|
||||
animation: floatSymbol 20s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes floatSymbol {
|
||||
0%, 100% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
25%, 75% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20vh) rotate(180deg);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
#get-fortune-btn {
|
||||
background: linear-gradient(45deg, #da70d6, #8a2be2);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
padding: 15px 30px;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 0 15px #c390ff;
|
||||
}
|
||||
|
||||
#get-fortune-btn:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 25px #d1a9ff;
|
||||
}
|
||||
|
||||
#get-fortune-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border: 4px solid rgba(255, 255, 255, 0.2);
|
||||
border-left-color: #ffc0cb;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
display: none; /* Hidden by default */
|
||||
}
|
||||
|
||||
.loading-spinner.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 40px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
header h1 {
|
||||
font-size: 2.2em;
|
||||
}
|
||||
.crystal-ball {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.fortune-card {
|
||||
padding: 20px;
|
||||
}
|
||||
.fortune-details {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.tarot-card-container {
|
||||
width: 150px;
|
||||
height: 233px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide side decor on smaller screens */
|
||||
@media (max-width: 1200px) {
|
||||
.side-decor {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
71
InfoGenie-frontend/public/60sapi/娱乐消遣/随机运势/index.html
Executable file
71
InfoGenie-frontend/public/60sapi/娱乐消遣/随机运势/index.html
Executable file
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>水晶球占卜</title>
|
||||
<link rel="stylesheet" href="css/background.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="side-decor left-decor"></div>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>水晶球占卜</h1>
|
||||
<p>洞察你今日的运势</p>
|
||||
</header>
|
||||
<main>
|
||||
<div class="crystal-ball-container">
|
||||
<div class="crystal-ball">
|
||||
<div class="reflection"></div>
|
||||
<div class="swirl"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fortune-card" class="fortune-card">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="fortune-content">
|
||||
<h2 id="luck-desc"></h2>
|
||||
<p id="luck-tip"></p>
|
||||
<div class="fortune-details">
|
||||
<div class="detail-item">
|
||||
<h3>今日咒语</h3>
|
||||
<p id="fortune-summary"></p>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<h3>幸运色</h3>
|
||||
<p id="lucky-color"></p>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<h3>幸运数字</h3>
|
||||
<p id="lucky-number"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Tarot Card Section -->
|
||||
<div class="tarot-container">
|
||||
<h2>每日塔罗指引</h2>
|
||||
<div id="tarot-card" class="tarot-card-container">
|
||||
<div class="tarot-card-inner">
|
||||
<div class="tarot-card-back">
|
||||
<!-- Back of the card design -->
|
||||
</div>
|
||||
<div class="tarot-card-front">
|
||||
<h3 id="tarot-name"></h3>
|
||||
<p id="tarot-interpretation"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="get-fortune-btn">再次占卜</button>
|
||||
</main>
|
||||
<footer>
|
||||
<p>仅供娱乐,祝您好运</p>
|
||||
</footer>
|
||||
</div>
|
||||
<div class="side-decor right-decor"></div>
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
170
InfoGenie-frontend/public/60sapi/娱乐消遣/随机运势/js/script.js
Executable file
170
InfoGenie-frontend/public/60sapi/娱乐消遣/随机运势/js/script.js
Executable file
@@ -0,0 +1,170 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const getFortuneBtn = document.getElementById('get-fortune-btn');
|
||||
const fortuneCard = document.getElementById('fortune-card');
|
||||
const fortuneContent = fortuneCard.querySelector('.fortune-content');
|
||||
const luckDescElem = document.getElementById('luck-desc');
|
||||
const luckTipElem = document.getElementById('luck-tip');
|
||||
const fortuneSummaryElem = document.getElementById('fortune-summary');
|
||||
const luckyColorElem = document.getElementById('lucky-color');
|
||||
const luckyNumberElem = document.getElementById('lucky-number');
|
||||
const loadingSpinner = fortuneCard.querySelector('.loading-spinner');
|
||||
const tarotCardContainer = document.getElementById('tarot-card');
|
||||
const tarotNameElem = document.getElementById('tarot-name');
|
||||
const tarotInterpretationElem = document.getElementById('tarot-interpretation');
|
||||
|
||||
const apiBaseUrls = [
|
||||
"https://60s.api.shumengya.top",
|
||||
];
|
||||
const apiPath = "/v2/luck";
|
||||
|
||||
const mantras = [
|
||||
"顺其自然,皆是美好。",
|
||||
"相信直觉,它知道方向。",
|
||||
"每一次呼吸都是新的开始。",
|
||||
"心怀感恩,好运自来。",
|
||||
"拥抱变化,发现惊喜。",
|
||||
"你的能量,超乎想象。",
|
||||
"保持微笑,宇宙会回应你。"
|
||||
];
|
||||
|
||||
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: "完成,整合,成就。一个旅程的成功结束,庆祝你的成就。" }
|
||||
];
|
||||
|
||||
let currentApiIndex = 0;
|
||||
|
||||
const showLoading = (isLoading) => {
|
||||
if (isLoading) {
|
||||
fortuneContent.classList.remove('visible');
|
||||
loadingSpinner.classList.add('visible');
|
||||
} else {
|
||||
loadingSpinner.classList.remove('visible');
|
||||
setTimeout(() => {
|
||||
fortuneContent.classList.add('visible');
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFortune = async () => {
|
||||
showLoading(true);
|
||||
tarotCardContainer.classList.remove('flipped'); // Reset card on new fetch
|
||||
|
||||
try {
|
||||
const url = apiBaseUrls[currentApiIndex] + apiPath;
|
||||
const response = await fetch(url, { timeout: 5000 });
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (data.code === 200 && data.data) {
|
||||
updateFortune(data.data);
|
||||
drawTarotCard(); // Draw a tarot card on success
|
||||
} else {
|
||||
throw new Error('Invalid data format');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`API error with ${apiBaseUrls[currentApiIndex]}:`, error);
|
||||
currentApiIndex = (currentApiIndex + 1) % apiBaseUrls.length;
|
||||
if (currentApiIndex !== 0) {
|
||||
fetchFortune(); // Try next API
|
||||
} else {
|
||||
displayError();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateFortune = (data) => {
|
||||
luckDescElem.textContent = data.luck_desc || '运势';
|
||||
luckTipElem.textContent = data.luck_tip || '今日运势平平,保持好心情。';
|
||||
|
||||
// Generate and display additional details
|
||||
fortuneSummaryElem.textContent = mantras[Math.floor(Math.random() * mantras.length)];
|
||||
luckyColorElem.style.backgroundColor = `#${Math.floor(Math.random()*16777215).toString(16).padStart(6, '0')}`;
|
||||
luckyNumberElem.textContent = Math.floor(Math.random() * 100);
|
||||
|
||||
showLoading(false);
|
||||
};
|
||||
|
||||
const displayError = () => {
|
||||
luckDescElem.textContent = '占卜失败';
|
||||
luckTipElem.textContent = '无法连接到星辰之力,请稍后再试。';
|
||||
fortuneSummaryElem.textContent = '---';
|
||||
luckyColorElem.style.backgroundColor = 'transparent';
|
||||
luckyNumberElem.textContent = '-';
|
||||
showLoading(false);
|
||||
tarotNameElem.textContent = '指引中断';
|
||||
tarotInterpretationElem.textContent = '星辰之力暂时无法连接。';
|
||||
tarotCardContainer.classList.add('flipped'); // Show error on card
|
||||
};
|
||||
|
||||
const drawTarotCard = () => {
|
||||
const card = tarotDeck[Math.floor(Math.random() * tarotDeck.length)];
|
||||
tarotNameElem.textContent = card.name;
|
||||
tarotInterpretationElem.textContent = card.interpretation;
|
||||
|
||||
// Flip the card after a short delay to allow the main content to appear
|
||||
setTimeout(() => {
|
||||
tarotCardContainer.classList.add('flipped');
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const createSideDecorations = () => {
|
||||
const leftContainer = document.querySelector('.left-decor');
|
||||
const rightContainer = document.querySelector('.right-decor');
|
||||
if (!leftContainer || !rightContainer) return;
|
||||
|
||||
const symbols = ['✧', '✦', '☾', '✶', '✵', '✩', '✨'];
|
||||
const symbolCount = 15; // Number of symbols per side
|
||||
|
||||
const createSymbols = (container) => {
|
||||
for (let i = 0; i < symbolCount; i++) {
|
||||
const symbol = document.createElement('span');
|
||||
symbol.classList.add('decor-symbol');
|
||||
symbol.textContent = symbols[Math.floor(Math.random() * symbols.length)];
|
||||
|
||||
// Randomize properties for a more natural look
|
||||
symbol.style.top = `${Math.random() * 90}vh`;
|
||||
symbol.style.left = `${Math.random() * 80}%`;
|
||||
symbol.style.fontSize = `${Math.random() * 20 + 10}px`;
|
||||
symbol.style.animationDelay = `${Math.random() * 20}s`;
|
||||
symbol.style.animationDuration = `${Math.random() * 20 + 15}s`; // Duration between 15s and 35s
|
||||
|
||||
container.appendChild(symbol);
|
||||
}
|
||||
};
|
||||
|
||||
createSymbols(leftContainer);
|
||||
createSymbols(rightContainer);
|
||||
};
|
||||
|
||||
getFortuneBtn.addEventListener('click', fetchFortune);
|
||||
tarotCardContainer.addEventListener('click', () => {
|
||||
tarotCardContainer.classList.toggle('flipped');
|
||||
});
|
||||
|
||||
// Initial actions on page load
|
||||
fetchFortune();
|
||||
createSideDecorations();
|
||||
});
|
||||
0
frontend/60sapi/实用功能/EpicGames免费游戏/接口集合.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机运势/接口集合.json
Normal file → Executable file
0
frontend/60sapi/实用功能/EpicGames免费游戏/接口集合.json → InfoGenie-frontend/public/60sapi/娱乐消遣/随机运势/接口集合.json
Normal file → Executable file
10
InfoGenie-frontend/public/60sapi/娱乐消遣/随机运势/返回接口.json
Executable file
10
InfoGenie-frontend/public/60sapi/娱乐消遣/随机运势/返回接口.json
Executable file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||
"data": {
|
||||
"luck_desc": "恋愛運",
|
||||
"luck_rank": 21,
|
||||
"luck_tip": "闪亮的邂逅之日!顺其自然吧",
|
||||
"luck_tip_index": 19
|
||||
}
|
||||
}
|
||||
0
frontend/60sapi/实用功能/EpicGames免费游戏/css/style.css → InfoGenie-frontend/public/60sapi/实用功能/EpicGames免费游戏/css/style.css
Normal file → Executable file
0
frontend/60sapi/实用功能/EpicGames免费游戏/css/style.css → InfoGenie-frontend/public/60sapi/实用功能/EpicGames免费游戏/css/style.css
Normal file → Executable file
0
frontend/60sapi/实用功能/EpicGames免费游戏/index.html → InfoGenie-frontend/public/60sapi/实用功能/EpicGames免费游戏/index.html
Normal file → Executable file
0
frontend/60sapi/实用功能/EpicGames免费游戏/index.html → InfoGenie-frontend/public/60sapi/实用功能/EpicGames免费游戏/index.html
Normal file → Executable file
2
frontend/60sapi/实用功能/EpicGames免费游戏/js/script.js → InfoGenie-frontend/public/60sapi/实用功能/EpicGames免费游戏/js/script.js
Normal file → Executable file
2
frontend/60sapi/实用功能/EpicGames免费游戏/js/script.js → InfoGenie-frontend/public/60sapi/实用功能/EpicGames免费游戏/js/script.js
Normal file → Executable file
@@ -13,7 +13,7 @@
|
||||
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/epic`);
|
||||
} catch (e) {
|
||||
// 如果无法加载接口集合,使用默认接口
|
||||
this.endpoints = ['https://60s-api.viki.moe/v2/epic'];
|
||||
this.endpoints = ['https://60s.api.shumengya.top/v2/epic'];
|
||||
}
|
||||
},
|
||||
// 获取当前接口URL
|
||||
0
frontend/60sapi/实用功能/农历信息/接口集合.json → InfoGenie-frontend/public/60sapi/实用功能/EpicGames免费游戏/接口集合.json
Normal file → Executable file
0
frontend/60sapi/实用功能/农历信息/接口集合.json → InfoGenie-frontend/public/60sapi/实用功能/EpicGames免费游戏/接口集合.json
Normal file → Executable file
0
frontend/60sapi/实用功能/EpicGames免费游戏/返回接口.json → InfoGenie-frontend/public/60sapi/实用功能/EpicGames免费游戏/返回接口.json
Normal file → Executable file
0
frontend/60sapi/实用功能/EpicGames免费游戏/返回接口.json → InfoGenie-frontend/public/60sapi/实用功能/EpicGames免费游戏/返回接口.json
Normal file → Executable file
233
InfoGenie-frontend/public/60sapi/实用功能/公网IP地址/css/background.css
Executable file
233
InfoGenie-frontend/public/60sapi/实用功能/公网IP地址/css/background.css
Executable file
@@ -0,0 +1,233 @@
|
||||
/* 动态背景样式 */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
z-index: -1;
|
||||
animation: backgroundMove 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes backgroundMove {
|
||||
0%, 100% {
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
}
|
||||
25% {
|
||||
background:
|
||||
radial-gradient(circle at 60% 30%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 30% 70%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 80%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
}
|
||||
50% {
|
||||
background:
|
||||
radial-gradient(circle at 80% 60%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 60% 70%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
}
|
||||
75% {
|
||||
background:
|
||||
radial-gradient(circle at 40% 90%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 70% 10%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 20% 60%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 浮动粒子效果 */
|
||||
.particles {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 50%;
|
||||
animation: float 15s infinite linear;
|
||||
}
|
||||
|
||||
.particle:nth-child(1) {
|
||||
left: 10%;
|
||||
animation-delay: 0s;
|
||||
animation-duration: 12s;
|
||||
}
|
||||
|
||||
.particle:nth-child(2) {
|
||||
left: 20%;
|
||||
animation-delay: 2s;
|
||||
animation-duration: 18s;
|
||||
}
|
||||
|
||||
.particle:nth-child(3) {
|
||||
left: 30%;
|
||||
animation-delay: 4s;
|
||||
animation-duration: 15s;
|
||||
}
|
||||
|
||||
.particle:nth-child(4) {
|
||||
left: 40%;
|
||||
animation-delay: 6s;
|
||||
animation-duration: 20s;
|
||||
}
|
||||
|
||||
.particle:nth-child(5) {
|
||||
left: 50%;
|
||||
animation-delay: 8s;
|
||||
animation-duration: 14s;
|
||||
}
|
||||
|
||||
.particle:nth-child(6) {
|
||||
left: 60%;
|
||||
animation-delay: 10s;
|
||||
animation-duration: 16s;
|
||||
}
|
||||
|
||||
.particle:nth-child(7) {
|
||||
left: 70%;
|
||||
animation-delay: 12s;
|
||||
animation-duration: 22s;
|
||||
}
|
||||
|
||||
.particle:nth-child(8) {
|
||||
left: 80%;
|
||||
animation-delay: 14s;
|
||||
animation-duration: 13s;
|
||||
}
|
||||
|
||||
.particle:nth-child(9) {
|
||||
left: 90%;
|
||||
animation-delay: 16s;
|
||||
animation-duration: 19s;
|
||||
}
|
||||
|
||||
.particle:nth-child(10) {
|
||||
left: 15%;
|
||||
animation-delay: 18s;
|
||||
animation-duration: 17s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% {
|
||||
transform: translateY(100vh) rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-100px) rotate(360deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 网格背景效果 */
|
||||
.grid-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
z-index: -1;
|
||||
opacity: 0.3;
|
||||
animation: gridMove 30s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes gridMove {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(50px, 50px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 光晕效果 */
|
||||
.glow-effect {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, rgba(74, 144, 226, 0.2) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: -1;
|
||||
animation: pulse 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式背景调整 */
|
||||
@media (max-width: 768px) {
|
||||
.grid-background {
|
||||
background-size: 30px 30px;
|
||||
}
|
||||
|
||||
.glow-effect {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.particle {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.grid-background {
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.glow-effect {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.particle {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
445
InfoGenie-frontend/public/60sapi/实用功能/公网IP地址/css/style.css
Executable file
445
InfoGenie-frontend/public/60sapi/实用功能/公网IP地址/css/style.css
Executable file
@@ -0,0 +1,445 @@
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 容器样式 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.header {
|
||||
text-align: center;
|
||||
padding: 3rem 2rem 2rem;
|
||||
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1), rgba(80, 200, 120, 0.1));
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #4a90e2, #50c878);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 i {
|
||||
margin-right: 0.5rem;
|
||||
background: linear-gradient(135deg, #4a90e2, #50c878);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 查询按钮区域 */
|
||||
.query-section {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.query-btn {
|
||||
background: linear-gradient(135deg, #4a90e2, #50c878);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 200px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.query-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(74, 144, 226, 0.4);
|
||||
background: linear-gradient(135deg, #3a7bc8, #40a868);
|
||||
}
|
||||
|
||||
.query-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.query-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #4a90e2;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading p {
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* IP信息卡片 */
|
||||
.ip-info {
|
||||
animation: fadeInUp 0.6s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.ip-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.ip-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ip-header i {
|
||||
font-size: 1.5rem;
|
||||
color: #4a90e2;
|
||||
}
|
||||
|
||||
.ip-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.ip-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1), rgba(80, 200, 120, 0.1));
|
||||
border-radius: 15px;
|
||||
border: 2px solid rgba(74, 144, 226, 0.2);
|
||||
}
|
||||
|
||||
.ip-address {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #2c3e50;
|
||||
background: linear-gradient(135deg, #4a90e2, #50c878);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: #4a90e2;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 1rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: #3a7bc8;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.ip-details {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(248, 249, 250, 0.8);
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.detail-item:hover {
|
||||
background: rgba(74, 144, 226, 0.1);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.detail-item i {
|
||||
color: #4a90e2;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail-item .label {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.detail-item .value {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* IP地址说明区域 */
|
||||
.ip-explanation {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.ip-explanation h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.3rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.ip-explanation h3 i {
|
||||
color: #4a90e2;
|
||||
}
|
||||
|
||||
.ip-explanation p {
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: rgba(248, 249, 250, 0.8);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-item:hover {
|
||||
background: rgba(74, 144, 226, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.feature-item i {
|
||||
color: #4a90e2;
|
||||
font-size: 1.5rem;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.feature-item h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.feature-item p {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 错误信息 */
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 99, 99, 0.3);
|
||||
animation: fadeInUp 0.6s ease;
|
||||
}
|
||||
|
||||
.error-message i {
|
||||
font-size: 3rem;
|
||||
color: #ff6b6b;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-message p {
|
||||
color: #666;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background: #ff5252;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
/* 页脚 */
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: rgba(248, 249, 250, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 2rem 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.ip-card, .ip-explanation {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.ip-address {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.ip-display {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.detail-item .label {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.header h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.query-btn {
|
||||
padding: 0.875rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.ip-address {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.ip-card, .ip-explanation {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
139
InfoGenie-frontend/public/60sapi/实用功能/公网IP地址/index.html
Executable file
139
InfoGenie-frontend/public/60sapi/实用功能/公网IP地址/index.html
Executable file
@@ -0,0 +1,139 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>公网IP地址查询</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/background.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 头部 -->
|
||||
<header class="header">
|
||||
<h1><i class="fas fa-globe"></i> 公网IP地址查询</h1>
|
||||
<p class="subtitle">快速获取您的公网IP地址信息</p>
|
||||
</header>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<main class="main-content">
|
||||
<!-- 查询按钮区域 -->
|
||||
<div class="query-section">
|
||||
<button id="queryBtn" class="query-btn">
|
||||
<i class="fas fa-search"></i>
|
||||
<span>查询我的IP地址</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<div id="loading" class="loading" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
<p>正在获取IP地址信息...</p>
|
||||
</div>
|
||||
|
||||
<!-- IP信息展示区域 -->
|
||||
<div id="ip-info" class="ip-info" style="display: none;">
|
||||
<div class="ip-card">
|
||||
<div class="ip-header">
|
||||
<i class="fas fa-network-wired"></i>
|
||||
<h2>您的公网IP地址</h2>
|
||||
</div>
|
||||
<div class="ip-display">
|
||||
<span id="ip-address" class="ip-address">---.---.---.---</span>
|
||||
<button id="copyBtn" class="copy-btn" title="复制IP地址">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ip-details">
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span class="label">查询时间:</span>
|
||||
<span id="query-time" class="value">--</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-server"></i>
|
||||
<span class="label">数据来源:</span>
|
||||
<span class="value">60s.viki.moe</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<span class="label">位置信息:</span>
|
||||
<span id="location" class="value">--</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-building"></i>
|
||||
<span class="label">网络服务商:</span>
|
||||
<span id="isp" class="value">--</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-flag"></i>
|
||||
<span class="label">国家:</span>
|
||||
<span id="country" class="value">--</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-map"></i>
|
||||
<span class="label">地区:</span>
|
||||
<span id="region" class="value">--</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-city"></i>
|
||||
<span class="label">城市:</span>
|
||||
<span id="city" class="value">--</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span class="label">时区:</span>
|
||||
<span id="timezone" class="value">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP地址信息说明 -->
|
||||
<div class="ip-explanation">
|
||||
<h3><i class="fas fa-info-circle"></i> 什么是公网IP地址?</h3>
|
||||
<p>公网IP地址是您的设备在互联网上的唯一标识符,由您的网络服务提供商(ISP)分配。通过这个地址,互联网上的其他设备可以找到并与您的设备通信。</p>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
<div>
|
||||
<h4>隐私保护</h4>
|
||||
<p>了解您的IP地址有助于保护网络隐私</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<div>
|
||||
<h4>地理位置</h4>
|
||||
<p>IP地址可以大致确定您的地理位置</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-cogs"></i>
|
||||
<div>
|
||||
<h4>网络配置</h4>
|
||||
<p>用于网络故障排除和配置</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div id="error-message" class="error-message" style="display: none;">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<p>获取IP地址失败,请检查网络连接或稍后重试</p>
|
||||
<button id="retryBtn" class="retry-btn">重试</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer">
|
||||
<p>© 2024 公网IP地址查询工具 | 数据来源: 60s.viki.moe</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
343
InfoGenie-frontend/public/60sapi/实用功能/公网IP地址/js/script.js
Executable file
343
InfoGenie-frontend/public/60sapi/实用功能/公网IP地址/js/script.js
Executable file
@@ -0,0 +1,343 @@
|
||||
// 公网IP地址查询应用
|
||||
class IPQueryApp {
|
||||
constructor() {
|
||||
this.apiEndpoint = 'https://60s.api.shumengya.top/v2/ip';
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.createParticles();
|
||||
this.createBackgroundElements();
|
||||
console.log('IP查询应用初始化完成');
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvents() {
|
||||
const queryBtn = document.getElementById('queryBtn');
|
||||
const retryBtn = document.getElementById('retryBtn');
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
|
||||
if (queryBtn) {
|
||||
queryBtn.addEventListener('click', () => this.queryIP());
|
||||
}
|
||||
|
||||
if (retryBtn) {
|
||||
retryBtn.addEventListener('click', () => this.queryIP());
|
||||
}
|
||||
|
||||
if (copyBtn) {
|
||||
copyBtn.addEventListener('click', () => this.copyIP());
|
||||
}
|
||||
|
||||
// 页面加载完成后自动查询一次
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(() => this.queryIP(), 500);
|
||||
});
|
||||
}
|
||||
|
||||
// 创建浮动粒子
|
||||
createParticles() {
|
||||
const particlesContainer = document.createElement('div');
|
||||
particlesContainer.className = 'particles';
|
||||
document.body.appendChild(particlesContainer);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const particle = document.createElement('div');
|
||||
particle.className = 'particle';
|
||||
particlesContainer.appendChild(particle);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建背景元素
|
||||
createBackgroundElements() {
|
||||
// 创建网格背景
|
||||
const gridBackground = document.createElement('div');
|
||||
gridBackground.className = 'grid-background';
|
||||
document.body.appendChild(gridBackground);
|
||||
|
||||
// 创建光晕效果
|
||||
const glowEffect = document.createElement('div');
|
||||
glowEffect.className = 'glow-effect';
|
||||
document.body.appendChild(glowEffect);
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
showLoading() {
|
||||
const loading = document.getElementById('loading');
|
||||
const ipInfo = document.getElementById('ipInfo');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
const queryBtn = document.getElementById('queryBtn');
|
||||
|
||||
if (loading) loading.style.display = 'block';
|
||||
if (ipInfo) ipInfo.style.display = 'none';
|
||||
if (errorMessage) errorMessage.style.display = 'none';
|
||||
if (queryBtn) {
|
||||
queryBtn.disabled = true;
|
||||
queryBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 查询中...';
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏加载状态
|
||||
hideLoading() {
|
||||
const loading = document.getElementById('loading');
|
||||
const queryBtn = document.getElementById('queryBtn');
|
||||
|
||||
if (loading) loading.style.display = 'none';
|
||||
if (queryBtn) {
|
||||
queryBtn.disabled = false;
|
||||
queryBtn.innerHTML = '<i class="fas fa-search"></i> 查询我的IP';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示错误信息
|
||||
showError(message) {
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
const ipInfo = document.getElementById('ip-info');
|
||||
|
||||
if (errorMessage) {
|
||||
errorMessage.style.display = 'block';
|
||||
const errorText = errorMessage.querySelector('p');
|
||||
if (errorText) errorText.textContent = message || '获取IP信息失败,请稍后重试';
|
||||
}
|
||||
if (ipInfo) ipInfo.style.display = 'none';
|
||||
|
||||
this.hideLoading();
|
||||
}
|
||||
|
||||
// 查询IP地址
|
||||
async queryIP() {
|
||||
try {
|
||||
this.showLoading();
|
||||
console.log('开始查询IP地址...');
|
||||
|
||||
const response = await fetch(this.apiEndpoint, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('API响应状态:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP错误: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('API返回数据:', data);
|
||||
|
||||
if (data.code === 200 && data.data) {
|
||||
this.displayIPInfo(data.data);
|
||||
} else {
|
||||
throw new Error(data.message || '获取IP信息失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('查询IP失败:', error);
|
||||
this.showError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示IP信息
|
||||
displayIPInfo(data) {
|
||||
const ipInfo = document.getElementById('ip-info');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
// 更新IP地址显示
|
||||
const ipAddressElement = document.getElementById('ip-address');
|
||||
if (ipAddressElement && data.ip) {
|
||||
ipAddressElement.textContent = data.ip;
|
||||
}
|
||||
|
||||
// 更新查询时间
|
||||
const queryTimeElement = document.getElementById('query-time');
|
||||
if (queryTimeElement) {
|
||||
const now = new Date();
|
||||
queryTimeElement.textContent = now.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
// 更新详细信息 - 只显示API提供的数据
|
||||
if (data.location) this.updateDetailItem('location', data.location);
|
||||
else this.hideDetailItem('location');
|
||||
|
||||
if (data.isp) this.updateDetailItem('isp', data.isp);
|
||||
else this.hideDetailItem('isp');
|
||||
|
||||
if (data.country) this.updateDetailItem('country', data.country);
|
||||
else this.hideDetailItem('country');
|
||||
|
||||
if (data.region) this.updateDetailItem('region', data.region);
|
||||
else this.hideDetailItem('region');
|
||||
|
||||
if (data.city) this.updateDetailItem('city', data.city);
|
||||
else this.hideDetailItem('city');
|
||||
|
||||
if (data.timezone) this.updateDetailItem('timezone', data.timezone);
|
||||
else this.hideDetailItem('timezone');
|
||||
|
||||
// 显示IP信息,隐藏错误信息
|
||||
if (ipInfo) ipInfo.style.display = 'block';
|
||||
if (errorMessage) errorMessage.style.display = 'none';
|
||||
|
||||
this.hideLoading();
|
||||
console.log('IP信息显示完成');
|
||||
}
|
||||
|
||||
// 更新详细信息项
|
||||
updateDetailItem(id, value) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
// 显示对应的详细信息行
|
||||
const detailRow = element.closest('.detail-item');
|
||||
if (detailRow) {
|
||||
detailRow.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏详细信息项
|
||||
hideDetailItem(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
// 隐藏整个详细信息行
|
||||
const detailRow = element.closest('.detail-item');
|
||||
if (detailRow) {
|
||||
detailRow.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 复制IP地址
|
||||
async copyIP() {
|
||||
const ipAddressElement = document.getElementById('ip-address');
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
|
||||
if (!ipAddressElement || !ipAddressElement.textContent) {
|
||||
this.showToast('没有可复制的IP地址', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(ipAddressElement.textContent);
|
||||
|
||||
// 更新按钮状态
|
||||
if (copyBtn) {
|
||||
const originalHTML = copyBtn.innerHTML;
|
||||
copyBtn.innerHTML = '<i class="fas fa-check"></i>';
|
||||
copyBtn.style.background = '#50c878';
|
||||
|
||||
setTimeout(() => {
|
||||
copyBtn.innerHTML = originalHTML;
|
||||
copyBtn.style.background = '#4a90e2';
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
this.showToast('IP地址已复制到剪贴板', 'success');
|
||||
console.log('IP地址已复制:', ipAddressElement.textContent);
|
||||
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error);
|
||||
this.showToast('复制失败,请手动选择复制', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示提示消息
|
||||
showToast(message, type = 'info') {
|
||||
// 移除已存在的toast
|
||||
const existingToast = document.querySelector('.toast');
|
||||
if (existingToast) {
|
||||
existingToast.remove();
|
||||
}
|
||||
|
||||
// 创建新的toast
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.innerHTML = `
|
||||
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
|
||||
// 添加toast样式
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${type === 'success' ? '#50c878' : type === 'error' ? '#ff6b6b' : '#4a90e2'};
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
animation: slideInRight 0.3s ease;
|
||||
max-width: 300px;
|
||||
`;
|
||||
|
||||
// 添加动画样式
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes slideOutRight {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 3秒后自动移除
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOutRight 0.3s ease';
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.remove();
|
||||
}
|
||||
if (style.parentNode) {
|
||||
style.remove();
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
formatTime(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
const app = new IPQueryApp();
|
||||
|
||||
// 导出到全局作用域(用于调试)
|
||||
window.IPQueryApp = app;
|
||||
3
InfoGenie-frontend/public/60sapi/实用功能/公网IP地址/接口集合.json
Executable file
3
InfoGenie-frontend/public/60sapi/实用功能/公网IP地址/接口集合.json
Executable file
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"https://60s.api.shumengya.top/v2/ip"
|
||||
]
|
||||
17
InfoGenie-frontend/public/60sapi/实用功能/公网IP地址/返回接口.json
Executable file
17
InfoGenie-frontend/public/60sapi/实用功能/公网IP地址/返回接口.json
Executable file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||
"data": {
|
||||
"ip": "2401:b60:16:83::"
|
||||
}
|
||||
}
|
||||
|
||||
// 注意:此API只返回IP地址,不包含以下信息:
|
||||
// - location (位置信息)
|
||||
// - isp (网络服务商)
|
||||
// - country (国家)
|
||||
// - region (地区)
|
||||
// - city (城市)
|
||||
// - timezone (时区)
|
||||
//
|
||||
// 如需这些信息,需要使用其他API服务
|
||||
575
InfoGenie-frontend/public/60sapi/实用功能/农历信息/css/background.css
Executable file
575
InfoGenie-frontend/public/60sapi/实用功能/农历信息/css/background.css
Executable file
@@ -0,0 +1,575 @@
|
||||
/* 农历主题背景样式 - 柔和版本 */
|
||||
body {
|
||||
background: linear-gradient(135deg,
|
||||
#f8f9fa 0%, /* 浅灰白 */
|
||||
#fff3e0 20%, /* 淡橙色 */
|
||||
#fef7e0 40%, /* 极淡黄 */
|
||||
#f3e5ab 60%, /* 柔和金色 */
|
||||
#e8dcc6 80%, /* 米色 */
|
||||
#f8f9fa 100% /* 浅灰白 */
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gentleShift 30s ease infinite;
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes gentleShift {
|
||||
0% { background-position: 0% 50%; }
|
||||
25% { background-position: 100% 50%; }
|
||||
50% { background-position: 100% 100%; }
|
||||
75% { background-position: 0% 100%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* 动态颜色调节系统 - 柔和版本 */
|
||||
.adaptive-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 70%, rgba(255, 255, 255, 0.25) 0%, transparent 50%),
|
||||
linear-gradient(45deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.3) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
animation: adaptiveShift 60s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes adaptiveShift {
|
||||
0% {
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 70%, rgba(255, 255, 255, 0.15) 0%, transparent 50%),
|
||||
linear-gradient(45deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.2) 100%);
|
||||
}
|
||||
25% {
|
||||
background:
|
||||
radial-gradient(circle at 70% 20%, rgba(255, 255, 255, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 30% 80%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.25) 100%);
|
||||
}
|
||||
50% {
|
||||
background:
|
||||
radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.15) 0%, transparent 50%),
|
||||
radial-gradient(circle at 10% 90%, rgba(255, 255, 255, 0.12) 0%, transparent 50%),
|
||||
linear-gradient(225deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.22) 100%);
|
||||
}
|
||||
75% {
|
||||
background:
|
||||
radial-gradient(circle at 90% 60%, rgba(255, 255, 255, 0.18) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 10%, rgba(255, 255, 255, 0.08) 0%, transparent 50%),
|
||||
linear-gradient(315deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.2) 100%);
|
||||
}
|
||||
100% {
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 70%, rgba(255, 255, 255, 0.15) 0%, transparent 50%),
|
||||
linear-gradient(45deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.2) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 高清稻穗贴图层 */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
/* 主稻穗束 - 高清细节 */
|
||||
radial-gradient(ellipse 1.5px 12px at 50% 45%, #DAA520 0%, #B8860B 30%, transparent 80%),
|
||||
radial-gradient(ellipse 1px 10px at 48% 50%, #FFD700 0%, #DAA520 40%, transparent 75%),
|
||||
radial-gradient(ellipse 1.2px 11px at 52% 48%, #FFCC00 0%, #B8860B 35%, transparent 78%),
|
||||
radial-gradient(ellipse 0.8px 9px at 49% 52%, #F4A460 0%, #DAA520 45%, transparent 70%),
|
||||
radial-gradient(ellipse 1.3px 13px at 51% 46%, #DEB887 0%, #B8860B 38%, transparent 82%),
|
||||
|
||||
/* 次级稻穗 */
|
||||
radial-gradient(ellipse 1px 8px at 30% 35%, #FFD700 0%, #DAA520 50%, transparent 75%),
|
||||
radial-gradient(ellipse 0.9px 7px at 32% 38%, #FFCC00 0%, #B8860B 45%, transparent 70%),
|
||||
radial-gradient(ellipse 1.1px 9px at 28% 36%, #DEB887 0%, #DAA520 40%, transparent 78%),
|
||||
|
||||
radial-gradient(ellipse 1px 8px at 70% 65%, #FFD700 0%, #DAA520 50%, transparent 75%),
|
||||
radial-gradient(ellipse 0.8px 7px at 72% 68%, #F4A460 0%, #B8860B 45%, transparent 70%),
|
||||
radial-gradient(ellipse 1.2px 9px at 68% 66%, #FFCC00 0%, #DAA520 40%, transparent 78%),
|
||||
|
||||
/* 散落稻粒 */
|
||||
radial-gradient(ellipse 0.5px 4px at 20% 80%, #FFD700 0%, transparent 60%),
|
||||
radial-gradient(ellipse 0.6px 5px at 80% 20%, #FFCC00 0%, transparent 65%),
|
||||
radial-gradient(ellipse 0.4px 3px at 15% 25%, #DEB887 0%, transparent 55%),
|
||||
radial-gradient(ellipse 0.7px 6px at 85% 75%, #DAA520 0%, transparent 70%),
|
||||
|
||||
/* 稻穗茎秆 - 更细致 */
|
||||
linear-gradient(88deg, transparent 49%, #9ACD32 49.5%, #8FBC8F 50%, #9ACD32 50.5%, transparent 51%),
|
||||
linear-gradient(92deg, transparent 49%, #8FBC8F 49.5%, #228B22 50%, #8FBC8F 50.5%, transparent 51%),
|
||||
linear-gradient(85deg, transparent 49%, #32CD32 49.5%, #9ACD32 50%, #32CD32 50.5%, transparent 51%);
|
||||
|
||||
background-size:
|
||||
25px 25px, 24px 24px, 26px 26px, 23px 23px, 27px 27px,
|
||||
20px 20px, 19px 19px, 21px 21px,
|
||||
22px 22px, 18px 18px, 23px 23px,
|
||||
15px 15px, 16px 16px, 14px 14px, 17px 17px,
|
||||
80px 80px, 85px 85px, 75px 75px;
|
||||
|
||||
background-position:
|
||||
0 0, 12px 12px, 6px 18px, 18px 6px, 3px 21px,
|
||||
40px 40px, 52px 48px, 35px 55px,
|
||||
120px 120px, 135px 115px, 110px 130px,
|
||||
200px 200px, 220px 180px, 180px 220px, 240px 160px,
|
||||
0 0, 40px 40px, 20px 60px;
|
||||
|
||||
opacity: 0.25;
|
||||
pointer-events: none;
|
||||
z-index: -2;
|
||||
animation: wheatSway 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes wheatSway {
|
||||
0%, 100% {
|
||||
transform: translateX(0) rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(5px) rotate(0.5deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-3px) rotate(-0.3deg);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(2px) rotate(0.2deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 大型稻穗背景层 */
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
/* 主稻穗茎秆 - 右侧大型 */
|
||||
linear-gradient(85deg, transparent 45%, #9ACD32 47%, #8FBC8F 48%, #228B22 49%, #8FBC8F 50%, #9ACD32 51%, transparent 53%),
|
||||
linear-gradient(87deg, transparent 46%, #32CD32 47.5%, #9ACD32 48.5%, #8FBC8F 49.5%, #9ACD32 50.5%, #32CD32 51.5%, transparent 54%),
|
||||
|
||||
/* 主稻穗穗头 - 大型椭圆稻粒群 */
|
||||
radial-gradient(ellipse 8px 25px at 75% 15%, #FFD700 0%, #DAA520 30%, #B8860B 60%, transparent 85%),
|
||||
radial-gradient(ellipse 7px 23px at 77% 18%, #FFCC00 0%, #DAA520 35%, transparent 80%),
|
||||
radial-gradient(ellipse 9px 27px at 73% 12%, #F4A460 0%, #B8860B 40%, transparent 88%),
|
||||
radial-gradient(ellipse 6px 22px at 79% 20%, #DEB887 0%, #DAA520 45%, transparent 75%),
|
||||
radial-gradient(ellipse 8px 24px at 75% 16%, #FFD700 0%, #B8860B 38%, transparent 82%),
|
||||
|
||||
/* 稻穗分支 */
|
||||
radial-gradient(ellipse 5px 18px at 72% 25%, #FFCC00 0%, #DAA520 50%, transparent 75%),
|
||||
radial-gradient(ellipse 4px 16px at 78% 28%, #F4A460 0%, #B8860B 45%, transparent 70%),
|
||||
radial-gradient(ellipse 6px 20px at 70% 22%, #DEB887 0%, #DAA520 40%, transparent 78%),
|
||||
radial-gradient(ellipse 5px 17px at 80% 30%, #FFD700 0%, #B8860B 42%, transparent 76%),
|
||||
|
||||
/* 左侧稻穗茎秆 */
|
||||
linear-gradient(95deg, transparent 15%, #9ACD32 17%, #8FBC8F 18%, #228B22 19%, #8FBC8F 20%, #9ACD32 21%, transparent 23%),
|
||||
|
||||
/* 左侧稻穗穗头 */
|
||||
radial-gradient(ellipse 6px 20px at 25% 25%, #FFD700 0%, #DAA520 30%, transparent 80%),
|
||||
radial-gradient(ellipse 5px 18px at 27% 28%, #FFCC00 0%, #B8860B 35%, transparent 75%),
|
||||
radial-gradient(ellipse 7px 22px at 23% 22%, #F4A460 0%, #DAA520 40%, transparent 85%),
|
||||
|
||||
/* 麦田远景效果 */
|
||||
linear-gradient(180deg, transparent 70%, rgba(255, 215, 0, 0.1) 75%, rgba(218, 165, 32, 0.15) 85%, rgba(255, 215, 0, 0.2) 95%, rgba(255, 215, 0, 0.25) 100%),
|
||||
|
||||
/* 散落稻粒 */
|
||||
radial-gradient(ellipse 2px 8px at 60% 40%, #FFD700 0%, transparent 60%),
|
||||
radial-gradient(ellipse 1.5px 6px at 40% 60%, #FFCC00 0%, transparent 65%),
|
||||
radial-gradient(ellipse 2.5px 10px at 85% 50%, #DEB887 0%, transparent 70%),
|
||||
radial-gradient(ellipse 1.8px 7px at 15% 80%, #DAA520 0%, transparent 68%);
|
||||
|
||||
background-size:
|
||||
/* 主茎秆 */
|
||||
100vw 80vh, 100vw 82vh,
|
||||
/* 主穗头 */
|
||||
50vw 60vh, 48vw 58vh, 52vw 62vh, 46vw 56vh, 50vw 60vh,
|
||||
/* 分支 */
|
||||
40vw 50vh, 38vw 48vh, 42vw 52vh, 36vw 46vh,
|
||||
/* 左侧茎秆 */
|
||||
100vw 70vh,
|
||||
/* 左侧穗头 */
|
||||
35vw 45vh, 33vw 43vh, 37vw 47vh,
|
||||
/* 麦田远景 */
|
||||
100vw 100vh,
|
||||
/* 散落稻粒 */
|
||||
20vw 20vh, 25vw 25vh, 30vw 30vh, 22vw 22vh;
|
||||
|
||||
background-position:
|
||||
/* 主茎秆 */
|
||||
70% 20%, 72% 18%,
|
||||
/* 主穗头 */
|
||||
60% 0%, 62% 2%, 58% -2%, 64% 4%, 60% 1%,
|
||||
/* 分支 */
|
||||
65% 15%, 67% 17%, 63% 13%, 69% 19%,
|
||||
/* 左侧茎秆 */
|
||||
20% 30%,
|
||||
/* 左侧穗头 */
|
||||
15% 20%, 17% 22%, 13% 18%,
|
||||
/* 麦田远景 */
|
||||
0% 0%,
|
||||
/* 散落稻粒 */
|
||||
30% 50%, 50% 70%, 80% 40%, 10% 80%;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
animation: wheatSway 25s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spiralRotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 流星效果容器 */
|
||||
.meteor-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 流星轨迹 */
|
||||
.meteor {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background: radial-gradient(circle, #FFD700 0%, #FFA500 50%, transparent 100%);
|
||||
border-radius: 50%;
|
||||
box-shadow:
|
||||
0 0 10px #FFD700,
|
||||
0 0 20px #FFA500,
|
||||
0 0 30px #FF8C00;
|
||||
animation: meteorFall linear infinite;
|
||||
}
|
||||
|
||||
/* 流星尾迹 */
|
||||
.meteor::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
#FFD700 0%,
|
||||
#FFA500 30%,
|
||||
#FF8C00 60%,
|
||||
transparent 100%);
|
||||
transform-origin: 0 50%;
|
||||
transform: rotate(-45deg);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@keyframes meteorFall {
|
||||
0% {
|
||||
transform: translateX(-100px) translateY(-100px);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(100vw + 100px)) translateY(calc(100vh + 100px));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 多个流星的不同轨迹 */
|
||||
.meteor:nth-child(1) {
|
||||
top: 10%;
|
||||
left: -100px;
|
||||
animation-duration: 8s;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.meteor:nth-child(2) {
|
||||
top: 20%;
|
||||
left: -100px;
|
||||
animation-duration: 12s;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.meteor:nth-child(3) {
|
||||
top: 30%;
|
||||
left: -100px;
|
||||
animation-duration: 10s;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
.meteor:nth-child(4) {
|
||||
top: 50%;
|
||||
left: -100px;
|
||||
animation-duration: 15s;
|
||||
animation-delay: 6s;
|
||||
}
|
||||
|
||||
.meteor:nth-child(5) {
|
||||
top: 70%;
|
||||
left: -100px;
|
||||
animation-duration: 9s;
|
||||
animation-delay: 8s;
|
||||
}
|
||||
|
||||
.meteor:nth-child(6) {
|
||||
top: 80%;
|
||||
left: -100px;
|
||||
animation-duration: 11s;
|
||||
animation-delay: 10s;
|
||||
}
|
||||
|
||||
/* 金色粒子效果 */
|
||||
.golden-particles {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background: radial-gradient(circle, #FFD700 0%, #FFA500 70%, transparent 100%);
|
||||
border-radius: 50%;
|
||||
animation: particleFloat linear infinite;
|
||||
}
|
||||
|
||||
@keyframes particleFloat {
|
||||
0% {
|
||||
transform: translateY(100vh) rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-100px) rotate(360deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 粒子的不同位置和动画时长 */
|
||||
.particle:nth-child(1) { left: 10%; animation-duration: 20s; animation-delay: 0s; }
|
||||
.particle:nth-child(2) { left: 20%; animation-duration: 25s; animation-delay: 2s; }
|
||||
.particle:nth-child(3) { left: 30%; animation-duration: 18s; animation-delay: 4s; }
|
||||
.particle:nth-child(4) { left: 40%; animation-duration: 22s; animation-delay: 6s; }
|
||||
.particle:nth-child(5) { left: 50%; animation-duration: 24s; animation-delay: 8s; }
|
||||
.particle:nth-child(6) { left: 60%; animation-duration: 19s; animation-delay: 10s; }
|
||||
.particle:nth-child(7) { left: 70%; animation-duration: 21s; animation-delay: 12s; }
|
||||
.particle:nth-child(8) { left: 80%; animation-duration: 23s; animation-delay: 14s; }
|
||||
.particle:nth-child(9) { left: 90%; animation-duration: 26s; animation-delay: 16s; }
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.meteor {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.meteor::before {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.particle {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
body::before {
|
||||
background-size:
|
||||
30px 30px, 25px 25px, 35px 35px, 28px 28px, 32px 32px,
|
||||
40px 40px, 45px 45px;
|
||||
}
|
||||
|
||||
body::after {
|
||||
background-size: 200px 200px, 150px 150px, 100px 100px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 麦穗飘舞特效 */
|
||||
.wheat-floating {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 移动设备性能优化 */
|
||||
@media (max-width: 768px) {
|
||||
.wheat-floating {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.golden-particles {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.meteor-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.adaptive-overlay {
|
||||
animation: none;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.wheat-particle {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 20px;
|
||||
background: linear-gradient(180deg,
|
||||
#FFD700 0%,
|
||||
#DAA520 50%,
|
||||
#B8860B 100%
|
||||
);
|
||||
border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
|
||||
opacity: 0.7;
|
||||
animation: wheatFloat 15s linear infinite;
|
||||
}
|
||||
|
||||
.wheat-particle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 2px;
|
||||
height: 8px;
|
||||
background: #8B7355;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.wheat-particle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 1px;
|
||||
width: 2px;
|
||||
height: 4px;
|
||||
background: #FFEC8C;
|
||||
border-radius: 50%;
|
||||
box-shadow:
|
||||
3px 2px 0 #FFEC8C,
|
||||
1px 6px 0 #FFEC8C,
|
||||
4px 8px 0 #FFEC8C;
|
||||
}
|
||||
|
||||
@keyframes wheatFloat {
|
||||
0% {
|
||||
transform: translateY(-100vh) translateX(0) rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
90% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100vh) translateX(50px) rotate(360deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 不同大小和速度的麦穗 */
|
||||
.wheat-particle:nth-child(1) {
|
||||
left: 10%;
|
||||
animation-duration: 12s;
|
||||
animation-delay: 0s;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.wheat-particle:nth-child(2) {
|
||||
left: 25%;
|
||||
animation-duration: 18s;
|
||||
animation-delay: 2s;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.wheat-particle:nth-child(3) {
|
||||
left: 40%;
|
||||
animation-duration: 15s;
|
||||
animation-delay: 4s;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.wheat-particle:nth-child(4) {
|
||||
left: 60%;
|
||||
animation-duration: 20s;
|
||||
animation-delay: 1s;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.wheat-particle:nth-child(5) {
|
||||
left: 75%;
|
||||
animation-duration: 14s;
|
||||
animation-delay: 3s;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
|
||||
.wheat-particle:nth-child(6) {
|
||||
left: 90%;
|
||||
animation-duration: 16s;
|
||||
animation-delay: 5s;
|
||||
transform: scale(1.0);
|
||||
}
|
||||
|
||||
.wheat-particle:nth-child(7) {
|
||||
left: 5%;
|
||||
animation-duration: 22s;
|
||||
animation-delay: 6s;
|
||||
transform: scale(0.6);
|
||||
}
|
||||
|
||||
.wheat-particle:nth-child(8) {
|
||||
left: 35%;
|
||||
animation-duration: 13s;
|
||||
animation-delay: 2.5s;
|
||||
transform: scale(1.3);
|
||||
}
|
||||
|
||||
/* 减少动画偏好设置 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
.meteor,
|
||||
.particle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user