Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04a4cb962a | |||
| d48c2647f9 | |||
| bef33251cb | |||
| 1221d6faf1 | |||
| 9d9924dd79 | |||
| c147502b4d | |||
| 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 |
36
.dockerignore
Normal file
36
.dockerignore
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Node modules
|
||||||
|
InfoGenie-frontend/node_modules
|
||||||
|
InfoGenie-frontend/build
|
||||||
|
|
||||||
|
# Python cache
|
||||||
|
InfoGenie-backend/__pycache__
|
||||||
|
InfoGenie-backend/**/__pycache__
|
||||||
|
InfoGenie-backend/*.pyc
|
||||||
|
InfoGenie-backend/**/*.pyc
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
InfoGenie-backend/test
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.backup
|
||||||
|
*.bak
|
||||||
214
.gitignore
vendored
Normal file → Executable file
214
.gitignore
vendored
Normal file → Executable file
@@ -1,209 +1,5 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
#项目自忽略
|
||||||
__pycache__/
|
.vscode
|
||||||
*.py[codz]
|
InfoGenie-frontend/node_modules
|
||||||
*$py.class
|
InfoGenie-frontend/build
|
||||||
|
InfoGenie-backend/__pycache__
|
||||||
# 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/
|
|
||||||
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"
|
||||||
}
|
}
|
||||||
57
Dockerfile
Normal file
57
Dockerfile
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# InfoGenie 统一 Docker 镜像
|
||||||
|
# 多阶段构建:前端构建 + 后端 + Nginx
|
||||||
|
|
||||||
|
# 阶段1: 前端构建
|
||||||
|
FROM node:18-alpine AS frontend-builder
|
||||||
|
|
||||||
|
WORKDIR /frontend
|
||||||
|
COPY InfoGenie-frontend/package*.json ./
|
||||||
|
RUN npm install --legacy-peer-deps
|
||||||
|
COPY InfoGenie-frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 阶段2: 最终镜像
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
# 安装 Nginx 和必要的工具
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
nginx \
|
||||||
|
supervisor \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 复制后端代码
|
||||||
|
COPY InfoGenie-backend/ ./backend/
|
||||||
|
|
||||||
|
# 安装 Python 依赖
|
||||||
|
RUN pip install --no-cache-dir -r ./backend/requirements.txt gunicorn
|
||||||
|
|
||||||
|
# 复制前端构建产物到 Nginx 目录
|
||||||
|
COPY --from=frontend-builder /frontend/build /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 创建持久化数据目录
|
||||||
|
RUN mkdir -p /app/data/logs
|
||||||
|
|
||||||
|
# 复制 Nginx 配置
|
||||||
|
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
COPY docker/default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 复制 Supervisor 配置
|
||||||
|
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
|
# 复制启动脚本
|
||||||
|
COPY docker/entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 2323
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
ENV FLASK_APP=app.py
|
||||||
|
ENV FLASK_ENV=production
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# 使用 supervisor 管理多进程
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
15
InfoGenie-backend/.env
Executable file
15
InfoGenie-backend/.env
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
# InfoGenie 环境变量配置文件
|
||||||
|
# 请勿将此文件提交到版本控制系统
|
||||||
|
|
||||||
|
# 邮件配置
|
||||||
|
MAIL_USERNAME=shumengya888@foxmail.com
|
||||||
|
MAIL_PASSWORD=dpdouefloajfdagd
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
MONGO_URI=mongodb://shumengya:shumengya520@47.108.90.0:27017/InfoGenie?authSource=admin
|
||||||
|
|
||||||
|
# 应用密钥
|
||||||
|
SECRET_KEY=infogenie-secret-key-2025
|
||||||
|
|
||||||
|
# 环境配置
|
||||||
|
FLASK_ENV=development
|
||||||
13
InfoGenie-backend/ai_config.json
Executable file
13
InfoGenie-backend/ai_config.json
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"deepseek": {
|
||||||
|
"api_key": "sk-832f8e5250464de08a31523c7fd712",
|
||||||
|
"api_base": "https://api.deepseek.com",
|
||||||
|
"model": ["deepseek-chat","deepseek-reasoner"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"kimi": {
|
||||||
|
"api_key": "sk-zdg9NBpTlhOcDDpoWfaBKu0KNDdGv18SipORnL2utawja",
|
||||||
|
"api_base": "https://api.moonshot.cn",
|
||||||
|
"model": ["kimi-k2-0905-preview","kimi-k2-0711-preview"]
|
||||||
|
}
|
||||||
|
}
|
||||||
79
backend/app.py → InfoGenie-backend/app.py
Normal file → Executable file
79
backend/app.py → InfoGenie-backend/app.py
Normal file → Executable file
@@ -2,11 +2,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
InfoGenie 后端主应用入口
|
InfoGenie 后端主应用入口
|
||||||
Created by: 神奇万事通
|
Created by: 万象口袋
|
||||||
Date: 2025-09-02
|
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_cors import CORS
|
||||||
from flask_pymongo import PyMongo
|
from flask_pymongo import PyMongo
|
||||||
import os
|
import os
|
||||||
@@ -16,12 +16,13 @@ import secrets
|
|||||||
|
|
||||||
# 导入模块
|
# 导入模块
|
||||||
from modules.auth import auth_bp
|
from modules.auth import auth_bp
|
||||||
from modules.api_60s import api_60s_bp
|
|
||||||
from modules.user_management import user_bp
|
from modules.user_management import user_bp
|
||||||
from modules.email_service import init_mail
|
from modules.email_service import init_mail
|
||||||
|
from modules.aimodelapp import aimodelapp_bp
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
|
|
||||||
|
# 创建Flask应用
|
||||||
def create_app():
|
def create_app():
|
||||||
"""创建Flask应用实例"""
|
"""创建Flask应用实例"""
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@@ -29,7 +30,7 @@ def create_app():
|
|||||||
# 加载配置
|
# 加载配置
|
||||||
app.config.from_object(Config)
|
app.config.from_object(Config)
|
||||||
|
|
||||||
# 启用CORS跨域支持
|
# 启用CORS跨域支持(允许所有源)
|
||||||
CORS(app, supports_credentials=True)
|
CORS(app, supports_credentials=True)
|
||||||
|
|
||||||
# 初始化MongoDB
|
# 初始化MongoDB
|
||||||
@@ -41,21 +42,25 @@ def create_app():
|
|||||||
|
|
||||||
# 注册蓝图
|
# 注册蓝图
|
||||||
app.register_blueprint(auth_bp, url_prefix='/api/auth')
|
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(user_bp, url_prefix='/api/user')
|
||||||
|
app.register_blueprint(aimodelapp_bp, url_prefix='/api/aimodelapp')
|
||||||
|
|
||||||
# 基础路由
|
# 基础路由
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""API根路径"""
|
"""API根路径"""
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'message': '✨ 神奇万事通 API 服务运行中 ✨',
|
'message': '万象口袋 后端 API 服务运行中',
|
||||||
'version': '1.0.0',
|
"description": "提供用户认证、用户管理、聚合API、小游戏接口和AI模型应用接口",
|
||||||
|
"email":"shumengya666@outlook.com",
|
||||||
|
'version': '2.2.0',
|
||||||
'timestamp': datetime.now().isoformat(),
|
'timestamp': datetime.now().isoformat(),
|
||||||
'endpoints': {
|
'endpoints': {
|
||||||
'auth': '/api/auth',
|
'auth': '/api/auth',
|
||||||
'60s_api': '/api/60s',
|
'60s_api': '/api/60s',
|
||||||
'user': '/api/user'
|
'user': '/api/user',
|
||||||
|
'smallgame': '/api/smallgame',
|
||||||
|
'aimodelapp': '/api/aimodelapp'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -102,6 +107,60 @@ def create_app():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': f'文件服务错误: {str(e)}'}), 500
|
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)
|
@app.errorhandler(404)
|
||||||
def not_found(error):
|
def not_found(error):
|
||||||
@@ -122,6 +181,4 @@ def create_app():
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app = create_app()
|
app = create_app()
|
||||||
print("🚀 启动 InfoGenie 后端服务...")
|
print("🚀 启动 InfoGenie 后端服务...")
|
||||||
print("📡 API地址: http://localhost:5000")
|
app.run(debug=True, host='0.0.0.0', port=5002)
|
||||||
print("📚 文档地址: http://localhost:5000/api/health")
|
|
||||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
|
||||||
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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
InfoGenie 配置文件
|
InfoGenie 配置文件
|
||||||
Created by: 神奇万事通
|
Created by: 万象口袋
|
||||||
Date: 2025-09-02
|
Date: 2025-09-02
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -22,11 +22,14 @@ class Config:
|
|||||||
# MongoDB 配置
|
# MongoDB 配置
|
||||||
MONGO_URI = os.environ.get('MONGO_URI') or 'mongodb://localhost:27017/InfoGenie'
|
MONGO_URI = os.environ.get('MONGO_URI') or 'mongodb://localhost:27017/InfoGenie'
|
||||||
|
|
||||||
# Session 配置
|
# hwt 配置
|
||||||
PERMANENT_SESSION_LIFETIME = timedelta(days=7) # 会话持续7天
|
HWT_LIFETIME = timedelta(days=7) # hwt持续7天
|
||||||
SESSION_COOKIE_SECURE = False # 开发环境设为False,生产环境设为True
|
HWT_SECURE = False # 开发环境设为False,生产环境设为True
|
||||||
SESSION_COOKIE_HTTPONLY = True
|
HWT_HTTPONLY = True
|
||||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
HWT_SAMESITE = 'Lax'
|
||||||
|
HWT_DOMAIN = None # 开发环境设为None,生产环境设为具体域名
|
||||||
|
HWT_PATH = '/'
|
||||||
|
HWT_REFRESH_EACH_REQUEST = True # 每次请求刷新hwt过期时间
|
||||||
|
|
||||||
# 邮件配置
|
# 邮件配置
|
||||||
MAIL_SERVER = 'smtp.qq.com'
|
MAIL_SERVER = 'smtp.qq.com'
|
||||||
@@ -35,7 +38,7 @@ class Config:
|
|||||||
MAIL_USE_TLS = False
|
MAIL_USE_TLS = False
|
||||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'your-email@qq.com'
|
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'your-email@qq.com'
|
||||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'your-app-password'
|
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'your-app-password'
|
||||||
MAIL_DEFAULT_SENDER = ('InfoGenie 神奇万事通', os.environ.get('MAIL_USERNAME') or 'your-email@qq.com')
|
MAIL_DEFAULT_SENDER = ('InfoGenie 万象口袋', os.environ.get('MAIL_USERNAME') or 'your-email@qq.com')
|
||||||
|
|
||||||
# API 配置
|
# API 配置
|
||||||
API_RATE_LIMIT = '100 per hour' # API调用频率限制
|
API_RATE_LIMIT = '100 per hour' # API调用频率限制
|
||||||
@@ -43,20 +46,15 @@ class Config:
|
|||||||
# 外部API配置
|
# 外部API配置
|
||||||
EXTERNAL_APIS = {
|
EXTERNAL_APIS = {
|
||||||
'60s': [
|
'60s': [
|
||||||
'https://60s.api.shumengya.top',
|
'https://60s.api.shumengya.top'
|
||||||
'https://60s-cf.viki.moe',
|
|
||||||
'https://60s.viki.moe',
|
|
||||||
'https://60s.b23.run',
|
|
||||||
'https://60s.114128.xyz',
|
|
||||||
'https://60s-cf.114128.xyz'
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
# 应用信息
|
# 应用信息
|
||||||
APP_INFO = {
|
APP_INFO = {
|
||||||
'name': '✨ 神奇万事通 ✨',
|
'name': '✨ 万象口袋 ✨',
|
||||||
'description': '🎨 一个多功能的聚合软件应用 💬',
|
'description': '🎨 一个多功能的聚合软件应用 💬',
|
||||||
'author': '👨💻 by-神奇万事通',
|
'author': '👨💻 by-万象口袋',
|
||||||
'version': '1.0.0',
|
'version': '1.0.0',
|
||||||
'icp': '📄 蜀ICP备2025151694号'
|
'icp': '📄 蜀ICP备2025151694号'
|
||||||
}
|
}
|
||||||
@@ -70,7 +68,7 @@ class ProductionConfig(Config):
|
|||||||
"""生产环境配置"""
|
"""生产环境配置"""
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
TESTING = False
|
TESTING = False
|
||||||
SESSION_COOKIE_SECURE = True
|
HWT_SECURE = True
|
||||||
|
|
||||||
class TestingConfig(Config):
|
class TestingConfig(Config):
|
||||||
"""测试环境配置"""
|
"""测试环境配置"""
|
||||||
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__/game_stats.cpython-313.pyc
Normal file
BIN
InfoGenie-backend/modules/__pycache__/game_stats.cpython-313.pyc
Normal 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.
1087
InfoGenie-backend/modules/aimodelapp.py
Executable file
1087
InfoGenie-backend/modules/aimodelapp.py
Executable file
File diff suppressed because it is too large
Load Diff
163
backend/modules/auth.py → InfoGenie-backend/modules/auth.py
Normal file → Executable file
163
backend/modules/auth.py → InfoGenie-backend/modules/auth.py
Normal file → Executable file
@@ -2,27 +2,77 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
用户认证模块
|
用户认证模块
|
||||||
Created by: 神奇万事通
|
Created by: 万象口袋
|
||||||
Date: 2025-09-02
|
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
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
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
|
from .email_service import send_verification_email, verify_code, is_qq_email, get_qq_avatar_url
|
||||||
|
|
||||||
auth_bp = Blueprint('auth', __name__)
|
auth_bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
#生成JWT token
|
||||||
|
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):
|
def validate_qq_email(email):
|
||||||
"""验证QQ邮箱格式"""
|
"""验证QQ邮箱格式"""
|
||||||
return is_qq_email(email)
|
return is_qq_email(email)
|
||||||
|
|
||||||
|
#验证密码格式
|
||||||
def validate_password(password):
|
def validate_password(password):
|
||||||
"""验证密码格式(6-20位)"""
|
"""验证密码格式(6-20位)"""
|
||||||
return 6 <= len(password) <= 20
|
return 6 <= len(password) <= 20
|
||||||
|
|
||||||
|
|
||||||
|
#==========================对外暴露的HTTP接口==========================
|
||||||
|
#发送验证码邮件
|
||||||
@auth_bp.route('/send-verification', methods=['POST'])
|
@auth_bp.route('/send-verification', methods=['POST'])
|
||||||
def send_verification():
|
def send_verification():
|
||||||
"""发送验证码邮件"""
|
"""发送验证码邮件"""
|
||||||
@@ -78,6 +128,7 @@ def send_verification():
|
|||||||
'message': '发送失败,请稍后重试'
|
'message': '发送失败,请稍后重试'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
#验证验证码
|
||||||
@auth_bp.route('/verify-code', methods=['POST'])
|
@auth_bp.route('/verify-code', methods=['POST'])
|
||||||
def verify_verification_code():
|
def verify_verification_code():
|
||||||
"""验证验证码"""
|
"""验证验证码"""
|
||||||
@@ -108,6 +159,7 @@ def verify_verification_code():
|
|||||||
'message': '验证失败,请稍后重试'
|
'message': '验证失败,请稍后重试'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
#用户注册
|
||||||
@auth_bp.route('/register', methods=['POST'])
|
@auth_bp.route('/register', methods=['POST'])
|
||||||
def register():
|
def register():
|
||||||
"""用户注册(需要先验证邮箱)"""
|
"""用户注册(需要先验证邮箱)"""
|
||||||
@@ -176,7 +228,15 @@ def register():
|
|||||||
'注册时间': datetime.now().isoformat(),
|
'注册时间': datetime.now().isoformat(),
|
||||||
'最后登录': None,
|
'最后登录': None,
|
||||||
'登录次数': 0,
|
'登录次数': 0,
|
||||||
'用户状态': 'active'
|
'用户状态': 'active',
|
||||||
|
'等级': 0,
|
||||||
|
'经验': 0,
|
||||||
|
'萌芽币': 0,
|
||||||
|
'签到系统': {
|
||||||
|
'连续签到天数': 0,
|
||||||
|
'今日是否已签到': False,
|
||||||
|
'签到时间': '2025-01-01'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result = users_collection.insert_one(user_data)
|
result = users_collection.insert_one(user_data)
|
||||||
@@ -204,42 +264,7 @@ def register():
|
|||||||
'message': '注册失败,请稍后重试'
|
'message': '注册失败,请稍后重试'
|
||||||
}), 500
|
}), 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'])
|
@auth_bp.route('/login', methods=['POST'])
|
||||||
def login():
|
def login():
|
||||||
"""用户登录(支持邮箱+验证码或邮箱+密码)"""
|
"""用户登录(支持邮箱+验证码或邮箱+密码)"""
|
||||||
@@ -313,15 +338,18 @@ def login():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# 设置会话
|
# 生成JWT token
|
||||||
session['user_id'] = str(user['_id'])
|
user_data = {
|
||||||
session['email'] = email
|
'user_id': str(user['_id']),
|
||||||
session['username'] = user.get('用户名', '')
|
'email': email,
|
||||||
session.permanent = True
|
'username': user.get('用户名', '')
|
||||||
|
}
|
||||||
|
token = generate_token(user_data)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': '登录成功!',
|
'message': '登录成功!',
|
||||||
|
'token': token,
|
||||||
'user': {
|
'user': {
|
||||||
'id': str(user['_id']),
|
'id': str(user['_id']),
|
||||||
'email': email,
|
'email': email,
|
||||||
@@ -339,9 +367,10 @@ def login():
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
# 登录成功,创建会话
|
# 登录成功,创建会话
|
||||||
session['user_id'] = str(user['_id'])
|
hwt = getattr(request, 'hwt', {})
|
||||||
session['account'] = user['账号']
|
hwt['user_id'] = str(user['_id'])
|
||||||
session['logged_in'] = True
|
hwt['account'] = user['账号']
|
||||||
|
hwt['logged_in'] = True
|
||||||
|
|
||||||
# 更新登录信息
|
# 更新登录信息
|
||||||
users_collection.update_one(
|
users_collection.update_one(
|
||||||
@@ -368,21 +397,16 @@ def login():
|
|||||||
'message': f'服务器错误: {str(e)}'
|
'message': f'服务器错误: {str(e)}'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
#用户登出
|
||||||
@auth_bp.route('/logout', methods=['POST'])
|
@auth_bp.route('/logout', methods=['POST'])
|
||||||
def logout():
|
def logout():
|
||||||
"""用户登出"""
|
"""用户登出"""
|
||||||
try:
|
try:
|
||||||
if 'logged_in' in session:
|
# JWT是无状态的,客户端删除token即可
|
||||||
session.clear()
|
return jsonify({
|
||||||
return jsonify({
|
'success': True,
|
||||||
'success': True,
|
'message': '已成功登出'
|
||||||
'message': '已成功登出'
|
}), 200
|
||||||
}), 200
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '用户未登录'
|
|
||||||
}), 401
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -390,17 +414,31 @@ def logout():
|
|||||||
'message': f'服务器错误: {str(e)}'
|
'message': f'服务器错误: {str(e)}'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
#检查登录状态
|
||||||
@auth_bp.route('/check', methods=['GET'])
|
@auth_bp.route('/check', methods=['GET'])
|
||||||
def check_login():
|
def check_login():
|
||||||
"""检查登录状态"""
|
"""检查登录状态"""
|
||||||
try:
|
try:
|
||||||
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({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'logged_in': True,
|
'logged_in': True,
|
||||||
'user': {
|
'user': {
|
||||||
'account': session.get('account'),
|
'id': user_data['user_id'],
|
||||||
'user_id': session.get('user_id')
|
'email': user_data['email'],
|
||||||
|
'username': user_data['username']
|
||||||
}
|
}
|
||||||
}), 200
|
}), 200
|
||||||
else:
|
else:
|
||||||
@@ -414,3 +452,4 @@ def check_login():
|
|||||||
'success': False,
|
'success': False,
|
||||||
'message': f'服务器错误: {str(e)}'
|
'message': f'服务器错误: {str(e)}'
|
||||||
}), 500
|
}), 500
|
||||||
|
#==========================对外暴露的HTTP接口==========================
|
||||||
15
backend/modules/email_service.py → InfoGenie-backend/modules/email_service.py
Normal file → Executable file
15
backend/modules/email_service.py → InfoGenie-backend/modules/email_service.py
Normal file → Executable file
@@ -18,15 +18,18 @@ import os
|
|||||||
# 验证码存储(生产环境建议使用Redis)
|
# 验证码存储(生产环境建议使用Redis)
|
||||||
verification_codes = {}
|
verification_codes = {}
|
||||||
|
|
||||||
|
# 初始化日志
|
||||||
def init_mail(app):
|
def init_mail(app):
|
||||||
"""初始化邮件配置"""
|
"""初始化邮件配置"""
|
||||||
# 使用smtplib直接发送,不需要Flask-Mail
|
# 使用smtplib直接发送,不需要Flask-Mail
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# 生成验证码
|
||||||
def generate_verification_code(length=6):
|
def generate_verification_code(length=6):
|
||||||
"""生成验证码"""
|
"""生成验证码"""
|
||||||
return ''.join(random.choices(string.digits, k=length))
|
return ''.join(random.choices(string.digits, k=length))
|
||||||
|
|
||||||
|
# 发送验证邮件
|
||||||
def send_verification_email(email, verification_type='register'):
|
def send_verification_email(email, verification_type='register'):
|
||||||
"""
|
"""
|
||||||
发送验证邮件
|
发送验证邮件
|
||||||
@@ -63,14 +66,14 @@ def send_verification_email(email, verification_type='register'):
|
|||||||
|
|
||||||
# 邮件模板
|
# 邮件模板
|
||||||
if verification_type == 'register':
|
if verification_type == 'register':
|
||||||
subject = '【InfoGenie】注册验证码'
|
subject = '【万象口袋】注册验证码'
|
||||||
html_content = f'''
|
html_content = f'''
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
<div style="text-align: center; margin-bottom: 30px;">
|
<div style="text-align: center; margin-bottom: 30px;">
|
||||||
<h1 style="color: #66bb6a; margin: 0;">InfoGenie 神奇万事通</h1>
|
<h1 style="color: #66bb6a; margin: 0;">万象口袋</h1>
|
||||||
<p style="color: #666; font-size: 14px; margin: 5px 0;">欢迎注册InfoGenie</p>
|
<p style="color: #666; font-size: 14px; margin: 5px 0;">欢迎注册万象口袋</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 100%); padding: 30px; border-radius: 15px; text-align: center;">
|
<div style="background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 100%); padding: 30px; border-radius: 15px; text-align: center;">
|
||||||
@@ -98,7 +101,7 @@ def send_verification_email(email, verification_type='register'):
|
|||||||
<body>
|
<body>
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
<div style="text-align: center; margin-bottom: 30px;">
|
<div style="text-align: center; margin-bottom: 30px;">
|
||||||
<h1 style="color: #66bb6a; margin: 0;">InfoGenie 神奇万事通</h1>
|
<h1 style="color: #66bb6a; margin: 0;">InfoGenie 万象口袋</h1>
|
||||||
<p style="color: #666; font-size: 14px; margin: 5px 0;">安全登录验证</p>
|
<p style="color: #666; font-size: 14px; margin: 5px 0;">安全登录验证</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -168,6 +171,7 @@ def send_verification_email(email, verification_type='register'):
|
|||||||
'message': '邮件发送失败,请稍后重试'
|
'message': '邮件发送失败,请稍后重试'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 验证验证码
|
||||||
def verify_code(email, code):
|
def verify_code(email, code):
|
||||||
"""
|
"""
|
||||||
验证验证码
|
验证验证码
|
||||||
@@ -221,6 +225,7 @@ def verify_code(email, code):
|
|||||||
'type': verification_type
|
'type': verification_type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 验证QQ邮箱格式
|
||||||
def is_qq_email(email):
|
def is_qq_email(email):
|
||||||
"""
|
"""
|
||||||
验证是否为QQ邮箱
|
验证是否为QQ邮箱
|
||||||
@@ -239,6 +244,7 @@ def is_qq_email(email):
|
|||||||
|
|
||||||
return domain in qq_domains
|
return domain in qq_domains
|
||||||
|
|
||||||
|
# 获取QQ头像URL
|
||||||
def get_qq_avatar_url(email):
|
def get_qq_avatar_url(email):
|
||||||
"""
|
"""
|
||||||
根据QQ邮箱获取QQ头像URL
|
根据QQ邮箱获取QQ头像URL
|
||||||
@@ -262,6 +268,7 @@ def get_qq_avatar_url(email):
|
|||||||
# 返回QQ头像API URL
|
# 返回QQ头像API URL
|
||||||
return f"http://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100"
|
return f"http://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100"
|
||||||
|
|
||||||
|
# 清理过期验证码
|
||||||
def cleanup_expired_codes():
|
def cleanup_expired_codes():
|
||||||
"""清理过期的验证码"""
|
"""清理过期的验证码"""
|
||||||
current_time = datetime.now()
|
current_time = datetime.now()
|
||||||
550
InfoGenie-backend/modules/user_management.py
Executable file
550
InfoGenie-backend/modules/user_management.py
Executable file
@@ -0,0 +1,550 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
|
||||||
|
#==========================对外暴露的HTTP接口==========================
|
||||||
|
# 获取用户资料
|
||||||
|
@user_bp.route('/profile', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_profile():
|
||||||
|
"""获取用户资料"""
|
||||||
|
try:
|
||||||
|
# 优先从JWT token获取用户信息
|
||||||
|
user_id = None
|
||||||
|
if hasattr(request, 'current_user') and request.current_user:
|
||||||
|
user_id = request.current_user.get('user_id')
|
||||||
|
else:
|
||||||
|
# 回退到hwt验证
|
||||||
|
hwt = getattr(request, 'hwt', {})
|
||||||
|
user_id = hwt.get('user_id')
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '无法获取用户信息'
|
||||||
|
}), 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': '用户不存在'
|
||||||
|
}), 404
|
||||||
|
# 返回用户信息(不包含密码)
|
||||||
|
profile = {
|
||||||
|
'account': user.get('邮箱'),
|
||||||
|
'username': user.get('用户名'),
|
||||||
|
'avatar': user.get('头像'),
|
||||||
|
'register_time': user.get('注册时间'),
|
||||||
|
'last_login': user.get('最后登录'),
|
||||||
|
'login_count': user.get('登录次数', 0),
|
||||||
|
'status': user.get('用户状态', 'active'),
|
||||||
|
'level': user.get('等级', 1),
|
||||||
|
'experience': user.get('经验', 0),
|
||||||
|
'coins': user.get('萌芽币', 0)
|
||||||
|
}
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': profile
|
||||||
|
}), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'服务器错误: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# 为指定账号增加萌芽币
|
||||||
|
@user_bp.route('/add-coins', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def add_coins():
|
||||||
|
"""为指定账号增加萌芽币(支持email或username指定账号,amount为正整数)"""
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
email = (data.get('email') or '').strip()
|
||||||
|
username = (data.get('username') or '').strip()
|
||||||
|
amount = data.get('amount')
|
||||||
|
|
||||||
|
# 参数校验
|
||||||
|
if not email and not username:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '请提供email或username其中之一'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
if amount is None:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'amount不能为空'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
amount_int = int(amount)
|
||||||
|
except Exception:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'amount必须为整数'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
if amount_int <= 0:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'amount必须为正整数'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
users_collection = current_app.mongo.db.userdata
|
||||||
|
query = {'邮箱': email} if email else {'用户名': username}
|
||||||
|
user = users_collection.find_one(query)
|
||||||
|
if not user:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '用户不存在'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
before_coins = user.get('萌芽币', 0)
|
||||||
|
update_result = users_collection.update_one(query, {'$inc': {'萌芽币': amount_int}})
|
||||||
|
|
||||||
|
if update_result.modified_count == 0:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '更新失败,请稍后重试'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
updated = users_collection.find_one({'_id': user['_id']})
|
||||||
|
new_coins = updated.get('萌芽币', before_coins)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': f"已为账户{email or username}增加{amount_int}萌芽币",
|
||||||
|
'data': {
|
||||||
|
'before_coins': before_coins,
|
||||||
|
'added': amount_int,
|
||||||
|
'new_coins': new_coins,
|
||||||
|
'user': {
|
||||||
|
'id': str(updated.get('_id')),
|
||||||
|
'email': updated.get('邮箱'),
|
||||||
|
'username': updated.get('用户名'),
|
||||||
|
'avatar': updated.get('头像'),
|
||||||
|
'register_time': updated.get('注册时间'),
|
||||||
|
'last_login': updated.get('最后登录'),
|
||||||
|
'login_count': updated.get('登录次数', 0),
|
||||||
|
'status': updated.get('用户状态', 'active'),
|
||||||
|
'level': updated.get('等级', 0),
|
||||||
|
'experience': updated.get('经验', 0),
|
||||||
|
'coins': new_coins
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'服务器错误: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# 列出所有用户
|
||||||
|
@user_bp.route('/list', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def list_users():
|
||||||
|
"""列出所有用户(不返回密码)"""
|
||||||
|
try:
|
||||||
|
users_collection = current_app.mongo.db.userdata
|
||||||
|
cursor = users_collection.find({}, {'密码': 0})
|
||||||
|
users = []
|
||||||
|
for u in cursor:
|
||||||
|
users.append({
|
||||||
|
'id': str(u.get('_id')),
|
||||||
|
'email': u.get('邮箱'),
|
||||||
|
'username': u.get('用户名'),
|
||||||
|
'avatar': u.get('头像'),
|
||||||
|
'register_time': u.get('注册时间'),
|
||||||
|
'last_login': u.get('最后登录'),
|
||||||
|
'login_count': u.get('登录次数', 0),
|
||||||
|
'status': u.get('用户状态', 'active'),
|
||||||
|
'level': u.get('等级', 0),
|
||||||
|
'experience': u.get('经验', 0),
|
||||||
|
'coins': u.get('萌芽币', 0)
|
||||||
|
})
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'count': len(users),
|
||||||
|
'data': users
|
||||||
|
}), 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
|
||||||
|
#==========================对外暴露的HTTP接口==========================
|
||||||
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
|
Werkzeug==2.3.7
|
||||||
|
|
||||||
|
# JWT认证
|
||||||
|
PyJWT==2.8.0
|
||||||
|
|
||||||
# HTTP请求
|
# HTTP请求
|
||||||
requests==2.31.0
|
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
100
InfoGenie-backend/test/test_add_coins.py
Normal file
100
InfoGenie-backend/test/test_add_coins.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
测试为指定账号增加萌芽币接口 (/api/user/add-coins)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# 加入后端根目录到路径,导入create_app
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from app import create_app
|
||||||
|
from modules.auth import generate_token
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
|
|
||||||
|
def run_test():
|
||||||
|
"""运行加币接口测试,打印真实响应并断言结果"""
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
db = app.mongo.db
|
||||||
|
users = db.userdata
|
||||||
|
|
||||||
|
# 构造一个临时测试用户(真实写库,测试结束删除)
|
||||||
|
test_email = "infogenie.test.addcoins@foxmail.com"
|
||||||
|
users.delete_many({'邮箱': test_email})
|
||||||
|
test_user = {
|
||||||
|
'邮箱': test_email,
|
||||||
|
'用户名': '测试用户_加币',
|
||||||
|
'密码': generate_password_hash('AddCoins123!'),
|
||||||
|
'头像': None,
|
||||||
|
'注册时间': datetime.now().isoformat(),
|
||||||
|
'最后登录': None,
|
||||||
|
'登录次数': 0,
|
||||||
|
'用户状态': 'active',
|
||||||
|
'等级': 0,
|
||||||
|
'经验': 0,
|
||||||
|
'萌芽币': 0,
|
||||||
|
'签到系统': {
|
||||||
|
'连续签到天数': 0,
|
||||||
|
'今日是否已签到': False,
|
||||||
|
'签到时间': datetime.now().strftime('%Y-%m-%d')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
insert_result = users.insert_one(test_user)
|
||||||
|
test_user_id = str(insert_result.inserted_id)
|
||||||
|
|
||||||
|
# 生成有效JWT用于认证
|
||||||
|
token = generate_token({
|
||||||
|
'user_id': test_user_id,
|
||||||
|
'email': test_email,
|
||||||
|
'username': test_user['用户名']
|
||||||
|
})
|
||||||
|
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
# 第一次加币: +500
|
||||||
|
resp1 = client.post(
|
||||||
|
'/api/user/add-coins',
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
json={'email': test_email, 'amount': 500}
|
||||||
|
)
|
||||||
|
print('第一次加币 状态码:', resp1.status_code)
|
||||||
|
data1 = resp1.get_json()
|
||||||
|
print('第一次加币 响应:')
|
||||||
|
print(json.dumps(data1, ensure_ascii=False, indent=2))
|
||||||
|
assert resp1.status_code == 200
|
||||||
|
assert data1.get('success') is True
|
||||||
|
assert data1['data']['before_coins'] == 0
|
||||||
|
assert data1['data']['added'] == 500
|
||||||
|
assert data1['data']['new_coins'] == 500
|
||||||
|
|
||||||
|
# 第二次加币: +200
|
||||||
|
resp2 = client.post(
|
||||||
|
'/api/user/add-coins',
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
json={'email': test_email, 'amount': 200}
|
||||||
|
)
|
||||||
|
print('第二次加币 状态码:', resp2.status_code)
|
||||||
|
data2 = resp2.get_json()
|
||||||
|
print('第二次加币 响应:')
|
||||||
|
print(json.dumps(data2, ensure_ascii=False, indent=2))
|
||||||
|
assert resp2.status_code == 200
|
||||||
|
assert data2.get('success') is True
|
||||||
|
assert data2['data']['before_coins'] == 500
|
||||||
|
assert data2['data']['added'] == 200
|
||||||
|
assert data2['data']['new_coins'] == 700
|
||||||
|
|
||||||
|
# 清理临时测试用户
|
||||||
|
users.delete_many({'邮箱': test_email})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print('🔧 开始测试 /api/user/add-coins 接口...')
|
||||||
|
run_test()
|
||||||
|
print('✅ 测试完成!')
|
||||||
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
81
InfoGenie-backend/test/test_user_list.py
Normal file
81
InfoGenie-backend/test/test_user_list.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
测试列出所有用户的HTTP接口 (/api/user/list)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# 将后端根目录加入路径,便于导入app
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from app import create_app
|
||||||
|
from modules.auth import generate_token
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
|
|
||||||
|
def run_test():
|
||||||
|
"""运行用户列表接口测试,输出真实数据"""
|
||||||
|
# 使用.env中的真实Mongo配置,不造假
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
db = app.mongo.db
|
||||||
|
users = db.userdata
|
||||||
|
|
||||||
|
# 插入一个测试用户(真实写入后再删除),确保可验证接口输出
|
||||||
|
test_email = "infogenie.test.user@foxmail.com"
|
||||||
|
users.delete_many({'邮箱': test_email})
|
||||||
|
test_user = {
|
||||||
|
'邮箱': test_email,
|
||||||
|
'用户名': '测试用户_列表',
|
||||||
|
'密码': generate_password_hash('TestPass123!'),
|
||||||
|
'头像': None,
|
||||||
|
'注册时间': datetime.now().isoformat(),
|
||||||
|
'最后登录': None,
|
||||||
|
'登录次数': 0,
|
||||||
|
'用户状态': 'active',
|
||||||
|
'等级': 0,
|
||||||
|
'经验': 0,
|
||||||
|
'萌芽币': 0,
|
||||||
|
'签到系统': {
|
||||||
|
'连续签到天数': 0,
|
||||||
|
'今日是否已签到': False,
|
||||||
|
'签到时间': datetime.now().strftime('%Y-%m-%d')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
insert_result = users.insert_one(test_user)
|
||||||
|
test_user_id = str(insert_result.inserted_id)
|
||||||
|
|
||||||
|
# 生成有效JWT,满足认证要求
|
||||||
|
token = generate_token({
|
||||||
|
'user_id': test_user_id,
|
||||||
|
'email': test_email,
|
||||||
|
'username': test_user['用户名']
|
||||||
|
})
|
||||||
|
|
||||||
|
client = app.test_client()
|
||||||
|
resp = client.get('/api/user/list', headers={'Authorization': f'Bearer {token}'})
|
||||||
|
|
||||||
|
print("状态码:", resp.status_code)
|
||||||
|
data = resp.get_json()
|
||||||
|
print("响应内容:")
|
||||||
|
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
# 基本断言,确保返回真实列表数据且包含刚插入的测试用户
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert data.get('success') is True
|
||||||
|
assert isinstance(data.get('data'), list)
|
||||||
|
assert any(u.get('email') == test_email for u in data['data'])
|
||||||
|
|
||||||
|
# 清理测试数据
|
||||||
|
users.delete_many({'邮箱': test_email})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print('🔎 开始测试 /api/user/list 接口...')
|
||||||
|
run_test()
|
||||||
|
print('✅ 测试完成!')
|
||||||
396
InfoGenie-backend/后端架构文档.md
Executable file
396
InfoGenie-backend/后端架构文档.md
Executable file
@@ -0,0 +1,396 @@
|
|||||||
|
# InfoGenie 后端架构文档
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
InfoGenie(万象口袋)是一个基于前后端分离架构的多功能聚合软件应用。后端采用Flask框架提供RESTful API服务,前端通过HTTP请求调用后端API,实现数据交互和业务逻辑处理。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
### 核心框架
|
||||||
|
- **Web框架**: Flask 2.3.3
|
||||||
|
- **数据库**: MongoDB (Flask-PyMongo 2.3.0)
|
||||||
|
- **认证**: JWT (PyJWT 2.8.0)
|
||||||
|
- **跨域**: Flask-CORS 4.0.0
|
||||||
|
|
||||||
|
### 辅助工具
|
||||||
|
- **邮件服务**: Flask-Mail 0.9.1
|
||||||
|
- **密码加密**: Werkzeug 2.3.7
|
||||||
|
- **环境配置**: python-dotenv 1.0.0
|
||||||
|
- **API限流**: Flask-Limiter 3.5.0
|
||||||
|
|
||||||
|
## 架构设计原则
|
||||||
|
|
||||||
|
### 前后端分离
|
||||||
|
- 后端专注于数据处理和业务逻辑
|
||||||
|
- 前端负责用户界面和交互体验
|
||||||
|
- 通过RESTful API进行数据交换
|
||||||
|
- 完全解耦,便于独立开发和部署
|
||||||
|
|
||||||
|
### 模块化设计
|
||||||
|
- 按功能划分独立模块
|
||||||
|
- 每个模块职责单一
|
||||||
|
- 便于维护和扩展
|
||||||
|
|
||||||
|
## 核心模块详解
|
||||||
|
|
||||||
|
### 1. 认证模块 (auth.py)
|
||||||
|
|
||||||
|
**功能职责**:
|
||||||
|
- 用户注册和登录
|
||||||
|
- JWT Token生成和管理
|
||||||
|
- 邮箱验证码验证
|
||||||
|
- QQ邮箱格式验证
|
||||||
|
|
||||||
|
**API端点**:
|
||||||
|
```
|
||||||
|
POST /api/auth/send-verification # 发送验证码
|
||||||
|
POST /api/auth/verify-code # 验证验证码
|
||||||
|
POST /api/auth/register # 用户注册
|
||||||
|
POST /api/auth/login # 用户登录
|
||||||
|
POST /api/auth/logout # 用户登出
|
||||||
|
GET /api/auth/check # 检查登录状态
|
||||||
|
```
|
||||||
|
|
||||||
|
**数据流程**:
|
||||||
|
1. 前端发送注册/登录请求
|
||||||
|
2. 后端验证邮箱格式(仅支持QQ邮箱)
|
||||||
|
3. 发送验证码邮件到用户邮箱
|
||||||
|
4. 用户输入验证码完成验证
|
||||||
|
5. 验证成功后生成JWT Token返回给前端
|
||||||
|
|
||||||
|
**安全特性**:
|
||||||
|
- 密码使用Werkzeug进行哈希加密
|
||||||
|
- JWT Token 7天有效期
|
||||||
|
- 验证码5分钟有效期,限制尝试次数
|
||||||
|
|
||||||
|
### 2. 用户管理模块 (user_management.py)
|
||||||
|
|
||||||
|
**功能职责**:
|
||||||
|
- 用户资料管理
|
||||||
|
- 密码修改
|
||||||
|
- 每日签到系统
|
||||||
|
- 用户游戏数据管理
|
||||||
|
- 账户删除
|
||||||
|
|
||||||
|
**API端点**:
|
||||||
|
```
|
||||||
|
GET /api/user/profile # 获取用户资料
|
||||||
|
POST /api/user/change-password # 修改密码
|
||||||
|
GET /api/user/stats # 获取用户统计
|
||||||
|
GET /api/user/game-data # 获取游戏数据
|
||||||
|
POST /api/user/checkin # 每日签到
|
||||||
|
POST /api/user/delete # 删除账户
|
||||||
|
```
|
||||||
|
|
||||||
|
**数据结构**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"邮箱": "user@qq.com",
|
||||||
|
"用户名": "用户名",
|
||||||
|
"密码": "哈希密码",
|
||||||
|
"头像": "QQ头像URL",
|
||||||
|
"注册时间": "2025-01-01T00:00:00",
|
||||||
|
"最后登录": "2025-01-01T00:00:00",
|
||||||
|
"登录次数": 10,
|
||||||
|
"用户状态": "active",
|
||||||
|
"等级": 5,
|
||||||
|
"经验": 1200,
|
||||||
|
"萌芽币": 1500,
|
||||||
|
"签到系统": {
|
||||||
|
"连续签到天数": 7,
|
||||||
|
"今日是否已签到": true,
|
||||||
|
"签到时间": "2025-01-01"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务逻辑**:
|
||||||
|
- 签到奖励:300萌芽币 + 200经验
|
||||||
|
- 等级升级:100 × 1.2^(等级) 经验需求
|
||||||
|
|
||||||
|
### 3. 邮件服务模块 (email_service.py)
|
||||||
|
|
||||||
|
**功能职责**:
|
||||||
|
- 验证码邮件发送
|
||||||
|
- QQ邮箱格式验证
|
||||||
|
- QQ头像获取
|
||||||
|
- 邮件模板管理
|
||||||
|
|
||||||
|
**邮件模板**:
|
||||||
|
- 注册验证码邮件(HTML格式)
|
||||||
|
- 登录验证码邮件(HTML格式)
|
||||||
|
- 支持自定义邮件内容和样式
|
||||||
|
|
||||||
|
**安全考虑**:
|
||||||
|
- 仅支持QQ邮箱(qq.com、vip.qq.com、foxmail.com)
|
||||||
|
- 使用SSL加密连接
|
||||||
|
- 验证码存储在内存中(生产环境建议使用Redis)
|
||||||
|
|
||||||
|
### 4. AI模型应用模块 (aimodelapp.py)
|
||||||
|
|
||||||
|
**功能职责**:
|
||||||
|
- 集成多种AI服务(DeepSeek、Kimi)
|
||||||
|
- 提供AI功能API接口
|
||||||
|
- 统一AI接口调用
|
||||||
|
- 管理用户萌芽币消费(每次调用消耗100萌芽币)
|
||||||
|
|
||||||
|
**支持的AI功能**:
|
||||||
|
1. **AI聊天接口** (`/api/aimodelapp/chat`)
|
||||||
|
2. **姓名分析** (`/api/aimodelapp/name-analysis`)
|
||||||
|
3. **变量命名助手** (`/api/aimodelapp/variable-naming`)
|
||||||
|
4. **AI写诗助手** (`/api/aimodelapp/poetry`)
|
||||||
|
5. **AI语言翻译** (`/api/aimodelapp/translation`)
|
||||||
|
6. **现代文转文言文** (`/api/aimodelapp/classical_conversion`)
|
||||||
|
7. **AI表情制作器** (`/api/aimodelapp/expression-maker`)
|
||||||
|
8. **Linux命令生成** (`/api/aimodelapp/linux-command`)
|
||||||
|
9. **获取可用模型** (`/api/aimodelapp/models`)
|
||||||
|
|
||||||
|
**AI配置**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"deepseek": {
|
||||||
|
"api_key": "your-api-key",
|
||||||
|
"api_base": "https://api.deepseek.com",
|
||||||
|
"model": ["deepseek-chat", "deepseek-reasoner"]
|
||||||
|
},
|
||||||
|
"kimi": {
|
||||||
|
"api_key": "your-api-key",
|
||||||
|
"api_base": "https://api.moonshot.cn",
|
||||||
|
"model": ["kimi-k2-0905-preview", "kimi-k2-0711-preview"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**调用流程**:
|
||||||
|
1. 前端发送AI请求(包含消息、模型提供商等参数)
|
||||||
|
2. 后端加载AI配置文件
|
||||||
|
3. 调用对应AI API(带重试机制)
|
||||||
|
4. 返回AI响应给前端
|
||||||
|
|
||||||
|
## API设计规范
|
||||||
|
|
||||||
|
### 请求/响应格式
|
||||||
|
|
||||||
|
**成功响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {...},
|
||||||
|
"message": "操作成功",
|
||||||
|
"timestamp": "2025-01-01T00:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "错误信息",
|
||||||
|
"error": "错误详情"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
**JWT Token认证**:
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**支持的认证端点**:
|
||||||
|
- 所有 `/api/user/*` 端点需要认证
|
||||||
|
- 部分 `/api/aimodelapp/*` 端点需要认证
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
|
||||||
|
**HTTP状态码**:
|
||||||
|
- 200: 成功
|
||||||
|
- 400: 请求参数错误
|
||||||
|
- 401: 未认证/认证失败
|
||||||
|
- 403: 权限不足
|
||||||
|
- 404: 资源不存在
|
||||||
|
- 409: 资源冲突
|
||||||
|
- 500: 服务器内部错误
|
||||||
|
|
||||||
|
## 数据库设计
|
||||||
|
|
||||||
|
### MongoDB集合
|
||||||
|
|
||||||
|
**主要集合**: `userdata`
|
||||||
|
- 存储所有用户相关数据
|
||||||
|
- 支持动态字段扩展
|
||||||
|
- 使用ObjectId作为用户唯一标识
|
||||||
|
|
||||||
|
### 数据关系
|
||||||
|
- 用户数据自包含,无复杂关联
|
||||||
|
- 通过用户ID进行数据关联
|
||||||
|
- 支持水平扩展
|
||||||
|
|
||||||
|
## 部署和配置
|
||||||
|
|
||||||
|
### 环境配置
|
||||||
|
|
||||||
|
**必需环境变量**:
|
||||||
|
```
|
||||||
|
SECRET_KEY=your-secret-key
|
||||||
|
MONGO_URI=mongodb://localhost:27017/InfoGenie
|
||||||
|
MAIL_USERNAME=your-email@qq.com
|
||||||
|
MAIL_PASSWORD=your-app-password
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动方式
|
||||||
|
|
||||||
|
**开发环境**:
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**生产环境**:
|
||||||
|
- 支持Docker部署
|
||||||
|
- 提供docker-compose配置
|
||||||
|
- 支持Gunicorn WSGI服务器
|
||||||
|
|
||||||
|
### 静态文件服务
|
||||||
|
|
||||||
|
**支持的前端资源**:
|
||||||
|
- `/60sapi/*`: 60秒API相关文件
|
||||||
|
- `/smallgame/*`: 小游戏相关文件
|
||||||
|
- `/aimodelapp/*`: AI模型应用相关文件
|
||||||
|
|
||||||
|
## 安全考虑
|
||||||
|
|
||||||
|
### 数据安全
|
||||||
|
- 密码哈希存储
|
||||||
|
- JWT Token安全传输
|
||||||
|
- 输入数据验证和过滤
|
||||||
|
|
||||||
|
### API安全
|
||||||
|
- CORS配置(生产环境限制域名)
|
||||||
|
- API限流保护
|
||||||
|
- 请求日志记录
|
||||||
|
|
||||||
|
### 部署安全
|
||||||
|
- 环境变量管理敏感信息
|
||||||
|
- HTTPS证书配置
|
||||||
|
- 防火墙和访问控制
|
||||||
|
|
||||||
|
## 前后端协作指南
|
||||||
|
|
||||||
|
### 前端调用示例
|
||||||
|
|
||||||
|
**用户登录**:
|
||||||
|
```javascript
|
||||||
|
// 1. 发送验证码
|
||||||
|
fetch('/api/auth/send-verification', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email: 'user@qq.com', type: 'login' })
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 验证验证码并登录
|
||||||
|
fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: 'user@qq.com',
|
||||||
|
code: '123456'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 保存token到localStorage
|
||||||
|
localStorage.setItem('token', response.token);
|
||||||
|
```
|
||||||
|
|
||||||
|
**调用需要认证的API**:
|
||||||
|
```javascript
|
||||||
|
fetch('/api/user/profile', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据约定
|
||||||
|
|
||||||
|
**前端发送数据格式**:
|
||||||
|
- 所有请求使用JSON格式
|
||||||
|
- 必填字段验证
|
||||||
|
- 参数命名使用snake_case
|
||||||
|
|
||||||
|
**后端返回数据格式**:
|
||||||
|
- 统一响应格式
|
||||||
|
- 时间戳使用ISO格式
|
||||||
|
- 错误信息清晰明确
|
||||||
|
|
||||||
|
### 开发协作流程
|
||||||
|
|
||||||
|
1. **API设计阶段**:
|
||||||
|
- 后端定义API接口规范
|
||||||
|
- 前端根据规范开发调用代码
|
||||||
|
- 约定数据格式和错误处理
|
||||||
|
|
||||||
|
2. **联调阶段**:
|
||||||
|
- 使用统一的测试数据
|
||||||
|
- 验证各种边界情况
|
||||||
|
- 确认错误处理逻辑
|
||||||
|
|
||||||
|
3. **部署阶段**:
|
||||||
|
- 后端部署API服务
|
||||||
|
- 前端配置API基础URL
|
||||||
|
- 验证跨域和认证配置
|
||||||
|
|
||||||
|
## 新功能添加
|
||||||
|
|
||||||
|
### 1. AI功能萌芽币消费系统
|
||||||
|
|
||||||
|
**功能描述**:
|
||||||
|
- 用户每次调用AI模型应用(aimodelapp)需消耗100萌芽币
|
||||||
|
- 当用户萌芽币余额不足时,无法使用AI功能
|
||||||
|
- 记录用户的AI使用历史
|
||||||
|
|
||||||
|
**API端点**:
|
||||||
|
```
|
||||||
|
GET /api/aimodelapp/coins # 查询用户萌芽币余额和使用历史
|
||||||
|
```
|
||||||
|
|
||||||
|
**技术实现**:
|
||||||
|
- 使用装饰器模式实现请求前验证和扣除萌芽币
|
||||||
|
- 在MongoDB中记录用户AI使用历史
|
||||||
|
- 通过JWT Token验证用户身份
|
||||||
|
|
||||||
|
**业务逻辑**:
|
||||||
|
1. 当用户请求AI功能时,首先验证JWT Token
|
||||||
|
2. 检查用户萌芽币余额是否≥100
|
||||||
|
3. 如余额充足,先扣除萌芽币,然后再调用AI服务
|
||||||
|
4. 记录使用历史,包括API类型、时间和消费萌芽币数量
|
||||||
|
5. 返回AI服务结果给用户
|
||||||
|
|
||||||
|
**响应示例(查询萌芽币余额)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"coins": 200,
|
||||||
|
"ai_cost": 100,
|
||||||
|
"can_use_ai": true,
|
||||||
|
"username": "用户名",
|
||||||
|
"usage_count": 1,
|
||||||
|
"recent_usage": [
|
||||||
|
{
|
||||||
|
"api_type": "chat",
|
||||||
|
"cost": 100,
|
||||||
|
"timestamp": "2025-09-16T11:15:47.285720"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": "当前萌芽币余额: 200"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**前端开发注意事项**:
|
||||||
|
- 每个需要调用AI功能的页面应首先检查用户萌芽币余额
|
||||||
|
- 当萌芽币不足时,向用户提示并引导用户通过签到等方式获取萌芽币
|
||||||
|
- 可在UI中展示用户最近的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"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
InfoGenie-frontend/.env.development
Normal file
11
InfoGenie-frontend/.env.development
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# React 开发环境变量
|
||||||
|
|
||||||
|
# API URL - 开发环境使用本地后端
|
||||||
|
REACT_APP_API_URL=http://127.0.0.1:5002
|
||||||
|
|
||||||
|
# 应用信息
|
||||||
|
REACT_APP_NAME=InfoGenie
|
||||||
|
REACT_APP_VERSION=1.0.0
|
||||||
|
|
||||||
|
# 调试模式
|
||||||
|
REACT_APP_DEBUG=true
|
||||||
13
InfoGenie-frontend/.env.production
Normal file
13
InfoGenie-frontend/.env.production
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# React 构建时环境变量
|
||||||
|
# 用于 Docker 构建
|
||||||
|
|
||||||
|
# API URL - 在 Docker 环境下,前端和后端在同一个容器
|
||||||
|
# 使用相对路径,这样前端会自动使用当前域名
|
||||||
|
REACT_APP_API_URL=
|
||||||
|
|
||||||
|
# 应用信息
|
||||||
|
REACT_APP_NAME=InfoGenie
|
||||||
|
REACT_APP_VERSION=1.0.0
|
||||||
|
|
||||||
|
# 调试模式(生产环境关闭)
|
||||||
|
REACT_APP_DEBUG=false
|
||||||
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
|
||||||
54
frontend/react-app/package-lock.json → InfoGenie-frontend/package-lock.json
generated
Normal file → Executable file
54
frontend/react-app/package-lock.json → InfoGenie-frontend/package-lock.json
generated
Normal file → Executable file
@@ -19,6 +19,7 @@
|
|||||||
"react-icons": "^4.11.0",
|
"react-icons": "^4.11.0",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"react-transition-group": "^4.4.5",
|
||||||
"styled-components": "^6.0.7",
|
"styled-components": "^6.0.7",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
}
|
}
|
||||||
@@ -82,6 +83,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
|
||||||
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
@@ -731,6 +733,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz",
|
||||||
"integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==",
|
"integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
@@ -1614,6 +1617,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz",
|
||||||
"integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==",
|
"integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-annotate-as-pure": "^7.27.1",
|
"@babel/helper-annotate-as-pure": "^7.27.1",
|
||||||
"@babel/helper-module-imports": "^7.27.1",
|
"@babel/helper-module-imports": "^7.27.1",
|
||||||
@@ -4678,8 +4682,7 @@
|
|||||||
"version": "15.7.15",
|
"version": "15.7.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/q": {
|
"node_modules/@types/q": {
|
||||||
"version": "1.5.8",
|
"version": "1.5.8",
|
||||||
@@ -4835,6 +4838,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
||||||
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
|
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.4.0",
|
"@eslint-community/regexpp": "^4.4.0",
|
||||||
"@typescript-eslint/scope-manager": "5.62.0",
|
"@typescript-eslint/scope-manager": "5.62.0",
|
||||||
@@ -4888,6 +4892,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
|
||||||
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
|
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "5.62.0",
|
"@typescript-eslint/scope-manager": "5.62.0",
|
||||||
"@typescript-eslint/types": "5.62.0",
|
"@typescript-eslint/types": "5.62.0",
|
||||||
@@ -5257,6 +5262,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -5355,6 +5361,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -6320,6 +6327,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001737",
|
"caniuse-lite": "^1.0.30001737",
|
||||||
"electron-to-chromium": "^1.5.211",
|
"electron-to-chromium": "^1.5.211",
|
||||||
@@ -7386,7 +7394,8 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
@@ -7746,6 +7755,16 @@
|
|||||||
"utila": "~0.4"
|
"utila": "~0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-helpers": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.8.7",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dom-serializer": {
|
"node_modules/dom-serializer": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||||
@@ -8242,6 +8261,7 @@
|
|||||||
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
||||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
@@ -11076,6 +11096,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
|
||||||
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
|
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "^27.5.1",
|
"@jest/core": "^27.5.1",
|
||||||
"import-local": "^3.0.2",
|
"import-local": "^3.0.2",
|
||||||
@@ -15147,6 +15168,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -16334,6 +16356,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
"util-deprecate": "^1.0.2"
|
"util-deprecate": "^1.0.2"
|
||||||
@@ -16699,6 +16722,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -16849,6 +16873,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -16900,6 +16925,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||||
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==",
|
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -17009,6 +17035,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-transition-group": {
|
||||||
|
"version": "4.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"dom-helpers": "^5.0.1",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.6.0",
|
||||||
|
"react-dom": ">=16.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -17402,6 +17444,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
||||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
},
|
},
|
||||||
@@ -17647,6 +17690,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -19352,6 +19396,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||||
"license": "(MIT OR CC0-1.0)",
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@@ -19781,6 +19826,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
|
||||||
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
|
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.7",
|
"@types/eslint-scope": "^3.7.7",
|
||||||
"@types/estree": "^1.0.8",
|
"@types/estree": "^1.0.8",
|
||||||
@@ -19852,6 +19898,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz",
|
||||||
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
|
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/bonjour": "^3.5.9",
|
"@types/bonjour": "^3.5.9",
|
||||||
"@types/connect-history-api-fallback": "^1.3.5",
|
"@types/connect-history-api-fallback": "^1.3.5",
|
||||||
@@ -20264,6 +20311,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
22
frontend/react-app/package.json → InfoGenie-frontend/package.json
Normal file → Executable file
22
frontend/react-app/package.json → InfoGenie-frontend/package.json
Normal file → Executable file
@@ -1,23 +1,30 @@
|
|||||||
{
|
{
|
||||||
"name": "infogenie-frontend",
|
"name": "infogenie-frontend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "✨ 神奇万事通 - 前端React应用",
|
"description": "✨ 万象口袋 - 前端React应用",
|
||||||
"keywords": ["react", "api", "mobile-first", "responsive"],
|
"keywords": [
|
||||||
"author": "神奇万事通",
|
"react",
|
||||||
|
"api",
|
||||||
|
"mobile-first",
|
||||||
|
"responsive"
|
||||||
|
],
|
||||||
|
"author": "万象口袋",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"homepage": "/",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
|
"axios": "^1.5.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hot-toast": "^2.4.1",
|
||||||
|
"react-icons": "^4.11.0",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"axios": "^1.5.0",
|
"react-transition-group": "^4.4.5",
|
||||||
"styled-components": "^6.0.7",
|
"styled-components": "^6.0.7",
|
||||||
"react-icons": "^4.11.0",
|
|
||||||
"react-hot-toast": "^2.4.1",
|
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -44,6 +51,5 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari 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 charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>随机JavaScript趣味题</title>
|
<title>随机JavaScript趣味题</title>
|
||||||
|
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
||||||
|
<link rel="dns-prefetch" href="https://60s.api.shumengya.top">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="stylesheet" href="css/background.css">
|
<link rel="stylesheet" href="css/background.css">
|
||||||
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||||||
<script src="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
<script src="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
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 {
|
class JSQuizApp {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.apiEndpoints = [
|
this.apiEndpoints = [
|
||||||
'https://60s-cf.viki.moe',
|
'https://60s.api.shumengya.top',
|
||||||
'https://60s.viki.moe',
|
|
||||||
'https://60s.b23.run',
|
|
||||||
'https://60s.114128.xyz',
|
|
||||||
'https://60s-cf.114128.xyz'
|
|
||||||
];
|
];
|
||||||
this.currentApiIndex = 0;
|
this.currentApiIndex = 0;
|
||||||
this.currentQuestion = null;
|
this.currentQuestion = null;
|
||||||
this.selectedOption = null;
|
this.selectedOption = null;
|
||||||
this.isAnswered = false;
|
this.isAnswered = false;
|
||||||
|
this.loadStartTime = null;
|
||||||
|
|
||||||
this.initElements();
|
this.initElements();
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
|
this.preloadResources();
|
||||||
this.loadQuestion();
|
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() {
|
bindEvents() {
|
||||||
this.elements.submitBtn.addEventListener('click', () => this.submitAnswer());
|
this.elements.submitBtn.addEventListener('click', () => this.submitAnswer());
|
||||||
@@ -84,6 +93,7 @@ class JSQuizApp {
|
|||||||
|
|
||||||
// 加载题目
|
// 加载题目
|
||||||
async loadQuestion() {
|
async loadQuestion() {
|
||||||
|
this.loadStartTime = Date.now();
|
||||||
this.showLoading();
|
this.showLoading();
|
||||||
this.resetQuestion();
|
this.resetQuestion();
|
||||||
|
|
||||||
@@ -92,15 +102,20 @@ class JSQuizApp {
|
|||||||
|
|
||||||
while (attempts < maxAttempts) {
|
while (attempts < maxAttempts) {
|
||||||
try {
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||||
|
|
||||||
const response = await fetch(this.getCurrentApiUrl(), {
|
const response = await fetch(this.getCurrentApiUrl(), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
timeout: 10000
|
signal: controller.signal
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
@@ -109,6 +124,8 @@ class JSQuizApp {
|
|||||||
|
|
||||||
if (data.code === 200 && data.data) {
|
if (data.code === 200 && data.data) {
|
||||||
this.currentQuestion = data.data;
|
this.currentQuestion = data.data;
|
||||||
|
const loadTime = Date.now() - this.loadStartTime;
|
||||||
|
console.log(`题目加载完成,耗时: ${loadTime}ms`);
|
||||||
this.displayQuestion();
|
this.displayQuestion();
|
||||||
return;
|
return;
|
||||||
} else {
|
} 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接口列表
|
// 加载API接口列表
|
||||||
async loadApiEndpoints() {
|
async loadApiEndpoints() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('./接口集合.json');
|
// 直接硬编码API端点,避免CORS问题
|
||||||
this.apiEndpoints = await response.json();
|
this.apiEndpoints = ["https://60s.api.shumengya.top"];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载API接口列表失败:', error);
|
console.error('加载API接口列表失败:', error);
|
||||||
this.showToast('加载接口配置失败', 'error');
|
this.showToast('加载接口配置失败', 'error');
|
||||||
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 吃疯狂星期四 然后再给我点杯奶茶 再给我两万块钱 懂吗你们"
|
||||||
|
}
|
||||||
|
}
|
||||||
52
frontend/60sapi/娱乐消遣/随机一言/css/background.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/css/background.css
Normal file → Executable file
52
frontend/60sapi/娱乐消遣/随机一言/css/background.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/css/background.css
Normal file → Executable file
@@ -4,11 +4,11 @@
|
|||||||
body {
|
body {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
#fff8dc 0%,
|
#f1f8e9 0%,
|
||||||
#ffeaa7 25%,
|
#dcedc8 25%,
|
||||||
#fdcb6e 50%,
|
#c8e6c8 50%,
|
||||||
#e17055 75%,
|
#a5d6a7 75%,
|
||||||
#d63031 100%
|
#81c784 100%
|
||||||
);
|
);
|
||||||
background-size: 400% 400%;
|
background-size: 400% 400%;
|
||||||
animation: gradientShift 15s ease infinite;
|
animation: gradientShift 15s ease infinite;
|
||||||
@@ -24,9 +24,9 @@ body::before {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 20% 80%, rgba(255, 215, 0, 0.1) 0%, transparent 50%),
|
radial-gradient(circle at 20% 80%, rgba(129, 199, 132, 0.1) 0%, transparent 50%),
|
||||||
radial-gradient(circle at 80% 20%, rgba(255, 223, 0, 0.1) 0%, transparent 50%),
|
radial-gradient(circle at 80% 20%, rgba(165, 214, 167, 0.1) 0%, transparent 50%),
|
||||||
radial-gradient(circle at 40% 40%, rgba(212, 175, 55, 0.05) 0%, transparent 50%);
|
radial-gradient(circle at 40% 40%, rgba(102, 187, 106, 0.05) 0%, transparent 50%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
@@ -40,11 +40,11 @@ body::after {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(2px 2px at 20px 30px, rgba(255, 215, 0, 0.3), transparent),
|
radial-gradient(2px 2px at 20px 30px, rgba(129, 199, 132, 0.3), transparent),
|
||||||
radial-gradient(2px 2px at 40px 70px, rgba(255, 223, 0, 0.2), transparent),
|
radial-gradient(2px 2px at 40px 70px, rgba(165, 214, 167, 0.2), transparent),
|
||||||
radial-gradient(1px 1px at 90px 40px, rgba(212, 175, 55, 0.4), transparent),
|
radial-gradient(1px 1px at 90px 40px, rgba(102, 187, 106, 0.4), transparent),
|
||||||
radial-gradient(1px 1px at 130px 80px, rgba(255, 215, 0, 0.2), transparent),
|
radial-gradient(1px 1px at 130px 80px, rgba(129, 199, 132, 0.2), transparent),
|
||||||
radial-gradient(2px 2px at 160px 30px, rgba(255, 223, 0, 0.3), transparent);
|
radial-gradient(2px 2px at 160px 30px, rgba(165, 214, 167, 0.3), transparent);
|
||||||
background-repeat: repeat;
|
background-repeat: repeat;
|
||||||
background-size: 200px 100px;
|
background-size: 200px 100px;
|
||||||
animation: sparkle 20s linear infinite;
|
animation: sparkle 20s linear infinite;
|
||||||
@@ -106,8 +106,8 @@ body::after {
|
|||||||
|
|
||||||
body::before {
|
body::before {
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 30% 70%, rgba(255, 215, 0, 0.08) 0%, transparent 40%),
|
radial-gradient(circle at 30% 70%, rgba(129, 199, 132, 0.08) 0%, transparent 40%),
|
||||||
radial-gradient(circle at 70% 30%, rgba(255, 223, 0, 0.08) 0%, transparent 40%);
|
radial-gradient(circle at 70% 30%, rgba(165, 214, 167, 0.08) 0%, transparent 40%);
|
||||||
}
|
}
|
||||||
|
|
||||||
body::after {
|
body::after {
|
||||||
@@ -121,9 +121,9 @@ body::after {
|
|||||||
body {
|
body {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
#fff8dc 0%,
|
#f1f8e9 0%,
|
||||||
#ffeaa7 50%,
|
#dcedc8 50%,
|
||||||
#fdcb6e 100%
|
#c8e6c8 100%
|
||||||
);
|
);
|
||||||
background-size: 150% 150%;
|
background-size: 150% 150%;
|
||||||
}
|
}
|
||||||
@@ -138,18 +138,18 @@ body::after {
|
|||||||
body {
|
body {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
#2c1810 0%,
|
#1b2e1b 0%,
|
||||||
#3d2914 25%,
|
#2e4a2e 25%,
|
||||||
#4a3319 50%,
|
#3e5e3e 50%,
|
||||||
#5c3e1f 75%,
|
#4e6e4e 75%,
|
||||||
#6b4423 100%
|
#5e7e5e 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
body::before {
|
body::before {
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 20% 80%, rgba(255, 215, 0, 0.05) 0%, transparent 50%),
|
radial-gradient(circle at 20% 80%, rgba(129, 199, 132, 0.05) 0%, transparent 50%),
|
||||||
radial-gradient(circle at 80% 20%, rgba(255, 223, 0, 0.05) 0%, transparent 50%);
|
radial-gradient(circle at 80% 20%, rgba(165, 214, 167, 0.05) 0%, transparent 50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +162,6 @@ body::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: linear-gradient(135deg, #fff8dc 0%, #ffeaa7 50%, #fdcb6e 100%);
|
background: linear-gradient(135deg, #f1f8e9 0%, #dcedc8 50%, #c8e6c8 100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
58
frontend/60sapi/娱乐消遣/随机一言/css/style.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/css/style.css
Normal file → Executable file
58
frontend/60sapi/娱乐消遣/随机一言/css/style.css → InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言/css/style.css
Normal file → Executable file
@@ -8,7 +8,7 @@
|
|||||||
body {
|
body {
|
||||||
font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #2c1810;
|
color: #2e7d32;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,20 +33,20 @@ body {
|
|||||||
.title {
|
.title {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #d4af37;
|
color: #2e7d32;
|
||||||
text-shadow:
|
text-shadow:
|
||||||
0 0 10px rgba(212, 175, 55, 0.8),
|
0 0 10px rgba(129, 199, 132, 0.8),
|
||||||
0 0 20px rgba(212, 175, 55, 0.6),
|
0 0 20px rgba(129, 199, 132, 0.6),
|
||||||
0 0 30px rgba(212, 175, 55, 0.4);
|
0 0 30px rgba(129, 199, 132, 0.4);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
animation: titleGlow 3s ease-in-out infinite alternate;
|
animation: titleGlow 3s ease-in-out infinite alternate;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
color: #b8860b;
|
color: #388e3c;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
text-shadow: 0 0 5px rgba(184, 134, 11, 0.5);
|
text-shadow: 0 0 5px rgba(102, 187, 106, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 主内容区域 */
|
/* 主内容区域 */
|
||||||
@@ -58,14 +58,14 @@ body {
|
|||||||
|
|
||||||
/* 一言容器 */
|
/* 一言容器 */
|
||||||
.quote-container {
|
.quote-container {
|
||||||
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1), rgba(255, 223, 0, 0.05));
|
background: linear-gradient(135deg, rgba(129, 199, 132, 0.1), rgba(165, 214, 167, 0.05));
|
||||||
border: 2px solid rgba(212, 175, 55, 0.3);
|
border: 2px solid rgba(102, 187, 106, 0.3);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 32px rgba(212, 175, 55, 0.2),
|
0 8px 32px rgba(102, 187, 106, 0.2),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -78,7 +78,7 @@ body {
|
|||||||
left: -2px;
|
left: -2px;
|
||||||
right: -2px;
|
right: -2px;
|
||||||
bottom: -2px;
|
bottom: -2px;
|
||||||
background: linear-gradient(45deg, #ffd700, #ffed4e, #ffd700, #ffed4e);
|
background: linear-gradient(45deg, #81c784, #a5d6a7, #81c784, #a5d6a7);
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
animation: borderGlow 4s linear infinite;
|
animation: borderGlow 4s linear infinite;
|
||||||
@@ -88,7 +88,7 @@ body {
|
|||||||
.loading {
|
.loading {
|
||||||
display: none;
|
display: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #d4af37;
|
color: #2e7d32;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading.show {
|
.loading.show {
|
||||||
@@ -98,8 +98,8 @@ body {
|
|||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border: 4px solid rgba(212, 175, 55, 0.3);
|
border: 4px solid rgba(102, 187, 106, 0.3);
|
||||||
border-top: 4px solid #d4af37;
|
border-top: 4px solid #2e7d32;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin: 0 auto 15px;
|
margin: 0 auto 15px;
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
@@ -118,15 +118,15 @@ body {
|
|||||||
.quote-text {
|
.quote-text {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
color: #2c1810;
|
color: #2e7d32;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
text-shadow: 0 1px 2px rgba(212, 175, 55, 0.1);
|
text-shadow: 0 1px 2px rgba(102, 187, 106, 0.1);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote-index {
|
.quote-index {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: #b8860b;
|
color: #388e3c;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ body {
|
|||||||
.error-message {
|
.error-message {
|
||||||
display: none;
|
display: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #cd853f;
|
color: #66bb6a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message.show {
|
.error-message.show {
|
||||||
@@ -157,20 +157,20 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.refresh-btn {
|
.refresh-btn {
|
||||||
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
background: linear-gradient(135deg, #81c784, #a5d6a7);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
padding: 15px 30px;
|
padding: 15px 30px;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #2c1810;
|
color: #2e7d32;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 4px 15px rgba(212, 175, 55, 0.3),
|
0 4px 15px rgba(102, 187, 106, 0.3),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -179,7 +179,7 @@ body {
|
|||||||
.refresh-btn:hover {
|
.refresh-btn:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 6px 20px rgba(212, 175, 55, 0.4),
|
0 6px 20px rgba(102, 187, 106, 0.4),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ body {
|
|||||||
.footer {
|
.footer {
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #b8860b;
|
color: #388e3c;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
@@ -215,15 +215,15 @@ body {
|
|||||||
@keyframes titleGlow {
|
@keyframes titleGlow {
|
||||||
0% {
|
0% {
|
||||||
text-shadow:
|
text-shadow:
|
||||||
0 0 10px rgba(212, 175, 55, 0.8),
|
0 0 10px rgba(129, 199, 132, 0.8),
|
||||||
0 0 20px rgba(212, 175, 55, 0.6),
|
0 0 20px rgba(129, 199, 132, 0.6),
|
||||||
0 0 30px rgba(212, 175, 55, 0.4);
|
0 0 30px rgba(129, 199, 132, 0.4);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
text-shadow:
|
text-shadow:
|
||||||
0 0 15px rgba(212, 175, 55, 1),
|
0 0 15px rgba(129, 199, 132, 1),
|
||||||
0 0 25px rgba(212, 175, 55, 0.8),
|
0 0 25px rgba(129, 199, 132, 0.8),
|
||||||
0 0 35px rgba(212, 175, 55, 0.6);
|
0 0 35px rgba(129, 199, 132, 0.6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
constructor() {
|
||||||
// API接口列表
|
// API接口列表
|
||||||
this.apiEndpoints = [
|
this.apiEndpoints = [
|
||||||
"https://60s-cf.viki.moe",
|
"https://60s.api.shumengya.top"
|
||||||
"https://60s.viki.moe",
|
|
||||||
"https://60s.b23.run",
|
|
||||||
"https://60s.114128.xyz",
|
|
||||||
"https://60s-cf.114128.xyz"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
this.currentEndpointIndex = 0;
|
this.currentEndpointIndex = 0;
|
||||||
@@ -117,7 +113,8 @@ class HitokotoApp {
|
|||||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
|
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${endpoint}/v2/hitokoto?encoding=text`, {
|
// 移除URL中的encoding=text参数,确保返回JSON格式
|
||||||
|
const response = await fetch(`${endpoint}/v2/hitokoto`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'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,
|
"code": 200,
|
||||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||||
"data": {
|
"data": {
|
||||||
"index": 2862,
|
"index": 121,
|
||||||
"hitokoto": "你带上罪恶之冠,即使背负上所有罪恶和孤独,绝不让你受伤"
|
"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`);
|
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/changya`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 如果无法加载接口集合,使用默认接口
|
// 如果无法加载接口集合,使用默认接口
|
||||||
this.endpoints = ['https://60s.viki.moe/v2/changya'];
|
this.endpoints = ['https://60s.api.shumengya.top/v2/changya'];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 获取当前接口URL
|
// 获取当前接口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 - FRESH GREEN VERSION */
|
||||||
|
body.theme-comic {
|
||||||
|
background: linear-gradient(-45deg, #c8e6c9, #dcedc8, #f1f8e9, #e8f5e8);
|
||||||
|
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;
|
||||||
|
}
|
||||||
200
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/css/style.css
Executable file
200
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/css/style.css
Executable file
@@ -0,0 +1,200 @@
|
|||||||
|
@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: #66bb6a;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Comic Theme Styles --- */
|
||||||
|
.theme-comic header h1 {
|
||||||
|
font-family: 'Zhi Mang Xing', cursive;
|
||||||
|
font-size: 4em;
|
||||||
|
color: #2e7d32; /* Fresh Green */
|
||||||
|
text-shadow: 2px 2px 0 #fff;
|
||||||
|
margin: 0.2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-comic .divider {
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, #81c784, #a5d6a7, #c8e6c9, #66bb6a);
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 20px auto;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-comic .joke-card {
|
||||||
|
background: rgba(248, 255, 248, 0.9); /* Light green tinted white */
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 40px;
|
||||||
|
min-height: 200px;
|
||||||
|
box-shadow: 0 8px 25px rgba(102, 187, 106, 0.15);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
transform: rotate(-1deg);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
border: 1px solid rgba(129, 199, 132, 0.3);
|
||||||
|
}
|
||||||
|
.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: #1b5e20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-comic .new-joke-btn {
|
||||||
|
background: linear-gradient(135deg, #66bb6a, #81c784); /* Fresh Green Gradient */
|
||||||
|
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 #388e3c; /* Darker Green */
|
||||||
|
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: #a5d6a7;
|
||||||
|
border: 1px solid #66bb6a;
|
||||||
|
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; }
|
||||||
|
}
|
||||||
57
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/index.html
Executable file
57
InfoGenie-frontend/public/60sapi/娱乐消遣/随机搞笑段子/index.html
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
<!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>
|
||||||
|
|
||||||
|
<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": "我不想读书,主要是因为家里牛啊,猪啊羊啊都没人喂。"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
/* 随机唱歌音频 - 淡绿色清新风格样式 */
|
/* 随机答案之书 - 淡绿色清新风格样式(与随机唱歌音频一致) */
|
||||||
|
|
||||||
/* 重置样式 */
|
/* 重置样式 */
|
||||||
* {
|
* {
|
||||||
@@ -49,97 +49,7 @@ body {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 用户卡片 */
|
/* 按钮 */
|
||||||
.user-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 15px;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
|
|
||||||
margin-bottom: 15px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
border: 3px solid rgba(129, 199, 132, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nickname {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: #2d5016;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
color: #5a7c65;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 歌曲信息 */
|
|
||||||
.song-card {
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
|
|
||||||
margin-bottom: 15px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-title {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #1b5e20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-meta {
|
|
||||||
color: #5a7c65;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 歌词 */
|
|
||||||
.lyrics {
|
|
||||||
background: rgba(129, 199, 132, 0.1);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 12px;
|
|
||||||
max-height: 220px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyrics p {
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 音频播放器卡片 */
|
|
||||||
.audio-card {
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
|
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
|
||||||
color: white;
|
color: white;
|
||||||
@@ -159,11 +69,6 @@ body {
|
|||||||
box-shadow: 0 6px 18px rgba(129, 199, 132, 0.45);
|
box-shadow: 0 6px 18px rgba(129, 199, 132, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
|
||||||
color: #5a7c65;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 加载与错误 */
|
/* 加载与错误 */
|
||||||
.loading, .error {
|
.loading, .error {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -198,10 +103,41 @@ body {
|
|||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 平板端适配 */
|
/* 答案卡片 */
|
||||||
@media (max-width: 1024px) and (min-width: 768px) {
|
.answer-card {
|
||||||
.container { padding: 16px; }
|
background: rgba(255, 255, 255, 0.9);
|
||||||
.header h1 { font-size: 1.8rem; }
|
padding: 16px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-text {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1b5e20;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-en {
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
color: #5a7c65;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 手机端优先优化 */
|
/* 手机端优先优化 */
|
||||||
@@ -210,42 +146,18 @@ body {
|
|||||||
.header { padding: 18px; }
|
.header { padding: 18px; }
|
||||||
.header h1 { font-size: 1.6rem; gap: 8px; }
|
.header h1 { font-size: 1.6rem; gap: 8px; }
|
||||||
|
|
||||||
.user-card {
|
.answer-card { padding: 16px; }
|
||||||
padding: 16px;
|
.answer-text { font-size: 1.3rem; }
|
||||||
flex-direction: column;
|
.answer-en { font-size: 0.95rem; }
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
.actions {
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-card, .audio-card {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyrics {
|
|
||||||
max-height: 180px;
|
|
||||||
text-align: left;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-actions {
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 200px;
|
max-width: 220px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
45
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/index.html
Normal file
45
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/index.html
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<title>📘真理之道</title>
|
||||||
|
<meta name="description" content="当你踌躇不定,犹豫不决时,不妨来这里看看吧。" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="./css/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>📘真理之道</h1>
|
||||||
|
<p>当你踌躇不定,犹豫不决时,不妨来这里看看吧</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 加载与错误状态 -->
|
||||||
|
<section id="loading" class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>正在加载中,请稍候…</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="error" class="error" style="display: none;">
|
||||||
|
<p>获取数据失败,请稍后重试</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<main id="content" style="display: none;" class="fade-in">
|
||||||
|
<div class="answer-card">
|
||||||
|
<div class="answer-text" id="answer">-</div>
|
||||||
|
<div class="answer-en" id="answer-en" style="display: none;">-</div>
|
||||||
|
<div class="meta">编号:<span id="index">-</span></div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn" id="refresh-btn">换一个</button>
|
||||||
|
<button class="btn" id="copy-btn">复制</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="./js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// 随机唱歌音频 页面脚本
|
// 随机答案之书 页面脚本
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
try {
|
try {
|
||||||
const res = await fetch('./接口集合.json');
|
const res = await fetch('./接口集合.json');
|
||||||
const endpoints = await res.json();
|
const endpoints = await res.json();
|
||||||
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/changya`);
|
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/answer`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 如果无法加载接口集合,使用默认接口
|
// 如果无法加载接口集合,使用默认接口
|
||||||
this.endpoints = ['https://60s.viki.moe/v2/changya'];
|
this.endpoints = ['https://60s.api.shumengya.top/v2/answer'];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 获取当前接口URL
|
// 获取当前接口URL
|
||||||
@@ -43,17 +43,11 @@
|
|||||||
loading: null,
|
loading: null,
|
||||||
error: null,
|
error: null,
|
||||||
container: null,
|
container: null,
|
||||||
avatar: null,
|
answer: null,
|
||||||
nickname: null,
|
answerEn: null,
|
||||||
gender: null,
|
indexEl: null,
|
||||||
songTitle: null,
|
|
||||||
songMeta: null,
|
|
||||||
lyrics: null,
|
|
||||||
audio: null,
|
|
||||||
likeCount: null,
|
|
||||||
publishTime: null,
|
|
||||||
link: null,
|
|
||||||
refreshBtn: null,
|
refreshBtn: null,
|
||||||
|
copyBtn: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function initDom() {
|
function initDom() {
|
||||||
@@ -61,18 +55,11 @@
|
|||||||
els.error = document.getElementById('error');
|
els.error = document.getElementById('error');
|
||||||
els.container = document.getElementById('content');
|
els.container = document.getElementById('content');
|
||||||
|
|
||||||
els.avatar = document.getElementById('avatar');
|
els.answer = document.getElementById('answer');
|
||||||
els.nickname = document.getElementById('nickname');
|
els.answerEn = document.getElementById('answer-en');
|
||||||
els.gender = document.getElementById('gender');
|
els.indexEl = document.getElementById('index');
|
||||||
els.songTitle = document.getElementById('song-title');
|
|
||||||
els.songMeta = document.getElementById('song-meta');
|
|
||||||
els.lyrics = document.getElementById('lyrics');
|
|
||||||
|
|
||||||
els.audio = document.getElementById('audio');
|
|
||||||
els.likeCount = document.getElementById('like-count');
|
|
||||||
els.publishTime = document.getElementById('publish-time');
|
|
||||||
els.link = document.getElementById('link');
|
|
||||||
els.refreshBtn = document.getElementById('refresh-btn');
|
els.refreshBtn = document.getElementById('refresh-btn');
|
||||||
|
els.copyBtn = document.getElementById('copy-btn');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLoading() {
|
function showLoading() {
|
||||||
@@ -94,14 +81,6 @@
|
|||||||
els.container.style.display = 'block';
|
els.container.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDuration(ms) {
|
|
||||||
if (!ms && ms !== 0) return '';
|
|
||||||
const totalSeconds = Math.floor(ms / 1000);
|
|
||||||
const m = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
|
|
||||||
const s = (totalSeconds % 60).toString().padStart(2, '0');
|
|
||||||
return `${m}:${s}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeText(text) {
|
function safeText(text) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = text == null ? '' : String(text);
|
div.textContent = text == null ? '' : String(text);
|
||||||
@@ -123,7 +102,7 @@
|
|||||||
|
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
timeout: 10000 // 10秒超时
|
timeout: 10000 // 10秒超时(兼容同目录页面风格)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
@@ -169,46 +148,22 @@
|
|||||||
|
|
||||||
function render(data) {
|
function render(data) {
|
||||||
const d = data?.data || {};
|
const d = data?.data || {};
|
||||||
const user = d.user || {};
|
|
||||||
const song = d.song || {};
|
|
||||||
const audio = d.audio || {};
|
|
||||||
|
|
||||||
// 用户信息
|
const cn = d.answer || '';
|
||||||
els.avatar.src = user.avatar_url || '';
|
const en = d.answer_en || '';
|
||||||
els.avatar.alt = (user.nickname || '用户') + ' 头像';
|
const idx = d.index != null ? d.index : d.id != null ? d.id : '-';
|
||||||
els.nickname.textContent = user.nickname || '未知用户';
|
|
||||||
els.gender.textContent = user.gender === 'female' ? '女' : user.gender === 'male' ? '男' : '未知';
|
|
||||||
|
|
||||||
// 歌曲信息
|
els.answer.innerHTML = safeText(cn || '-');
|
||||||
els.songTitle.textContent = song.name || '未知歌曲';
|
|
||||||
els.songMeta.textContent = song.singer ? `演唱:${song.singer}` : '';
|
|
||||||
|
|
||||||
els.lyrics.innerHTML = '';
|
if (en) {
|
||||||
if (Array.isArray(song.lyrics)) {
|
els.answerEn.style.display = 'block';
|
||||||
const frag = document.createDocumentFragment();
|
els.answerEn.innerHTML = safeText(en);
|
||||||
song.lyrics.forEach(line => {
|
} else {
|
||||||
const p = document.createElement('p');
|
els.answerEn.style.display = 'none';
|
||||||
p.innerHTML = safeText(line);
|
els.answerEn.innerHTML = '';
|
||||||
frag.appendChild(p);
|
|
||||||
});
|
|
||||||
els.lyrics.appendChild(frag);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 音频
|
els.indexEl.textContent = idx;
|
||||||
els.audio.src = audio.url || '';
|
|
||||||
els.audio.preload = 'none';
|
|
||||||
|
|
||||||
// 其他信息
|
|
||||||
els.likeCount.textContent = typeof audio.like_count === 'number' ? audio.like_count : '-';
|
|
||||||
const publish = audio.publish || (audio.publish_at ? new Date(audio.publish_at).toLocaleString() : '');
|
|
||||||
els.publishTime.textContent = publish;
|
|
||||||
els.link.href = audio.link || '#';
|
|
||||||
els.link.target = '_blank';
|
|
||||||
|
|
||||||
// 时长信息
|
|
||||||
const durationEl = document.getElementById('duration');
|
|
||||||
durationEl.textContent = formatDuration(audio.duration);
|
|
||||||
|
|
||||||
showContent();
|
showContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +196,24 @@
|
|||||||
if (els.refreshBtn) {
|
if (els.refreshBtn) {
|
||||||
els.refreshBtn.addEventListener('click', load);
|
els.refreshBtn.addEventListener('click', load);
|
||||||
}
|
}
|
||||||
// 快捷键 Ctrl+R 刷新(不拦截浏览器默认刷新)
|
if (els.copyBtn) {
|
||||||
|
els.copyBtn.addEventListener('click', async () => {
|
||||||
|
const textParts = [];
|
||||||
|
const cn = els.answer?.textContent?.trim();
|
||||||
|
const en = els.answerEn?.textContent?.trim();
|
||||||
|
if (cn) textParts.push(cn);
|
||||||
|
if (en) textParts.push(en);
|
||||||
|
const finalText = textParts.join('\n');
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(finalText);
|
||||||
|
const old = els.copyBtn.textContent;
|
||||||
|
els.copyBtn.textContent = '已复制';
|
||||||
|
setTimeout(() => { els.copyBtn.textContent = old; }, 1200);
|
||||||
|
} catch (e) {
|
||||||
|
alert('复制失败,请手动选择文本复制');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
3
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/接口集合.json
Normal file
3
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/接口集合.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"https://60s.api.shumengya.top"
|
||||||
|
]
|
||||||
10
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/返回接口.json
Normal file
10
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/返回接口.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||||
|
"data": {
|
||||||
|
"id": "63",
|
||||||
|
"answer": "那不值得纠结",
|
||||||
|
"answer_en": "It's not worth worrying about",
|
||||||
|
"index": 62
|
||||||
|
}
|
||||||
|
}
|
||||||
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, #f1f8e9, #e8f5e8, #c8e6c9, #dcedc8);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradientBG 20s ease infinite;
|
||||||
|
color: #2e7d32;
|
||||||
|
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: #2e7d32;
|
||||||
|
text-shadow: 0 0 10px #81c784, 0 0 20px #a5d6a7;
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #388e3c;
|
||||||
|
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, 230, 201, 0.3));
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 0 30px #81c784, 0 0 60px #66bb6a, inset 0 0 20px rgba(220, 255, 220, 0.3);
|
||||||
|
animation: float 6s ease-in-out infinite;
|
||||||
|
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(200, 230, 201, 0.2), rgba(129, 199, 132, 0.3));
|
||||||
|
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(248, 255, 248, 0.8);
|
||||||
|
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(129, 199, 132, 0.3);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(102, 187, 106, 0.2);
|
||||||
|
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: #2e7d32;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#luck-tip {
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: #388e3c;
|
||||||
|
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: #66bb6a;
|
||||||
|
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: #2e7d32;
|
||||||
|
text-shadow: 0 0 8px #81c784;
|
||||||
|
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, #66bb6a, #388e3c);
|
||||||
|
border: 2px solid #81c784;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 3em;
|
||||||
|
color: #c8e6c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tarot-card-back::after {
|
||||||
|
content: '✧'; /* A simple star symbol */
|
||||||
|
text-shadow: 0 0 10px #e8f5e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tarot-card-front {
|
||||||
|
background: linear-gradient(135deg, #4caf50, #66bb6a);
|
||||||
|
border: 2px solid #81c784;
|
||||||
|
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: #e8f5e8;
|
||||||
|
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(129, 199, 132, 0.5);
|
||||||
|
text-shadow: 0 0 10px rgba(200, 230, 201, 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, #66bb6a, #4caf50);
|
||||||
|
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 #81c784;
|
||||||
|
}
|
||||||
|
|
||||||
|
#get-fortune-btn:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 0 25px #a5d6a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
#get-fortune-btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
border: 4px solid rgba(129, 199, 132, 0.3);
|
||||||
|
border-left-color: #66bb6a;
|
||||||
|
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(46, 125, 50, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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
@@ -13,7 +13,7 @@
|
|||||||
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/epic`);
|
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/epic`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 如果无法加载接口集合,使用默认接口
|
// 如果无法加载接口集合,使用默认接口
|
||||||
this.endpoints = ['https://60s-api.viki.moe/v2/epic'];
|
this.endpoints = ['https://60s.api.shumengya.top/v2/epic'];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 获取当前接口URL
|
// 获取当前接口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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user