update: 2026-03-28 20:59
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -20,8 +20,14 @@ __pycache__/
|
|||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
|
# Backend (Go)
|
||||||
|
infogenie-backend-go/vendor/
|
||||||
|
infogenie-backend-go/*.exe
|
||||||
|
infogenie-backend-go/server
|
||||||
|
infogenie-backend-go/tmp/
|
||||||
|
infogenie-backend-go/go.work*
|
||||||
|
|
||||||
# Env (do not commit secrets)
|
# Env (do not commit secrets)
|
||||||
InfoGenie-backend/.env
|
InfoGenie-backend/.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
|
|||||||
113
CLAUDE.md
Normal file
113
CLAUDE.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working in this repository.
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
This repo is a multi-app workspace with the React frontend as the main active surface:
|
||||||
|
|
||||||
|
- `infogenie-frontend/` — React 18 SPA built with Create React App. This is the primary app.
|
||||||
|
- `infogenie-backend-java/` — Spring Boot 3.5 / Java 17 backend skeleton.
|
||||||
|
- `InfoGenie-go-backend/` — legacy/alternate backend directory; currently only contains a README.
|
||||||
|
|
||||||
|
The frontend mixes two layers:
|
||||||
|
|
||||||
|
- A React shell for navigation, auth, shared layout, and embedded views.
|
||||||
|
- A large set of static HTML/JS mini-apps under `public/` and the corresponding built copies under `build/`.
|
||||||
|
|
||||||
|
## Frontend architecture
|
||||||
|
|
||||||
|
Key React entry points:
|
||||||
|
|
||||||
|
- `src/index.js` bootstraps the app and explicitly unregisters service workers / clears caches to avoid stale assets.
|
||||||
|
- `src/App.js` owns the top-level layout, router, global toast container, particle effect, and auth provider.
|
||||||
|
- `src/contexts/UserContext.js` is the session source of truth. It hydrates from `localStorage`, calls the auth service, and exposes login/logout helpers.
|
||||||
|
- `src/utils/api.js` is the shared Axios layer. It injects the bearer token, handles 401 redirects, and defines the separate auth-center client.
|
||||||
|
- `src/config/env.js` resolves runtime API endpoints from `REACT_APP_*` env vars or the current origin.
|
||||||
|
|
||||||
|
The app is route-driven, with major pages for:
|
||||||
|
|
||||||
|
- home entry / section selection
|
||||||
|
- login + auth callback
|
||||||
|
- 60s API browser and item detail routes
|
||||||
|
- small games
|
||||||
|
- toolbox
|
||||||
|
- AI app launcher
|
||||||
|
- user profile and admin
|
||||||
|
|
||||||
|
### Config-driven content
|
||||||
|
|
||||||
|
Most catalog-like UI is data-driven rather than hard-coded in pages:
|
||||||
|
|
||||||
|
- `src/config/Api60sConfig.js` defines the 60s API categories, toolbox catalogs, and static page path mappings.
|
||||||
|
- `src/config/StaticPageConfig.js` defines AI app and small-game launch metadata.
|
||||||
|
- `src/utils/site60sVisibility.js` fetches the backend site visibility list and filters the 60s API catalog before rendering.
|
||||||
|
|
||||||
|
This means many UI changes are done by editing config objects, not component logic.
|
||||||
|
|
||||||
|
### Static page embedding pattern
|
||||||
|
|
||||||
|
Several pages open a full-screen embedded static page instead of rendering everything in React:
|
||||||
|
|
||||||
|
- AI apps use `FullscreenEmbed` with token injection.
|
||||||
|
- Toolbox items also launch through `FullscreenEmbed`.
|
||||||
|
- 60s API detail pages map `itemId` to a static HTML page path.
|
||||||
|
|
||||||
|
When changing any mini-app, check both the React catalog entry and the matching file path under `public/`.
|
||||||
|
|
||||||
|
## Backend architecture
|
||||||
|
|
||||||
|
`infogenie-backend-java/` is currently a minimal Spring Boot bootstrap:
|
||||||
|
|
||||||
|
- `src/main/java/.../InfogenieBackendJavaApplication.java` contains only the application entry point.
|
||||||
|
- `src/test/java/.../InfogenieBackendJavaApplicationTests.java` contains the default context load test.
|
||||||
|
- `src/main/resources/application.yaml` only sets the application name right now.
|
||||||
|
|
||||||
|
There is no REST controller layer in this Java module yet.
|
||||||
|
|
||||||
|
## Common commands
|
||||||
|
|
||||||
|
### Frontend (`infogenie-frontend/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
npm run dev
|
||||||
|
npm run build
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Single test file or pattern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test -- --runInBand --watch=false src/path/to/test-file.test.js
|
||||||
|
npm test -- --runInBand --watch=false --testNamePattern="pattern"
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no separate lint script in `package.json`; the project relies on CRA's built-in ESLint checks during `start`, `test`, and `build`.
|
||||||
|
|
||||||
|
### Java backend (`infogenie-backend-java/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn test
|
||||||
|
mvn spring-boot:run
|
||||||
|
mvn -Dtest=InfogenieBackendJavaApplicationTests test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
Frontend runtime config comes from:
|
||||||
|
|
||||||
|
- `REACT_APP_API_URL`
|
||||||
|
- `REACT_APP_AUTH_URL`
|
||||||
|
- `REACT_APP_AUTH_API_URL`
|
||||||
|
- `REACT_APP_DEBUG`
|
||||||
|
|
||||||
|
If these are unset, the frontend falls back to the current origin for the main API and built-in auth defaults for auth URLs.
|
||||||
|
|
||||||
|
## Practical notes
|
||||||
|
|
||||||
|
- The frontend auth flow depends on `localStorage` keys such as `token`, `user`, and `expiresAt`.
|
||||||
|
- 401 responses from the shared Axios client clear the session and redirect to `/login`.
|
||||||
|
- The 60s API list is filtered by backend visibility data, so a missing/failed visibility fetch intentionally fails open and shows all items.
|
||||||
|
- The home page is just a launcher for the main product areas; the detailed behavior lives in the section pages and config files.
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
# Python cache
|
|
||||||
__pycache__
|
|
||||||
|
|
||||||
# Git
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# Test files
|
|
||||||
InfoGenie-backend/test
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# InfoGenie 环境变量配置示例
|
|
||||||
# 复制为 .env 并按需填写;不要把 .env 提交到仓库
|
|
||||||
|
|
||||||
# 邮件配置
|
|
||||||
MAIL_USERNAME=your_email@example.com
|
|
||||||
MAIL_PASSWORD=your_mail_password_or_app_token
|
|
||||||
|
|
||||||
# 数据库配置
|
|
||||||
MONGO_URI=mongodb://user:pass@127.0.0.1:27017/InfoGenie?authSource=admin
|
|
||||||
|
|
||||||
# 应用密钥
|
|
||||||
SECRET_KEY=change-me
|
|
||||||
|
|
||||||
# 环境配置
|
|
||||||
FLASK_ENV=development
|
|
||||||
|
|
||||||
3
InfoGenie-backend/.gitignore
vendored
3
InfoGenie-backend/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
#项目自忽略
|
|
||||||
.vscode
|
|
||||||
__pycache__
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# InfoGenie 后端 Docker 镜像
|
|
||||||
# 仅包含后端服务,使用 Gunicorn
|
|
||||||
|
|
||||||
FROM python:3.10-slim
|
|
||||||
|
|
||||||
# 安装 curl 用于健康检查
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# 设置工作目录
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 复制依赖文件
|
|
||||||
COPY requirements.txt .
|
|
||||||
|
|
||||||
# 安装 Python 依赖
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt gunicorn
|
|
||||||
|
|
||||||
# 复制后端代码
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# 创建持久化数据目录
|
|
||||||
RUN mkdir -p /app/data/logs
|
|
||||||
|
|
||||||
# 暴露端口
|
|
||||||
EXPOSE 2323
|
|
||||||
|
|
||||||
# 使用 Gunicorn 启动应用
|
|
||||||
CMD ["gunicorn", "--bind", "0.0.0.0:2323", "--workers", "4", "--threads", "2", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "app:app"]
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
InfoGenie 后端主应用入口
|
|
||||||
Created by: 万象口袋
|
|
||||||
Date: 2025-09-02
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import Flask, jsonify, request, send_from_directory
|
|
||||||
from flask_cors import CORS
|
|
||||||
from flask_pymongo import PyMongo
|
|
||||||
import os
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import hashlib
|
|
||||||
import secrets
|
|
||||||
|
|
||||||
# 导入模块
|
|
||||||
from modules.auth import auth_bp
|
|
||||||
from modules.user_management import user_bp
|
|
||||||
from modules.email_service import init_mail
|
|
||||||
from modules.aimodelapp import aimodelapp_bp
|
|
||||||
|
|
||||||
from config import Config
|
|
||||||
|
|
||||||
# 创建Flask应用
|
|
||||||
def create_app():
|
|
||||||
"""创建Flask应用实例"""
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
# 加载配置
|
|
||||||
app.config.from_object(Config)
|
|
||||||
|
|
||||||
# 启用CORS跨域支持(允许所有源)
|
|
||||||
CORS(app, supports_credentials=True)
|
|
||||||
|
|
||||||
# 初始化MongoDB
|
|
||||||
mongo = PyMongo(app)
|
|
||||||
app.mongo = mongo
|
|
||||||
|
|
||||||
# 初始化邮件服务
|
|
||||||
init_mail(app)
|
|
||||||
|
|
||||||
# 注册蓝图
|
|
||||||
app.register_blueprint(auth_bp, url_prefix='/api/auth')
|
|
||||||
app.register_blueprint(user_bp, url_prefix='/api/user')
|
|
||||||
app.register_blueprint(aimodelapp_bp, url_prefix='/api/aimodelapp')
|
|
||||||
|
|
||||||
# 基础路由
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
"""API根路径"""
|
|
||||||
return jsonify({
|
|
||||||
'message': '万象口袋 后端 API 服务运行中',
|
|
||||||
"description": "提供用户认证、用户管理、聚合API、小游戏接口和AI模型应用接口",
|
|
||||||
"email":"shumengya666@outlook.com",
|
|
||||||
'version': '2.2.0',
|
|
||||||
'timestamp': datetime.now().isoformat(),
|
|
||||||
'endpoints': {
|
|
||||||
'auth': '/api/auth',
|
|
||||||
'60s_api': '/api/60s',
|
|
||||||
'user': '/api/user',
|
|
||||||
'smallgame': '/api/smallgame',
|
|
||||||
'aimodelapp': '/api/aimodelapp'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.route('/api/health')
|
|
||||||
def health_check():
|
|
||||||
"""健康检查接口"""
|
|
||||||
try:
|
|
||||||
# 检查数据库连接
|
|
||||||
mongo.db.command('ping')
|
|
||||||
db_status = 'connected'
|
|
||||||
except Exception as e:
|
|
||||||
db_status = f'error: {str(e)}'
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'status': 'running',
|
|
||||||
'database': db_status,
|
|
||||||
'timestamp': datetime.now().isoformat()
|
|
||||||
})
|
|
||||||
|
|
||||||
# 60sapi静态文件服务
|
|
||||||
@app.route('/60sapi/<path:filename>')
|
|
||||||
def serve_60sapi_files(filename):
|
|
||||||
"""提供60sapi目录下的静态文件服务"""
|
|
||||||
try:
|
|
||||||
# 获取项目根目录
|
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
api_directory = os.path.join(project_root, 'frontend', '60sapi')
|
|
||||||
|
|
||||||
# 安全检查:确保文件路径在允许的目录内
|
|
||||||
full_path = os.path.join(api_directory, filename)
|
|
||||||
if not os.path.commonpath([api_directory, full_path]) == api_directory:
|
|
||||||
return jsonify({'error': '非法文件路径'}), 403
|
|
||||||
|
|
||||||
# 检查文件是否存在
|
|
||||||
if not os.path.exists(full_path):
|
|
||||||
return jsonify({'error': '文件不存在'}), 404
|
|
||||||
|
|
||||||
# 获取文件目录和文件名
|
|
||||||
directory = os.path.dirname(full_path)
|
|
||||||
file_name = os.path.basename(full_path)
|
|
||||||
|
|
||||||
return send_from_directory(directory, file_name)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'error': f'文件服务错误: {str(e)}'}), 500
|
|
||||||
|
|
||||||
# smallgame静态文件服务
|
|
||||||
@app.route('/smallgame/<path:filename>')
|
|
||||||
def serve_smallgame_files(filename):
|
|
||||||
"""提供smallgame目录下的静态文件服务"""
|
|
||||||
try:
|
|
||||||
# 获取项目根目录
|
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
game_directory = os.path.join(project_root, 'frontend', 'smallgame')
|
|
||||||
|
|
||||||
# 安全检查:确保文件路径在允许的目录内
|
|
||||||
full_path = os.path.join(game_directory, filename)
|
|
||||||
if not os.path.commonpath([game_directory, full_path]) == game_directory:
|
|
||||||
return jsonify({'error': '非法文件路径'}), 403
|
|
||||||
|
|
||||||
# 检查文件是否存在
|
|
||||||
if not os.path.exists(full_path):
|
|
||||||
return jsonify({'error': '文件不存在'}), 404
|
|
||||||
|
|
||||||
# 获取文件目录和文件名
|
|
||||||
directory = os.path.dirname(full_path)
|
|
||||||
file_name = os.path.basename(full_path)
|
|
||||||
|
|
||||||
return send_from_directory(directory, file_name)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'error': f'文件服务错误: {str(e)}'}), 500
|
|
||||||
|
|
||||||
# aimodelapp静态文件服务
|
|
||||||
@app.route('/aimodelapp/<path:filename>')
|
|
||||||
def serve_aimodelapp_files(filename):
|
|
||||||
"""提供aimodelapp目录下的静态文件服务"""
|
|
||||||
try:
|
|
||||||
# 获取项目根目录
|
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
ai_directory = os.path.join(project_root, 'frontend', 'public', 'aimodelapp')
|
|
||||||
|
|
||||||
# 安全检查:确保文件路径在允许的目录内
|
|
||||||
full_path = os.path.join(ai_directory, filename)
|
|
||||||
if not os.path.commonpath([ai_directory, full_path]) == ai_directory:
|
|
||||||
return jsonify({'error': '非法文件路径'}), 403
|
|
||||||
|
|
||||||
# 检查文件是否存在
|
|
||||||
if not os.path.exists(full_path):
|
|
||||||
return jsonify({'error': '文件不存在'}), 404
|
|
||||||
|
|
||||||
# 获取文件目录和文件名
|
|
||||||
directory = os.path.dirname(full_path)
|
|
||||||
file_name = os.path.basename(full_path)
|
|
||||||
|
|
||||||
return send_from_directory(directory, file_name)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'error': f'文件服务错误: {str(e)}'}), 500
|
|
||||||
|
|
||||||
# 错误处理
|
|
||||||
@app.errorhandler(404)
|
|
||||||
def not_found(error):
|
|
||||||
return jsonify({
|
|
||||||
'error': 'API接口不存在',
|
|
||||||
'message': '请检查请求路径是否正确'
|
|
||||||
}), 404
|
|
||||||
|
|
||||||
@app.errorhandler(500)
|
|
||||||
def internal_error(error):
|
|
||||||
return jsonify({
|
|
||||||
'error': '服务器内部错误',
|
|
||||||
'message': '请稍后重试或联系管理员'
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
# 为 Gunicorn 创建应用实例
|
|
||||||
app = create_app()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print("🚀 启动 InfoGenie 后端服务...")
|
|
||||||
app.run(debug=True, host='0.0.0.0', port=5002)
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
InfoGenie 配置文件
|
|
||||||
Created by: 万象口袋
|
|
||||||
Date: 2025-09-02
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from datetime import timedelta
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
# 加载环境变量
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""应用配置类"""
|
|
||||||
|
|
||||||
# 基础配置
|
|
||||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'infogenie-secret-key-2025'
|
|
||||||
|
|
||||||
# MongoDB 配置
|
|
||||||
MONGO_URI = os.environ.get('MONGO_URI') or 'mongodb://localhost:27017/InfoGenie'
|
|
||||||
|
|
||||||
# hwt 配置
|
|
||||||
HWT_LIFETIME = timedelta(days=7) # hwt持续7天
|
|
||||||
HWT_SECURE = False # 开发环境设为False,生产环境设为True
|
|
||||||
HWT_HTTPONLY = True
|
|
||||||
HWT_SAMESITE = 'Lax'
|
|
||||||
HWT_DOMAIN = None # 开发环境设为None,生产环境设为具体域名
|
|
||||||
HWT_PATH = '/'
|
|
||||||
HWT_REFRESH_EACH_REQUEST = True # 每次请求刷新hwt过期时间
|
|
||||||
|
|
||||||
# 邮件配置
|
|
||||||
MAIL_SERVER = 'smtp.qq.com'
|
|
||||||
MAIL_PORT = 465
|
|
||||||
MAIL_USE_SSL = True
|
|
||||||
MAIL_USE_TLS = False
|
|
||||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'your-email@qq.com'
|
|
||||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'your-app-password'
|
|
||||||
MAIL_DEFAULT_SENDER = ('InfoGenie 万象口袋', os.environ.get('MAIL_USERNAME') or 'your-email@qq.com')
|
|
||||||
|
|
||||||
# API 配置
|
|
||||||
API_RATE_LIMIT = '100 per hour' # API调用频率限制
|
|
||||||
|
|
||||||
# 外部API配置
|
|
||||||
EXTERNAL_APIS = {
|
|
||||||
'60s': [
|
|
||||||
'https://60s.api.shumengya.top'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
# 应用信息
|
|
||||||
APP_INFO = {
|
|
||||||
'name': '✨ 万象口袋 ✨',
|
|
||||||
'description': '🎨 一个多功能的聚合软件应用 💬',
|
|
||||||
'author': '👨💻 by-万象口袋',
|
|
||||||
'version': '1.0.0',
|
|
||||||
'icp': '📄 蜀ICP备2025151694号'
|
|
||||||
}
|
|
||||||
|
|
||||||
class DevelopmentConfig(Config):
|
|
||||||
"""开发环境配置"""
|
|
||||||
DEBUG = True
|
|
||||||
TESTING = False
|
|
||||||
|
|
||||||
class ProductionConfig(Config):
|
|
||||||
"""生产环境配置"""
|
|
||||||
DEBUG = False
|
|
||||||
TESTING = False
|
|
||||||
HWT_SECURE = True
|
|
||||||
|
|
||||||
class TestingConfig(Config):
|
|
||||||
"""测试环境配置"""
|
|
||||||
DEBUG = True
|
|
||||||
TESTING = True
|
|
||||||
MONGO_URI = 'mongodb://localhost:27017/InfoGenie_Test'
|
|
||||||
|
|
||||||
# 配置字典
|
|
||||||
config = {
|
|
||||||
'development': DevelopmentConfig,
|
|
||||||
'production': ProductionConfig,
|
|
||||||
'testing': TestingConfig,
|
|
||||||
'default': DevelopmentConfig
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
infogenie-backend:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: infogenie-backend
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "2323:2323"
|
|
||||||
volumes:
|
|
||||||
# 持久化数据映射
|
|
||||||
- /shumengya/docker/infogenie-backend/data:/app/data
|
|
||||||
|
|
||||||
environment:
|
|
||||||
# 从 .env 文件读取环境变量
|
|
||||||
- MONGO_URI=${MONGO_URI}
|
|
||||||
- MAIL_USERNAME=${MAIL_USERNAME}
|
|
||||||
- MAIL_PASSWORD=${MAIL_PASSWORD}
|
|
||||||
- SECRET_KEY=${SECRET_KEY}
|
|
||||||
- FLASK_ENV=production
|
|
||||||
# 生产环境配置
|
|
||||||
- HWT_DOMAIN=.shumengya.top
|
|
||||||
- HWT_SECURE=False # 如果使用 HTTPS 反向代理,设为 False;直接 HTTPS 设为 True
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
networks:
|
|
||||||
- infogenie-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:2323/api/health || exit 1"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
networks:
|
|
||||||
infogenie-network:
|
|
||||||
driver: bridge
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,455 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
用户认证模块
|
|
||||||
Created by: 万象口袋
|
|
||||||
Date: 2025-09-02
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import Blueprint, request, jsonify, current_app
|
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
|
||||||
import hashlib
|
|
||||||
import re
|
|
||||||
import jwt
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from functools import wraps
|
|
||||||
from .email_service import send_verification_email, verify_code, is_qq_email, get_qq_avatar_url
|
|
||||||
|
|
||||||
auth_bp = Blueprint('auth', __name__)
|
|
||||||
|
|
||||||
#生成JWT token
|
|
||||||
def generate_token(user_data):
|
|
||||||
"""生成JWT token"""
|
|
||||||
payload = {
|
|
||||||
'user_id': user_data['user_id'],
|
|
||||||
'email': user_data['email'],
|
|
||||||
'username': user_data['username'],
|
|
||||||
'exp': datetime.utcnow() + timedelta(days=7), # 7天过期
|
|
||||||
'iat': datetime.utcnow()
|
|
||||||
}
|
|
||||||
return jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
|
|
||||||
|
|
||||||
#验证JWT token
|
|
||||||
def verify_token(token):
|
|
||||||
"""验证JWT token"""
|
|
||||||
try:
|
|
||||||
payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
|
|
||||||
return {'success': True, 'data': payload}
|
|
||||||
except jwt.ExpiredSignatureError:
|
|
||||||
return {'success': False, 'message': 'Token已过期'}
|
|
||||||
except jwt.InvalidTokenError:
|
|
||||||
return {'success': False, 'message': 'Token无效'}
|
|
||||||
|
|
||||||
#JWT token验证装饰器
|
|
||||||
def token_required(f):
|
|
||||||
"""JWT token验证装饰器"""
|
|
||||||
@wraps(f)
|
|
||||||
def decorated(*args, **kwargs):
|
|
||||||
token = request.headers.get('Authorization')
|
|
||||||
if not token:
|
|
||||||
return jsonify({'success': False, 'message': '缺少认证token'}), 401
|
|
||||||
|
|
||||||
if token.startswith('Bearer '):
|
|
||||||
token = token[7:]
|
|
||||||
|
|
||||||
result = verify_token(token)
|
|
||||||
if not result['success']:
|
|
||||||
return jsonify({'success': False, 'message': result['message']}), 401
|
|
||||||
|
|
||||||
request.current_user = result['data']
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
return decorated
|
|
||||||
|
|
||||||
#验证QQ邮箱格式
|
|
||||||
def validate_qq_email(email):
|
|
||||||
"""验证QQ邮箱格式"""
|
|
||||||
return is_qq_email(email)
|
|
||||||
|
|
||||||
#验证密码格式
|
|
||||||
def validate_password(password):
|
|
||||||
"""验证密码格式(6-20位)"""
|
|
||||||
return 6 <= len(password) <= 20
|
|
||||||
|
|
||||||
|
|
||||||
#==========================对外暴露的HTTP接口==========================
|
|
||||||
#发送验证码邮件
|
|
||||||
@auth_bp.route('/send-verification', methods=['POST'])
|
|
||||||
def send_verification():
|
|
||||||
"""发送验证码邮件"""
|
|
||||||
try:
|
|
||||||
data = request.get_json()
|
|
||||||
email = data.get('email', '').strip()
|
|
||||||
verification_type = data.get('type', 'register') # register, login
|
|
||||||
|
|
||||||
# 参数验证
|
|
||||||
if not email:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '邮箱地址不能为空'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
if not validate_qq_email(email):
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '仅支持QQ邮箱(qq.com、vip.qq.com、foxmail.com)'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
# 获取数据库集合
|
|
||||||
db = current_app.mongo.db
|
|
||||||
users_collection = db.userdata
|
|
||||||
|
|
||||||
# 检查邮箱是否已注册
|
|
||||||
existing_user = users_collection.find_one({'邮箱': email})
|
|
||||||
|
|
||||||
if verification_type == 'register' and existing_user:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '该邮箱已被注册'
|
|
||||||
}), 409
|
|
||||||
|
|
||||||
if verification_type == 'login' and not existing_user:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '该邮箱尚未注册'
|
|
||||||
}), 404
|
|
||||||
|
|
||||||
# 发送验证码
|
|
||||||
result = send_verification_email(email, verification_type)
|
|
||||||
|
|
||||||
if result['success']:
|
|
||||||
return jsonify(result), 200
|
|
||||||
else:
|
|
||||||
return jsonify(result), 500
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
current_app.logger.error(f"发送验证码失败: {str(e)}")
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '发送失败,请稍后重试'
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
#验证验证码
|
|
||||||
@auth_bp.route('/verify-code', methods=['POST'])
|
|
||||||
def verify_verification_code():
|
|
||||||
"""验证验证码"""
|
|
||||||
try:
|
|
||||||
data = request.get_json()
|
|
||||||
email = data.get('email', '').strip()
|
|
||||||
code = data.get('code', '').strip()
|
|
||||||
|
|
||||||
# 参数验证
|
|
||||||
if not email or not code:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '邮箱和验证码不能为空'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
# 验证码校验
|
|
||||||
result = verify_code(email, code)
|
|
||||||
|
|
||||||
if result['success']:
|
|
||||||
return jsonify(result), 200
|
|
||||||
else:
|
|
||||||
return jsonify(result), 400
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
current_app.logger.error(f"验证码校验失败: {str(e)}")
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '验证失败,请稍后重试'
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
#用户注册
|
|
||||||
@auth_bp.route('/register', methods=['POST'])
|
|
||||||
def register():
|
|
||||||
"""用户注册(需要先验证邮箱)"""
|
|
||||||
try:
|
|
||||||
data = request.get_json()
|
|
||||||
email = data.get('email', '').strip()
|
|
||||||
username = data.get('username', '').strip()
|
|
||||||
password = data.get('password', '').strip()
|
|
||||||
code = data.get('code', '').strip()
|
|
||||||
|
|
||||||
# 参数验证
|
|
||||||
if not all([email, username, password, code]):
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '所有字段都不能为空'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
if not validate_qq_email(email):
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '仅支持QQ邮箱注册'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
if not validate_password(password):
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '密码长度必须在6-20位之间'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
# 验证验证码
|
|
||||||
verify_result = verify_code(email, code)
|
|
||||||
if not verify_result['success'] or verify_result.get('type') != 'register':
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '验证码无效或已过期'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
# 获取数据库集合
|
|
||||||
db = current_app.mongo.db
|
|
||||||
users_collection = db.userdata
|
|
||||||
|
|
||||||
# 检查邮箱是否已被注册
|
|
||||||
if users_collection.find_one({'邮箱': email}):
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '该邮箱已被注册'
|
|
||||||
}), 409
|
|
||||||
|
|
||||||
# 检查用户名是否已被使用
|
|
||||||
if users_collection.find_one({'用户名': username}):
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '该用户名已被使用'
|
|
||||||
}), 409
|
|
||||||
|
|
||||||
# 获取QQ头像
|
|
||||||
avatar_url = get_qq_avatar_url(email)
|
|
||||||
|
|
||||||
# 创建新用户
|
|
||||||
password_hash = generate_password_hash(password)
|
|
||||||
user_data = {
|
|
||||||
'邮箱': email,
|
|
||||||
'用户名': username,
|
|
||||||
'密码': password_hash,
|
|
||||||
'头像': avatar_url,
|
|
||||||
'注册时间': datetime.now().isoformat(),
|
|
||||||
'最后登录': None,
|
|
||||||
'登录次数': 0,
|
|
||||||
'用户状态': 'active',
|
|
||||||
'等级': 0,
|
|
||||||
'经验': 0,
|
|
||||||
'萌芽币': 0,
|
|
||||||
'签到系统': {
|
|
||||||
'连续签到天数': 0,
|
|
||||||
'今日是否已签到': False,
|
|
||||||
'签到时间': '2025-01-01'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result = users_collection.insert_one(user_data)
|
|
||||||
|
|
||||||
if result.inserted_id:
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'message': '注册成功!',
|
|
||||||
'user': {
|
|
||||||
'email': email,
|
|
||||||
'username': username,
|
|
||||||
'avatar': avatar_url
|
|
||||||
}
|
|
||||||
}), 201
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '注册失败,请稍后重试'
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
current_app.logger.error(f"注册失败: {str(e)}")
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '注册失败,请稍后重试'
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
#用户登录
|
|
||||||
@auth_bp.route('/login', methods=['POST'])
|
|
||||||
def login():
|
|
||||||
"""用户登录(支持邮箱+验证码或邮箱+密码)"""
|
|
||||||
try:
|
|
||||||
data = request.get_json()
|
|
||||||
email = data.get('email', '').strip()
|
|
||||||
password = data.get('password', '').strip()
|
|
||||||
code = data.get('code', '').strip()
|
|
||||||
|
|
||||||
# 参数验证
|
|
||||||
if not email:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '邮箱地址不能为空'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
if not validate_qq_email(email):
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '仅支持QQ邮箱登录'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
# 获取数据库集合
|
|
||||||
db = current_app.mongo.db
|
|
||||||
users_collection = db.userdata
|
|
||||||
|
|
||||||
# 查找用户
|
|
||||||
user = users_collection.find_one({'邮箱': email})
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '该邮箱尚未注册'
|
|
||||||
}), 404
|
|
||||||
|
|
||||||
# 检查用户状态
|
|
||||||
if user.get('用户状态') != 'active':
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '账号已被禁用,请联系管理员'
|
|
||||||
}), 403
|
|
||||||
|
|
||||||
# 验证方式:验证码登录或密码登录
|
|
||||||
if code:
|
|
||||||
# 验证码登录
|
|
||||||
verify_result = verify_code(email, code)
|
|
||||||
if not verify_result['success'] or verify_result.get('type') != 'login':
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '验证码无效或已过期'
|
|
||||||
}), 400
|
|
||||||
elif password:
|
|
||||||
# 密码登录
|
|
||||||
if not check_password_hash(user['密码'], password):
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '密码错误'
|
|
||||||
}), 401
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '请输入密码或验证码'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
# 登录成功,更新用户信息
|
|
||||||
users_collection.update_one(
|
|
||||||
{'邮箱': email},
|
|
||||||
{
|
|
||||||
'$set': {'最后登录': datetime.now().isoformat()},
|
|
||||||
'$inc': {'登录次数': 1}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# 生成JWT token
|
|
||||||
user_data = {
|
|
||||||
'user_id': str(user['_id']),
|
|
||||||
'email': email,
|
|
||||||
'username': user.get('用户名', '')
|
|
||||||
}
|
|
||||||
token = generate_token(user_data)
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'message': '登录成功!',
|
|
||||||
'token': token,
|
|
||||||
'user': {
|
|
||||||
'id': str(user['_id']),
|
|
||||||
'email': email,
|
|
||||||
'username': user.get('用户名', ''),
|
|
||||||
'avatar': user.get('头像', ''),
|
|
||||||
'login_count': user.get('登录次数', 0) + 1
|
|
||||||
}
|
|
||||||
}), 200
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
current_app.logger.error(f"登录失败: {str(e)}")
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': '登录失败,请稍后重试'
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
# 登录成功,创建会话
|
|
||||||
hwt = getattr(request, 'hwt', {})
|
|
||||||
hwt['user_id'] = str(user['_id'])
|
|
||||||
hwt['account'] = user['账号']
|
|
||||||
hwt['logged_in'] = True
|
|
||||||
|
|
||||||
# 更新登录信息
|
|
||||||
users_collection.update_one(
|
|
||||||
{'_id': user['_id']},
|
|
||||||
{
|
|
||||||
'$set': {'最后登录': datetime.now().isoformat()},
|
|
||||||
'$inc': {'登录次数': 1}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'message': '登录成功!',
|
|
||||||
'user': {
|
|
||||||
'account': user['账号'],
|
|
||||||
'last_login': user.get('最后登录'),
|
|
||||||
'login_count': user.get('登录次数', 0) + 1
|
|
||||||
}
|
|
||||||
}), 200
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': f'服务器错误: {str(e)}'
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
#用户登出
|
|
||||||
@auth_bp.route('/logout', methods=['POST'])
|
|
||||||
def logout():
|
|
||||||
"""用户登出"""
|
|
||||||
try:
|
|
||||||
# JWT是无状态的,客户端删除token即可
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'message': '已成功登出'
|
|
||||||
}), 200
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': f'服务器错误: {str(e)}'
|
|
||||||
}), 500
|
|
||||||
|
|
||||||
#检查登录状态
|
|
||||||
@auth_bp.route('/check', methods=['GET'])
|
|
||||||
def check_login():
|
|
||||||
"""检查登录状态"""
|
|
||||||
try:
|
|
||||||
token = request.headers.get('Authorization')
|
|
||||||
if not token:
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'logged_in': False
|
|
||||||
}), 200
|
|
||||||
|
|
||||||
if token.startswith('Bearer '):
|
|
||||||
token = token[7:]
|
|
||||||
|
|
||||||
result = verify_token(token)
|
|
||||||
if result['success']:
|
|
||||||
user_data = result['data']
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'logged_in': True,
|
|
||||||
'user': {
|
|
||||||
'id': user_data['user_id'],
|
|
||||||
'email': user_data['email'],
|
|
||||||
'username': user_data['username']
|
|
||||||
}
|
|
||||||
}), 200
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'logged_in': False
|
|
||||||
}), 200
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'message': f'服务器错误: {str(e)}'
|
|
||||||
}), 500
|
|
||||||
#==========================对外暴露的HTTP接口==========================
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
邮件发送模块
|
|
||||||
负责处理用户注册、登录验证邮件
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
import smtplib
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from email.mime.text import MIMEText
|
|
||||||
from email.header import Header
|
|
||||||
from flask import current_app
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
# 验证码存储(生产环境建议使用Redis)
|
|
||||||
verification_codes = {}
|
|
||||||
|
|
||||||
# 初始化日志
|
|
||||||
def init_mail(app):
|
|
||||||
"""初始化邮件配置"""
|
|
||||||
# 使用smtplib直接发送,不需要Flask-Mail
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 生成验证码
|
|
||||||
def generate_verification_code(length=6):
|
|
||||||
"""生成验证码"""
|
|
||||||
return ''.join(random.choices(string.digits, k=length))
|
|
||||||
|
|
||||||
# 发送验证邮件
|
|
||||||
def send_verification_email(email, verification_type='register'):
|
|
||||||
"""
|
|
||||||
发送验证邮件
|
|
||||||
|
|
||||||
Args:
|
|
||||||
email: 收件人邮箱
|
|
||||||
verification_type: 验证类型 ('register', 'login', 'reset_password')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 发送结果
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 验证QQ邮箱格式
|
|
||||||
if not is_qq_email(email):
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'message': '仅支持QQ邮箱注册登录'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 生成验证码
|
|
||||||
code = generate_verification_code()
|
|
||||||
|
|
||||||
# 存储验证码(5分钟有效期)
|
|
||||||
verification_codes[email] = {
|
|
||||||
'code': code,
|
|
||||||
'type': verification_type,
|
|
||||||
'expires_at': datetime.now() + timedelta(minutes=5),
|
|
||||||
'attempts': 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 获取邮件配置 - 使用与QQEmailSendAPI相同的配置
|
|
||||||
sender_email = os.environ.get('MAIL_USERNAME', '3205788256@qq.com')
|
|
||||||
sender_password = os.environ.get('MAIL_PASSWORD', 'szcaxvbftusqddhi')
|
|
||||||
|
|
||||||
# 邮件模板
|
|
||||||
if verification_type == 'register':
|
|
||||||
subject = '【万象口袋】注册验证码'
|
|
||||||
html_content = f'''
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
||||||
<div style="text-align: center; margin-bottom: 30px;">
|
|
||||||
<h1 style="color: #66bb6a; margin: 0;">万象口袋</h1>
|
|
||||||
<p style="color: #666; font-size: 14px; margin: 5px 0;">欢迎注册万象口袋</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 100%); padding: 30px; border-radius: 15px; text-align: center;">
|
|
||||||
<h2 style="color: #2e7d32; margin-bottom: 20px;">验证码</h2>
|
|
||||||
<div style="background: white; padding: 20px; border-radius: 10px; margin: 20px 0;">
|
|
||||||
<span style="font-size: 32px; font-weight: bold; color: #66bb6a; letter-spacing: 5px;">{code}</span>
|
|
||||||
</div>
|
|
||||||
<p style="color: #4a4a4a; margin: 15px 0;">请在5分钟内输入此验证码完成注册</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 10px;">
|
|
||||||
<p style="color: #666; font-size: 12px; margin: 0; text-align: center;">
|
|
||||||
如果您没有申请注册,请忽略此邮件<br>
|
|
||||||
此验证码5分钟内有效,请勿泄露给他人
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
'''
|
|
||||||
else: # login
|
|
||||||
subject = '【InfoGenie】登录验证码'
|
|
||||||
html_content = f'''
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
||||||
<div style="text-align: center; margin-bottom: 30px;">
|
|
||||||
<h1 style="color: #66bb6a; margin: 0;">InfoGenie 万象口袋</h1>
|
|
||||||
<p style="color: #666; font-size: 14px; margin: 5px 0;">安全登录验证</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%); padding: 30px; border-radius: 15px; text-align: center;">
|
|
||||||
<h2 style="color: white; margin-bottom: 20px;">登录验证码</h2>
|
|
||||||
<div style="background: white; padding: 20px; border-radius: 10px; margin: 20px 0;">
|
|
||||||
<span style="font-size: 32px; font-weight: bold; color: #66bb6a; letter-spacing: 5px;">{code}</span>
|
|
||||||
</div>
|
|
||||||
<p style="color: white; margin: 15px 0;">请在5分钟内输入此验证码完成登录</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 10px;">
|
|
||||||
<p style="color: #666; font-size: 12px; margin: 0; text-align: center;">
|
|
||||||
如果不是您本人操作,请检查账户安全<br>
|
|
||||||
此验证码5分钟内有效,请勿泄露给他人
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
'''
|
|
||||||
|
|
||||||
# 创建邮件 - 使用与QQEmailSendAPI相同的方式
|
|
||||||
message = MIMEText(html_content, 'html', 'utf-8')
|
|
||||||
message['From'] = sender_email # 直接使用邮箱地址,不使用Header包装
|
|
||||||
message['To'] = email
|
|
||||||
message['Subject'] = Header(subject, 'utf-8')
|
|
||||||
|
|
||||||
# 发送邮件 - 使用SSL端口465
|
|
||||||
try:
|
|
||||||
# 使用与QQEmailSendAPI相同的连接方式
|
|
||||||
smtp_obj = smtplib.SMTP_SSL('smtp.qq.com', 465)
|
|
||||||
smtp_obj.login(sender_email, sender_password)
|
|
||||||
smtp_obj.sendmail(sender_email, [email], message.as_string())
|
|
||||||
smtp_obj.quit()
|
|
||||||
|
|
||||||
print(f"验证码邮件发送成功: {email}")
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'message': '验证码已发送到您的邮箱',
|
|
||||||
'email': email
|
|
||||||
}
|
|
||||||
|
|
||||||
except smtplib.SMTPAuthenticationError as auth_error:
|
|
||||||
print(f"SMTP认证失败: {str(auth_error)}")
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'message': 'SMTP认证失败,请检查邮箱配置'
|
|
||||||
}
|
|
||||||
except smtplib.SMTPConnectError as conn_error:
|
|
||||||
print(f"SMTP连接失败: {str(conn_error)}")
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'message': 'SMTP服务器连接失败'
|
|
||||||
}
|
|
||||||
except Exception as smtp_error:
|
|
||||||
print(f"SMTP发送失败: {str(smtp_error)}")
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'message': f'邮件发送失败: {str(smtp_error)}'
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"邮件发送失败: {str(e)}")
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'message': '邮件发送失败,请稍后重试'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 验证验证码
|
|
||||||
def verify_code(email, code):
|
|
||||||
"""
|
|
||||||
验证验证码
|
|
||||||
|
|
||||||
Args:
|
|
||||||
email: 邮箱地址
|
|
||||||
code: 验证码
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 验证结果
|
|
||||||
"""
|
|
||||||
if email not in verification_codes:
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'message': '验证码不存在或已过期'
|
|
||||||
}
|
|
||||||
|
|
||||||
stored_info = verification_codes[email]
|
|
||||||
|
|
||||||
# 检查过期时间
|
|
||||||
if datetime.now() > stored_info['expires_at']:
|
|
||||||
del verification_codes[email]
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'message': '验证码已过期,请重新获取'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 检查尝试次数
|
|
||||||
if stored_info['attempts'] >= 3:
|
|
||||||
del verification_codes[email]
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'message': '验证码输入错误次数过多,请重新获取'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 验证码校验
|
|
||||||
if stored_info['code'] != code:
|
|
||||||
stored_info['attempts'] += 1
|
|
||||||
return {
|
|
||||||
'success': False,
|
|
||||||
'message': f'验证码错误,还可尝试{3 - stored_info["attempts"]}次'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 验证成功,删除验证码
|
|
||||||
verification_type = stored_info['type']
|
|
||||||
del verification_codes[email]
|
|
||||||
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'message': '验证码验证成功',
|
|
||||||
'type': verification_type
|
|
||||||
}
|
|
||||||
|
|
||||||
# 验证QQ邮箱格式
|
|
||||||
def is_qq_email(email):
|
|
||||||
"""
|
|
||||||
验证是否为QQ邮箱
|
|
||||||
|
|
||||||
Args:
|
|
||||||
email: 邮箱地址
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否为QQ邮箱
|
|
||||||
"""
|
|
||||||
if not email or '@' not in email:
|
|
||||||
return False
|
|
||||||
|
|
||||||
domain = email.split('@')[1].lower()
|
|
||||||
qq_domains = ['qq.com', 'vip.qq.com', 'foxmail.com']
|
|
||||||
|
|
||||||
return domain in qq_domains
|
|
||||||
|
|
||||||
# 获取QQ头像URL
|
|
||||||
def get_qq_avatar_url(email):
|
|
||||||
"""
|
|
||||||
根据QQ邮箱获取QQ头像URL
|
|
||||||
|
|
||||||
Args:
|
|
||||||
email: QQ邮箱地址
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: QQ头像URL
|
|
||||||
"""
|
|
||||||
if not is_qq_email(email):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 提取QQ号码
|
|
||||||
qq_number = email.split('@')[0]
|
|
||||||
|
|
||||||
# 验证是否为纯数字(QQ号)
|
|
||||||
if not qq_number.isdigit():
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 返回QQ头像API URL
|
|
||||||
return f"https://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100"
|
|
||||||
|
|
||||||
# 清理过期验证码
|
|
||||||
def cleanup_expired_codes():
|
|
||||||
"""清理过期的验证码"""
|
|
||||||
current_time = datetime.now()
|
|
||||||
expired_emails = [
|
|
||||||
email for email, info in verification_codes.items()
|
|
||||||
if current_time > info['expires_at']
|
|
||||||
]
|
|
||||||
|
|
||||||
for email in expired_emails:
|
|
||||||
del verification_codes[email]
|
|
||||||
|
|
||||||
return len(expired_emails)
|
|
||||||
@@ -1,550 +0,0 @@
|
|||||||
#!/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接口==========================
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# InfoGenie 后端依赖包
|
|
||||||
# Web框架
|
|
||||||
Flask==2.3.3
|
|
||||||
Flask-CORS==4.0.0
|
|
||||||
|
|
||||||
# 数据库
|
|
||||||
Flask-PyMongo==2.3.0
|
|
||||||
pymongo==4.5.0
|
|
||||||
|
|
||||||
# 密码加密
|
|
||||||
Werkzeug==2.3.7
|
|
||||||
|
|
||||||
# JWT认证
|
|
||||||
PyJWT==2.8.0
|
|
||||||
|
|
||||||
# HTTP请求
|
|
||||||
requests==2.31.0
|
|
||||||
|
|
||||||
# 邮件发送
|
|
||||||
Flask-Mail==0.9.1
|
|
||||||
|
|
||||||
# 数据处理
|
|
||||||
python-dateutil==2.8.2
|
|
||||||
|
|
||||||
# 环境变量
|
|
||||||
python-dotenv==1.0.0
|
|
||||||
|
|
||||||
# 开发工具
|
|
||||||
flask-limiter==3.5.0 # API限流
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
@echo off
|
|
||||||
python app.py
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
python3 app.py
|
|
||||||
@@ -1,396 +0,0 @@
|
|||||||
# 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使用记录和萌芽币消费情况
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"账号":"3205788256",
|
|
||||||
"邮箱":"3205788256@qq.com",
|
|
||||||
"密码":"0123456789",
|
|
||||||
"等级":0,
|
|
||||||
"经验":0,
|
|
||||||
"萌芽币":0,
|
|
||||||
"签到系统":{
|
|
||||||
"连续签到天数":0,
|
|
||||||
"今日是否已签到":false,
|
|
||||||
"签到时间":"2025-01-01"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
# React 开发环境变量
|
# 复制为 .env.local 后可按需覆盖本地开发值
|
||||||
|
|
||||||
# API URL - 开发环境使用本地后端
|
|
||||||
REACT_APP_API_URL=http://127.0.0.1:5002
|
REACT_APP_API_URL=http://127.0.0.1:5002
|
||||||
|
REACT_APP_AUTH_URL=https://auth.shumengya.top
|
||||||
# 应用信息
|
REACT_APP_AUTH_API_URL=https://auth.api.shumengya.top
|
||||||
REACT_APP_NAME=InfoGenie
|
REACT_APP_NAME=InfoGenie
|
||||||
REACT_APP_VERSION=1.0.0
|
REACT_APP_VERSION=1.0.0
|
||||||
|
|
||||||
# 调试模式
|
|
||||||
REACT_APP_DEBUG=true
|
REACT_APP_DEBUG=true
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
# React 生产环境变量
|
# 生产前端环境变量
|
||||||
# 用于前端独立部署
|
|
||||||
|
|
||||||
# API URL - 指向后端服务器
|
|
||||||
# 使用域名方式访问后端 API
|
|
||||||
REACT_APP_API_URL=https://infogenie.api.shumengya.top
|
REACT_APP_API_URL=https://infogenie.api.shumengya.top
|
||||||
|
REACT_APP_AUTH_URL=https://auth.shumengya.top
|
||||||
# 应用信息
|
REACT_APP_AUTH_API_URL=https://auth.api.shumengya.top
|
||||||
REACT_APP_NAME=InfoGenie
|
REACT_APP_NAME=InfoGenie
|
||||||
REACT_APP_VERSION=1.0.0
|
REACT_APP_VERSION=1.0.0
|
||||||
|
|
||||||
# 调试模式(生产环境关闭)
|
|
||||||
REACT_APP_DEBUG=false
|
REACT_APP_DEBUG=false
|
||||||
|
|||||||
0
InfoGenie-frontend/前端架构文档.md → InfoGenie-frontend/README.md
Executable file → Normal file
0
InfoGenie-frontend/前端架构文档.md → InfoGenie-frontend/README.md
Executable file → Normal file
@@ -1,4 +0,0 @@
|
|||||||
@echo off
|
|
||||||
npm run build
|
|
||||||
npx serve -s build
|
|
||||||
pause
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# 生产环境API配置
|
|
||||||
REACT_APP_API_URL=https://infogenie.api.shumengya.top
|
|
||||||
|
|
||||||
# 生产环境API配置
|
|
||||||
REACT_APP_API_URL=http://127.0.0.1:5002
|
|
||||||
33
InfoGenie-frontend/package-lock.json
generated
33
InfoGenie-frontend/package-lock.json
generated
@@ -83,7 +83,6 @@
|
|||||||
"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",
|
||||||
@@ -733,7 +732,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1617,7 +1615,6 @@
|
|||||||
"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",
|
||||||
@@ -4682,7 +4679,8 @@
|
|||||||
"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",
|
||||||
@@ -4838,7 +4836,6 @@
|
|||||||
"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",
|
||||||
@@ -4892,7 +4889,6 @@
|
|||||||
"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",
|
||||||
@@ -5262,7 +5258,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -5361,7 +5356,6 @@
|
|||||||
"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",
|
||||||
@@ -6327,7 +6321,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"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",
|
||||||
@@ -6486,9 +6479,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001739",
|
"version": "1.0.30001780",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz",
|
||||||
"integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==",
|
"integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -7394,8 +7387,7 @@
|
|||||||
"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",
|
||||||
@@ -8261,7 +8253,6 @@
|
|||||||
"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",
|
||||||
@@ -11096,7 +11087,6 @@
|
|||||||
"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",
|
||||||
@@ -15168,7 +15158,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -16356,7 +16345,6 @@
|
|||||||
"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"
|
||||||
@@ -16722,7 +16710,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -16873,7 +16860,6 @@
|
|||||||
"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"
|
||||||
@@ -16925,7 +16911,6 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -17444,7 +17429,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -17690,7 +17674,6 @@
|
|||||||
"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",
|
||||||
@@ -19396,7 +19379,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -19826,7 +19808,6 @@
|
|||||||
"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",
|
||||||
@@ -19898,7 +19879,6 @@
|
|||||||
"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",
|
||||||
@@ -20311,7 +20291,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "infogenie-frontend",
|
"name": "infogenie-frontend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "✨ 万象口袋 - 前端React应用",
|
"description": "万象口袋 - 前端React应用",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
"api",
|
"api",
|
||||||
|
|||||||
13
InfoGenie-frontend/public/60sapi/ig-embed.js
Normal file
13
InfoGenie-frontend/public/60sapi/ig-embed.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 在 iframe 内嵌展示时隐藏静态页自带的顶栏,避免与 FullscreenEmbed 顶栏重复
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
if (window.self !== window.top) {
|
||||||
|
var el = document.createElement('style');
|
||||||
|
el.setAttribute('data-ig-embed', '1');
|
||||||
|
el.textContent = '.header{display:none!important}';
|
||||||
|
document.head.appendChild(el);
|
||||||
|
}
|
||||||
|
} catch (e) { /* 忽略 */ }
|
||||||
|
})();
|
||||||
13
InfoGenie-frontend/public/60sapi/sixty-runtime.js
Normal file
13
InfoGenie-frontend/public/60sapi/sixty-runtime.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/* 由 SPA 在 iframe URL 上附加 ?sixty_base=encodeURIComponent(baseUrl);无参数时默认萌芽节点 */
|
||||||
|
(function () {
|
||||||
|
var fallback = 'https://60s.api.shumengya.top';
|
||||||
|
var base = fallback;
|
||||||
|
try {
|
||||||
|
var q = new URLSearchParams(window.location.search).get('sixty_base');
|
||||||
|
if (q) {
|
||||||
|
base = decodeURIComponent(q);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
base = String(base).replace(/\/+$/, '');
|
||||||
|
window.__SIXTY_API_BASE__ = base || fallback;
|
||||||
|
})();
|
||||||
105
InfoGenie-frontend/public/60sapi/周期资讯/AI资讯快报.html
Normal file
105
InfoGenie-frontend/public/60sapi/周期资讯/AI资讯快报.html
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>AI资讯快报</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}
|
||||||
|
.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px}
|
||||||
|
.date{display:flex;align-items:center;gap:6px;font-size:13px;color:#6b7280;padding-bottom:12px;border-bottom:1px solid #e5e7eb;margin-bottom:16px}
|
||||||
|
.item{display:flex;gap:10px;padding:11px 0;border-bottom:1px solid #f3f4f6}.item:last-child{border-bottom:none}
|
||||||
|
.num{flex-shrink:0;width:24px;height:24px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;margin-top:1px;color:#9ca3af;background:#f3f4f6}
|
||||||
|
.num.t1{color:#fff;background:#ef4444}.num.t2{color:#fff;background:#f97316}.num.t3{color:#fff;background:#eab308}
|
||||||
|
.text{flex:1;font-size:14px;line-height:1.7}
|
||||||
|
.tip{margin-top:20px;padding:14px 16px;border-radius:12px;font-size:13px;line-height:1.6;color:#6b7280;background:#f0fdf4;border:1px solid rgba(34,197,94,.1);font-style:italic}
|
||||||
|
.cover{margin-top:16px;text-align:center}.cover img{max-width:100%;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,.08)}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px}
|
||||||
|
@media(max-width:640px){.body{padding:16px 12px 32px}.text{font-size:13px}.header h1{font-size:14px}}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>🤖 AI资讯快报</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const API=window.__SIXTY_API_BASE__+'/v2/ai-news?encoding=json';
|
||||||
|
function escapeHtml(s){
|
||||||
|
if(s==null)return'';
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
function pickNewsList(d){
|
||||||
|
if(!d||typeof d!=='object')return[];
|
||||||
|
if(Array.isArray(d))return d;
|
||||||
|
const keys=['news','items','list','data','content'];
|
||||||
|
for(const k of keys){
|
||||||
|
const v=d[k];
|
||||||
|
if(Array.isArray(v))return v;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
function renderNewsItem(t,i){
|
||||||
|
const cls=i===0?'t1':i===1?'t2':i===2?'t3':'';
|
||||||
|
if(typeof t==='string'){
|
||||||
|
return `<div class="item"><div class="num ${cls}">${i+1}</div><div class="text">${escapeHtml(t)}</div></div>`;
|
||||||
|
}
|
||||||
|
if(t&&typeof t==='object'){
|
||||||
|
const title=t.title||t.name||'';
|
||||||
|
const link=t.link||t.url||'';
|
||||||
|
const detail=(t.detail||t.desc||t.description||'').trim();
|
||||||
|
const source=t.source||'';
|
||||||
|
const short=detail.length>220?detail.slice(0,220)+'…':detail;
|
||||||
|
let titleHtml=escapeHtml(title);
|
||||||
|
if(link)titleHtml=`<a href="${escapeHtml(link)}" target="_blank" rel="noopener" style="color:#059669;font-weight:600;text-decoration:none">${titleHtml} ↗</a>`;
|
||||||
|
let body=`<div class="text"><div style="margin-bottom:6px">${titleHtml}</div>`;
|
||||||
|
if(short)body+=`<div style="font-size:13px;color:#6b7280;line-height:1.65">${escapeHtml(short)}</div>`;
|
||||||
|
if(source)body+=`<div style="font-size:12px;color:#9ca3af;margin-top:6px">来源 · ${escapeHtml(source)}</div>`;
|
||||||
|
body+='</div>';
|
||||||
|
return `<div class="item"><div class="num ${cls}">${i+1}</div>${body}</div>`;
|
||||||
|
}
|
||||||
|
return `<div class="item"><div class="num ${cls}">${i+1}</div><div class="text">${escapeHtml(String(t))}</div></div>`;
|
||||||
|
}
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(API,{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
if(json.code!=null&&json.code!==200&&json.code!==0){
|
||||||
|
throw new Error(json.message||('接口 code '+json.code));
|
||||||
|
}
|
||||||
|
const d=json.data||json;
|
||||||
|
let html='';
|
||||||
|
if(d.date)html+=`<div class="date">🕐 ${escapeHtml(d.date)} ${escapeHtml(d.day_of_week||'')}</div>`;
|
||||||
|
const news=pickNewsList(d);
|
||||||
|
if(news.length===0){
|
||||||
|
html+=`<div class="tip" style="margin-top:0;font-style:normal">
|
||||||
|
<strong>当日暂无 AI 资讯条目</strong><br><br>
|
||||||
|
官方说明:数据源并非每日更新,重大资讯也可能集中在晚间。建议 <strong>22:00 后</strong> 再试,或在接口中传入 <code style="background:#fff;padding:2px 6px;border-radius:6px">?date=2025-11-11</code> 查看历史日期。
|
||||||
|
</div>`;
|
||||||
|
}else{
|
||||||
|
news.forEach((t,i)=>{html+=renderNewsItem(t,i);});
|
||||||
|
}
|
||||||
|
if(d.tip)html+=`<div class="tip">💡 ${escapeHtml(d.tip)}</div>`;
|
||||||
|
if(d.image)html+=`<div class="cover"><img src="${escapeHtml(d.image)}" alt="封面" loading="lazy"></div>`;
|
||||||
|
el.innerHTML=html||'<div class="err">暂无数据</div>';
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${escapeHtml(e.message)}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
111
InfoGenie-frontend/public/60sapi/周期资讯/Epic免费游戏.html
Normal file
111
InfoGenie-frontend/public/60sapi/周期资讯/Epic免费游戏.html
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Epic免费游戏</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px}
|
||||||
|
.rank-item{display:flex;align-items:stretch;gap:12px;padding:12px 14px;margin-bottom:6px;border-radius:12px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.04);transition:all .15s}
|
||||||
|
.rank-item:hover{box-shadow:0 3px 10px rgba(0,0,0,.08);transform:translateX(2px)}
|
||||||
|
.thumb-wrap{flex-shrink:0;width:120px;min-height:68px;border-radius:10px;overflow:hidden;background:#f3f4f6;align-self:center}
|
||||||
|
.thumb-wrap img{display:block;width:100%;height:100%;min-height:68px;object-fit:cover}
|
||||||
|
.rank-main{display:flex;align-items:flex-start;gap:10px;flex:1;min-width:0}
|
||||||
|
.badge{flex-shrink:0;width:30px;height:30px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800;color:#9ca3af;background:#f3f4f6}
|
||||||
|
.badge.t1{color:#fff;background:linear-gradient(135deg,#ef4444,#dc2626)}.badge.t2{color:#fff;background:linear-gradient(135deg,#f97316,#ea580c)}.badge.t3{color:#fff;background:linear-gradient(135deg,#eab308,#ca8a04)}
|
||||||
|
.rank-body{flex:1;min-width:0}
|
||||||
|
.rank-title{font-size:14px;font-weight:500;line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.rank-title a{color:inherit;text-decoration:none}.rank-title a:hover{color:#059669}
|
||||||
|
.rank-meta{font-size:11px;color:#b0b0b0;margin-top:2px}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px}
|
||||||
|
@media(max-width:640px){.thumb-wrap{width:88px;min-height:50px}.thumb-wrap img{min-height:50px}}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>🎮 Epic免费游戏</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function safeHttpUrl(u){
|
||||||
|
if(!u||typeof u!=='string')return'';
|
||||||
|
const t=u.trim();
|
||||||
|
return /^https?:\/\//i.test(t)?t:'';
|
||||||
|
}
|
||||||
|
function escapeAttr(s){
|
||||||
|
return String(s).replace(/&/g,'&').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
function escapeHtml(s){
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
|
}
|
||||||
|
/** 从 Epic / 60s 多种字段里取封面图 */
|
||||||
|
function pickEpicCover(item){
|
||||||
|
if(!item)return'';
|
||||||
|
const tryKeys=['poster','cover','coverUrl','image','imageUrl','thumbnail','icon','logo'];
|
||||||
|
for(const k of tryKeys){
|
||||||
|
const v=item[k];
|
||||||
|
if(typeof v==='string'&&safeHttpUrl(v))return safeHttpUrl(v);
|
||||||
|
}
|
||||||
|
const imgs=item.keyImages||item.images;
|
||||||
|
if(Array.isArray(imgs)&&imgs.length){
|
||||||
|
const wide=imgs.find(function(k){return k&&(/wide|landscape|offerimagewide|diesel/i.test(String(k.type||'')));});
|
||||||
|
const first=imgs[0];
|
||||||
|
const url=(wide&&wide.url)||(first&&first.url)||'';
|
||||||
|
return safeHttpUrl(url);
|
||||||
|
}
|
||||||
|
return'';
|
||||||
|
}
|
||||||
|
function pickEpicList(d){
|
||||||
|
if(Array.isArray(d))return d;
|
||||||
|
if(!d||typeof d!=='object')return[];
|
||||||
|
const keys=['games','freeGames','items','list','data','elements'];
|
||||||
|
for(let i=0;i<keys.length;i++){
|
||||||
|
const v=d[keys[i]];
|
||||||
|
if(Array.isArray(v))return v;
|
||||||
|
}
|
||||||
|
return[];
|
||||||
|
}
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(window.__SIXTY_API_BASE__+'/v2/epic?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data||json;
|
||||||
|
const list=pickEpicList(d);
|
||||||
|
if(!list.length)throw new Error('暂无数据');
|
||||||
|
let html='';
|
||||||
|
list.slice(0,50).forEach((item,i)=>{
|
||||||
|
const title=item.title||item.name||String(item);
|
||||||
|
const link=item.link||item.url||item.storeUrl||'';
|
||||||
|
const hot=item.hot||item.hotScore||item.heat||item.score||'';
|
||||||
|
const desc=item.desc||item.description||item.subtitle||'';
|
||||||
|
const img=pickEpicCover(item);
|
||||||
|
const cls=i===0?'t1':i===1?'t2':i===2?'t3':'';
|
||||||
|
html+='<div class="rank-item">';
|
||||||
|
if(img)html+=`<div class="thumb-wrap"><img src="${escapeAttr(img)}" alt="" loading="lazy" decoding="async" onerror="this.style.visibility='hidden'"/></div>`;
|
||||||
|
html+='<div class="rank-main"><div class="badge '+cls+'">'+(i+1)+'</div><div class="rank-body">';
|
||||||
|
html+=`<div class="rank-title">${link?`<a href="${escapeAttr(link)}" target="_blank" rel="noopener">${escapeHtml(title)} ↗</a>`:escapeHtml(title)}</div>`;
|
||||||
|
if(desc)html+=`<div class="rank-meta">${escapeHtml(desc.length>120?desc.slice(0,120)+'…':desc)}</div>`;
|
||||||
|
if(hot)html+=`<div class="rank-meta">🔥 ${typeof hot==='number'?hot.toLocaleString():hot}</div>`;
|
||||||
|
html+='</div></div></div>';
|
||||||
|
});
|
||||||
|
el.innerHTML=html;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
64
InfoGenie-frontend/public/60sapi/周期资讯/农历信息.html
Normal file
64
InfoGenie-frontend/public/60sapi/周期资讯/农历信息.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>农历信息</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px}
|
||||||
|
.hero{background:linear-gradient(135deg,#312e81,#4c1d95);border-radius:20px;padding:28px 24px;margin-bottom:16px;color:#fff;text-align:center}
|
||||||
|
.big{font-size:26px;font-weight:800;text-shadow:0 2px 8px rgba(0,0,0,.2);margin-bottom:4px}
|
||||||
|
.sub{font-size:14px;opacity:.85;margin-bottom:12px}
|
||||||
|
.chips{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
|
||||||
|
.chip{background:rgba(255,255,255,.15);padding:4px 12px;border-radius:8px;font-size:12px}
|
||||||
|
.section{background:#fff;border-radius:14px;padding:16px;margin-bottom:10px;box-shadow:0 1px 3px rgba(0,0,0,.04)}
|
||||||
|
.section-title{font-size:13px;font-weight:700;color:#6b7280;margin-bottom:10px;display:flex;align-items:center;gap:6px}
|
||||||
|
.row{display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f9fafb}.row:last-child{border-bottom:none}
|
||||||
|
.rk{font-size:13px;color:#9ca3af}.rv{font-size:13px;color:#1f2937;font-weight:500}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>🌙 农历信息</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(window.__SIXTY_API_BASE__+'/v2/lunar?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data||json;
|
||||||
|
if(!d?.solar){el.innerHTML='<div class="err">暂无农历数据</div>';return}
|
||||||
|
const s=d.solar||{},l=d.lunar||{},z=d.zodiac||{},sc=d.sixty_cycle||{},st=d.stats||{},tb=d.taboo?.day||{};
|
||||||
|
let chips='';
|
||||||
|
if(d.constellation?.name)chips+=`<span class="chip">⭐ ${d.constellation.name}</span>`;
|
||||||
|
if(z.year)chips+=`<span class="chip">🐾 ${z.year}年</span>`;
|
||||||
|
if(d.phase?.name)chips+=`<span class="chip">🌙 ${d.phase.name}</span>`;
|
||||||
|
if(d.term?.stage?.name)chips+=`<span class="chip">🌿 ${d.term.stage.name}</span>`;
|
||||||
|
let html=`<div class="hero"><div class="big">${s.full||''} ${s.week_desc||''}</div><div class="sub">${l.year_desc||''} ${l.month_desc||''}${l.day_desc||''}</div><div class="chips">${chips}</div></div>`;
|
||||||
|
html+=`<div class="section"><div class="section-title">📅 日期信息</div><div class="row"><span class="rk">阳历</span><span class="rv">${s.full_with_time||s.full}</span></div><div class="row"><span class="rk">农历</span><span class="rv">${l.desc_short||l.month_desc+l.day_desc}</span></div><div class="row"><span class="rk">季节</span><span class="rv">${s.season_desc||''} (${s.season_name||''})</span></div>${s.is_leap_year!==undefined?`<div class="row"><span class="rk">闰年</span><span class="rv">${s.is_leap_year?'是':'否'}</span></div>`:''}</div>`;
|
||||||
|
if(sc.day)html+=`<div class="section"><div class="section-title">🔮 天干地支</div><div class="row"><span class="rk">年柱</span><span class="rv">${sc.year?.name||''}</span></div><div class="row"><span class="rk">月柱</span><span class="rv">${sc.month?.name||''}</span></div><div class="row"><span class="rk">日柱</span><span class="rv">${sc.day?.name||''}</span></div><div class="row"><span class="rk">时柱</span><span class="rv">${sc.hour?.name||''}</span></div></div>`;
|
||||||
|
if(st.percents_formatted)html+=`<div class="section"><div class="section-title">📊 时间进度</div><div class="row"><span class="rk">今年进度</span><span class="rv">${st.percents_formatted.year}</span></div><div class="row"><span class="rk">本月进度</span><span class="rv">${st.percents_formatted.month}</span></div><div class="row"><span class="rk">今天进度</span><span class="rv">${st.percents_formatted.day}</span></div><div class="row"><span class="rk">第几天</span><span class="rv">第 ${st.day_of_year} 天 / 第 ${st.week_of_year} 周</span></div></div>`;
|
||||||
|
if(tb.recommends||tb.avoids)html+=`<div class="section"><div class="section-title">📜 宜忌</div>${tb.recommends?`<div class="row"><span class="rk">宜</span><span class="rv" style="color:#16a34a">${tb.recommends.replace(/\./g,' · ')}</span></div>`:''}${tb.avoids?`<div class="row"><span class="rk">忌</span><span class="rv" style="color:#dc2626">${tb.avoids.replace(/\./g,' · ')}</span></div>`:''}</div>`;
|
||||||
|
el.innerHTML=html;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
64
InfoGenie-frontend/public/60sapi/周期资讯/历史上的今天.html
Normal file
64
InfoGenie-frontend/public/60sapi/周期资讯/历史上的今天.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>历史上的今天</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px}
|
||||||
|
.hero{background:linear-gradient(135deg,#92400e,#b45309);border-radius:16px;padding:20px;margin-bottom:16px;color:#fff;text-align:center}
|
||||||
|
.hero h2{font-size:22px;margin-bottom:4px}.hero p{font-size:13px;opacity:.8}
|
||||||
|
.card{background:#fff;border-radius:14px;padding:16px;margin-bottom:10px;box-shadow:0 1px 3px rgba(0,0,0,.04);border-left:3px solid #f59e0b}
|
||||||
|
.card.birth{border-left-color:#3b82f6}.card.death{border-left-color:#ef4444}
|
||||||
|
.year{display:inline-block;padding:2px 8px;border-radius:6px;font-size:12px;font-weight:700;margin-bottom:6px;background:#fef3c7;color:#92400e}
|
||||||
|
.card.birth .year{background:#dbeafe;color:#1e40af}.card.death .year{background:#fecaca;color:#991b1b}
|
||||||
|
.card-title{font-size:15px;font-weight:600;margin-bottom:6px;line-height:1.5}
|
||||||
|
.card-desc{font-size:13px;color:#6b7280;line-height:1.7}
|
||||||
|
.card-link{display:inline-flex;align-items:center;gap:4px;font-size:12px;color:#059669;margin-top:8px;text-decoration:none}
|
||||||
|
.card-link:hover{text-decoration:underline}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>📅 历史上的今天</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(window.__SIXTY_API_BASE__+'/v2/today-in-history?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data||json;
|
||||||
|
const items=d?.items||(Array.isArray(d)?d:[]);
|
||||||
|
if(!items.length)throw new Error('暂无历史数据');
|
||||||
|
let html=`<div class="hero"><h2>📅 历史上的今天</h2><p>${d?.month||''}月${d?.day||''}日 · 共 ${items.length} 条</p></div>`;
|
||||||
|
items.forEach(item=>{
|
||||||
|
const t=item.event_type||'event';
|
||||||
|
const label=t==='birth'?'出生':t==='death'?'逝世':'事件';
|
||||||
|
html+=`<div class="card ${t}"><div class="year">${item.year||''}年 · ${label}</div><div class="card-title">${item.title||''}</div>`;
|
||||||
|
if(item.description){const desc=item.description.length>200?item.description.slice(0,200)+'…':item.description;html+=`<div class="card-desc">${desc}</div>`}
|
||||||
|
if(item.link)html+=`<a class="card-link" href="${item.link}" target="_blank" rel="noopener">↗ 查看详情</a>`;
|
||||||
|
html+='</div>';
|
||||||
|
});
|
||||||
|
el.innerHTML=html;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
71
InfoGenie-frontend/public/60sapi/周期资讯/当日货币汇率.html
Normal file
71
InfoGenie-frontend/public/60sapi/周期资讯/当日货币汇率.html
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>当日货币汇率</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px}
|
||||||
|
.hero{background:linear-gradient(135deg,#065f46,#059669);border-radius:16px;padding:20px;margin-bottom:16px;color:#fff;text-align:center}
|
||||||
|
.hero h2{font-size:22px;margin-bottom:4px}.hero p{font-size:12px;opacity:.8}
|
||||||
|
.search{width:100%;padding:12px 16px;border:2px solid #e5e7eb;border-radius:12px;font-size:14px;font-family:inherit;margin-bottom:12px;transition:border-color .2s}
|
||||||
|
.search:focus{border-color:#4ade80;outline:none}
|
||||||
|
.table{border-radius:12px;overflow:hidden;border:1px solid #e5e7eb}
|
||||||
|
.row{display:flex;align-items:center;padding:12px 16px;border-bottom:1px solid #f3f4f6}
|
||||||
|
.row:last-child{border-bottom:none}.row:nth-child(even){background:#f9fafb}.row:hover{background:#f0fdf4}
|
||||||
|
.currency{flex:1;font-size:14px;font-weight:600}.rate{font-size:14px;color:#059669;font-weight:500;font-variant-numeric:tabular-nums}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px}
|
||||||
|
.hint{text-align:center;margin-top:12px;color:#9ca3af;font-size:12px}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>💱 当日货币汇率</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let allRates=[];let baseCode='CNY';
|
||||||
|
const common=['USD','EUR','GBP','JPY','KRW','HKD','TWD','SGD','AUD','CAD','CHF','RUB','THB','MYR','INR','VND'];
|
||||||
|
function renderTable(keyword){
|
||||||
|
let list=keyword?allRates.filter(r=>r.currency.toLowerCase().includes(keyword.toLowerCase())):
|
||||||
|
[...allRates.filter(r=>common.includes(r.currency)),...allRates.filter(r=>!common.includes(r.currency))];
|
||||||
|
let html='<div class="table">';
|
||||||
|
list.slice(0,80).forEach((r,i)=>{
|
||||||
|
html+=`<div class="row"><div class="currency">${r.currency}</div><div class="rate">1 ${baseCode} = ${Number(r.rate).toFixed(4)} ${r.currency}</div></div>`;
|
||||||
|
});
|
||||||
|
html+='</div>';
|
||||||
|
if(list.length>80)html+=`<div class="hint">共 ${list.length} 种货币,搜索查看更多</div>`;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(window.__SIXTY_API_BASE__+'/v2/exchange-rate?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data||json;
|
||||||
|
if(!d||!d.rates)throw new Error('暂无汇率数据');
|
||||||
|
allRates=d.rates;baseCode=d.base_code||'CNY';
|
||||||
|
let html=`<div class="hero"><h2>💱 ${baseCode} 汇率</h2><p>更新时间:${d.updated||'未知'}</p></div>`;
|
||||||
|
html+=`<input class="search" placeholder="搜索货币代码,如 USD、EUR..." oninput="filterRates(this.value)">`;
|
||||||
|
html+=`<div id="table-wrap">${renderTable('')}</div>`;
|
||||||
|
el.innerHTML=html;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
function filterRates(v){document.getElementById('table-wrap').innerHTML=renderTable(v)}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
52
InfoGenie-frontend/public/60sapi/周期资讯/必应每日壁纸.html
Normal file
52
InfoGenie-frontend/public/60sapi/周期资讯/必应每日壁纸.html
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>必应每日壁纸</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px;text-align:center}
|
||||||
|
.img-wrap{border-radius:16px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,.1)}
|
||||||
|
.img-wrap img{width:100%;display:block}
|
||||||
|
.title{margin-top:16px;font-size:16px;font-weight:600;color:#374151}
|
||||||
|
.desc{margin-top:6px;font-size:13px;color:#6b7280}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>🖼️ 必应每日壁纸</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const BASE=window.__SIXTY_API_BASE__;
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(BASE+'/v2/bing?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data||json;
|
||||||
|
let url=typeof d==='string'&&d.startsWith('http')?d:d?.url||d?.image||d?.cover||`${BASE}/v2/bing?encoding=image-proxy`;
|
||||||
|
let html=`<div class="img-wrap"><img src="${url}" alt="必应壁纸" loading="lazy"></div>`;
|
||||||
|
if(d?.title)html+=`<div class="title">${d.title}</div>`;
|
||||||
|
if(d?.copyright||d?.desc)html+=`<div class="desc">${d.copyright||d.desc}</div>`;
|
||||||
|
el.innerHTML=html;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
81
InfoGenie-frontend/public/60sapi/周期资讯/摸鱼日历.html
Normal file
81
InfoGenie-frontend/public/60sapi/周期资讯/摸鱼日历.html
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>摸鱼日历</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px}
|
||||||
|
.hero{background:linear-gradient(135deg,#0e7490,#06b6d4);border-radius:20px;padding:28px 24px;margin-bottom:16px;color:#fff;text-align:center}
|
||||||
|
.emoji{font-size:48px;margin-bottom:8px}
|
||||||
|
.title{font-size:20px;font-weight:700;margin-bottom:4px}
|
||||||
|
.sub{font-size:13px;opacity:.85}
|
||||||
|
.quote{background:rgba(255,255,255,.12);border-radius:12px;padding:14px 16px;margin-top:16px;font-size:14px;font-style:italic;line-height:1.7}
|
||||||
|
.section{background:#fff;border-radius:14px;padding:16px;margin-bottom:10px;box-shadow:0 1px 3px rgba(0,0,0,.04)}
|
||||||
|
.section-title{font-size:13px;font-weight:700;color:#6b7280;margin-bottom:10px;display:flex;align-items:center;gap:6px}
|
||||||
|
.progress-label{display:flex;justify-content:space-between;font-size:12px;color:#9ca3af;margin-bottom:4px;margin-top:10px}
|
||||||
|
.progress-bar{height:8px;background:#f3f4f6;border-radius:4px;overflow:hidden;margin-bottom:4px}
|
||||||
|
.progress-fill{height:100%;border-radius:4px;transition:width .6s ease}
|
||||||
|
.cd-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
|
||||||
|
.cd-item{border-radius:12px;padding:14px;text-align:center}
|
||||||
|
.cd-num{font-size:28px;font-weight:800;margin-bottom:2px}
|
||||||
|
.cd-label{font-size:12px;color:#6b7280}
|
||||||
|
.row{display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f9fafb}.row:last-child{border-bottom:none}
|
||||||
|
.rk{font-size:13px;color:#9ca3af}.rv{font-size:13px;color:#1f2937;font-weight:500}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px}
|
||||||
|
@media(max-width:480px){.cd-grid{grid-template-columns:1fr}}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>🐟 摸鱼日历</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(window.__SIXTY_API_BASE__+'/v2/moyu?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data||json;
|
||||||
|
if(!d){el.innerHTML='<div class="err">暂无数据</div>';return}
|
||||||
|
const dt=d.date||{},p=d.progress||{},cd=d.countdown||{},nh=d.nextHoliday||{},nw=d.nextWeekend||{};
|
||||||
|
let html=`<div class="hero"><div class="emoji">🐟</div><div class="title">摸鱼日历</div><div class="sub">${dt.gregorian||''} ${dt.weekday||''} · 农历${dt.lunar?.monthCN||''}${dt.lunar?.dayCN||''} · ${dt.lunar?.zodiac||''}年</div>${d.moyuQuote?`<div class="quote">"${d.moyuQuote}"</div>`:''}</div>`;
|
||||||
|
html+='<div class="section"><div class="section-title">⏳ 时间进度</div>';
|
||||||
|
if(p.week)html+=`<div class="progress-label"><span>本周进度</span><span>${p.week.percentage}%(${p.week.passed}/${p.week.total}天)</span></div><div class="progress-bar"><div class="progress-fill" style="width:${p.week.percentage}%;background:linear-gradient(90deg,#4ade80,#22c55e)"></div></div>`;
|
||||||
|
if(p.month)html+=`<div class="progress-label"><span>本月进度</span><span>${p.month.percentage}%(还剩${p.month.remaining}天)</span></div><div class="progress-bar"><div class="progress-fill" style="width:${p.month.percentage}%;background:linear-gradient(90deg,#60a5fa,#3b82f6)"></div></div>`;
|
||||||
|
if(p.year)html+=`<div class="progress-label"><span>本年进度</span><span>${p.year.percentage}%(还剩${p.year.remaining}天)</span></div><div class="progress-bar"><div class="progress-fill" style="width:${p.year.percentage}%;background:linear-gradient(90deg,#f472b6,#ec4899)"></div></div>`;
|
||||||
|
html+='</div>';
|
||||||
|
html+='<div class="section"><div class="section-title">🎯 倒计时</div><div class="cd-grid">';
|
||||||
|
html+=`<div class="cd-item" style="background:#f0fdf4"><div class="cd-num" style="color:#16a34a">${cd.toFriday??'-'}</div><div class="cd-label">距周五</div></div>`;
|
||||||
|
html+=`<div class="cd-item" style="background:#eff6ff"><div class="cd-num" style="color:#2563eb">${cd.toWeekEnd??'-'}</div><div class="cd-label">距周末</div></div>`;
|
||||||
|
if(nh.name)html+=`<div class="cd-item" style="background:#fef3c7"><div class="cd-num" style="color:#d97706">${nh.until??'-'}</div><div class="cd-label">距${nh.name}(${nh.duration}天假)</div></div>`;
|
||||||
|
html+=`<div class="cd-item" style="background:#fce7f3"><div class="cd-num" style="color:#db2777">${cd.toYearEnd??'-'}</div><div class="cd-label">距年末</div></div>`;
|
||||||
|
html+='</div></div>';
|
||||||
|
if(d.today){
|
||||||
|
html+='<div class="section"><div class="section-title">📋 今日状态</div>';
|
||||||
|
html+=`<div class="row"><span class="rk">今天是</span><span class="rv">${d.today.isWeekend?'🎉 周末':'💼 工作日'}${d.today.isHoliday&&d.today.holidayName?` · ${d.today.holidayName}`:''}</span></div>`;
|
||||||
|
if(nw.date)html+=`<div class="row"><span class="rk">下个周末</span><span class="rv">${nw.date} ${nw.weekday}(${nw.daysUntil}天后)</span></div>`;
|
||||||
|
if(nh.name)html+=`<div class="row"><span class="rk">下个假期</span><span class="rv">${nh.name} · ${nh.date}</span></div>`;
|
||||||
|
html+='</div>';
|
||||||
|
}
|
||||||
|
el.innerHTML=html;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
64
InfoGenie-frontend/public/60sapi/周期资讯/每天60s读懂世界.html
Normal file
64
InfoGenie-frontend/public/60sapi/周期资讯/每天60s读懂世界.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>每天60s读懂世界</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}
|
||||||
|
.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px}
|
||||||
|
.date{display:flex;align-items:center;gap:6px;font-size:13px;color:#6b7280;padding-bottom:12px;border-bottom:1px solid #e5e7eb;margin-bottom:16px}
|
||||||
|
.item{display:flex;gap:10px;padding:11px 0;border-bottom:1px solid #f3f4f6}
|
||||||
|
.item:last-child{border-bottom:none}
|
||||||
|
.num{flex-shrink:0;width:24px;height:24px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;margin-top:1px;color:#9ca3af;background:#f3f4f6}
|
||||||
|
.num.t1{color:#fff;background:#ef4444}.num.t2{color:#fff;background:#f97316}.num.t3{color:#fff;background:#eab308}
|
||||||
|
.text{flex:1;font-size:14px;line-height:1.7}
|
||||||
|
.tip{margin-top:20px;padding:14px 16px;border-radius:12px;font-size:13px;line-height:1.6;color:#6b7280;background:#f0fdf4;border:1px solid rgba(34,197,94,.1);font-style:italic}
|
||||||
|
.cover{margin-top:16px;text-align:center}.cover img{max-width:100%;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,.08)}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px}
|
||||||
|
@media(max-width:640px){.body{padding:16px 12px 32px}.text{font-size:13px}.header h1{font-size:14px}}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>🌍 每天60s读懂世界</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const API=window.__SIXTY_API_BASE__+'/v2/60s?encoding=json';
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(API,{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data||json;
|
||||||
|
let html='';
|
||||||
|
if(d.date)html+=`<div class="date">🕐 ${d.date} ${d.day_of_week||''}</div>`;
|
||||||
|
const news=d.news||[];
|
||||||
|
news.forEach((t,i)=>{
|
||||||
|
const cls=i===0?'t1':i===1?'t2':i===2?'t3':'';
|
||||||
|
html+=`<div class="item"><div class="num ${cls}">${i+1}</div><div class="text">${t}</div></div>`;
|
||||||
|
});
|
||||||
|
if(d.tip)html+=`<div class="tip">💡 ${d.tip}</div>`;
|
||||||
|
if(d.image)html+=`<div class="cover"><img src="${d.image}" alt="封面" loading="lazy"></div>`;
|
||||||
|
el.innerHTML=html||'<div class="err">暂无数据</div>';
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
56
InfoGenie-frontend/public/60sapi/娱乐消遣/JS趣味题.html
Normal file
56
InfoGenie-frontend/public/60sapi/娱乐消遣/JS趣味题.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>JS趣味题</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px;display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 68px)}
|
||||||
|
.quote{background:linear-gradient(135deg,#f0fdf4,#ecfdf5);border-radius:20px;padding:36px 28px;text-align:center;border:1px solid rgba(34,197,94,.12);position:relative;width:100%}
|
||||||
|
.quote::before{content:'"';position:absolute;top:10px;left:20px;font-size:60px;color:rgba(34,197,94,.15);font-family:serif;line-height:1}
|
||||||
|
.quote-text{font-size:18px;line-height:2;color:#374151;font-weight:500}
|
||||||
|
.quote-src{font-size:12px;color:#9ca3af;margin-top:16px}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px;width:100%}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px;width:100%}
|
||||||
|
@media(max-width:640px){.quote-text{font-size:15px}.quote{padding:28px 20px}}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>💻 JS趣味题</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(window.__SIXTY_API_BASE__+'/v2/awesome-js?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data!==undefined?json.data:json;
|
||||||
|
let text='',source='';
|
||||||
|
if(typeof d==='string'){text=d}
|
||||||
|
else if(d&&typeof d==='object'){
|
||||||
|
for(const k of['content','text','hitokoto','duanzi','joke','saying','answer','kfc','data']){if(typeof d[k]==='string'){text=d[k];break}}
|
||||||
|
if(!text){for(const v of Object.values(d)){if(typeof v==='string'&&v.length>5){text=v;break}}}
|
||||||
|
if(!text)text=JSON.stringify(d,null,2);
|
||||||
|
source=d.from||d.source||d.author||'';
|
||||||
|
}else{text=String(d??'')}
|
||||||
|
el.innerHTML=`<div class="quote"><div class="quote-text">${text}</div>${source?`<div class="quote-src">—— ${source}</div>`:''}</div>`;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
56
InfoGenie-frontend/public/60sapi/娱乐消遣/今日运势.html
Normal file
56
InfoGenie-frontend/public/60sapi/娱乐消遣/今日运势.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>今日运势</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px;display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 68px)}
|
||||||
|
.quote{background:linear-gradient(135deg,#f0fdf4,#ecfdf5);border-radius:20px;padding:36px 28px;text-align:center;border:1px solid rgba(34,197,94,.12);position:relative;width:100%}
|
||||||
|
.quote::before{content:'"';position:absolute;top:10px;left:20px;font-size:60px;color:rgba(34,197,94,.15);font-family:serif;line-height:1}
|
||||||
|
.quote-text{font-size:18px;line-height:2;color:#374151;font-weight:500}
|
||||||
|
.quote-src{font-size:12px;color:#9ca3af;margin-top:16px}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px;width:100%}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px;width:100%}
|
||||||
|
@media(max-width:640px){.quote-text{font-size:15px}.quote{padding:28px 20px}}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>⭐ 今日运势</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(window.__SIXTY_API_BASE__+'/v2/luck?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data!==undefined?json.data:json;
|
||||||
|
let text='',source='';
|
||||||
|
if(typeof d==='string'){text=d}
|
||||||
|
else if(d&&typeof d==='object'){
|
||||||
|
for(const k of['content','text','hitokoto','duanzi','joke','saying','answer','kfc','data']){if(typeof d[k]==='string'){text=d[k];break}}
|
||||||
|
if(!text){for(const v of Object.values(d)){if(typeof v==='string'&&v.length>5){text=v;break}}}
|
||||||
|
if(!text)text=JSON.stringify(d,null,2);
|
||||||
|
source=d.from||d.source||d.author||'';
|
||||||
|
}else{text=String(d??'')}
|
||||||
|
el.innerHTML=`<div class="quote"><div class="quote-text">${text}</div>${source?`<div class="quote-src">—— ${source}</div>`:''}</div>`;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
61
InfoGenie-frontend/public/60sapi/娱乐消遣/发病文学.html
Normal file
61
InfoGenie-frontend/public/60sapi/娱乐消遣/发病文学.html
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>发病文学</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px}
|
||||||
|
.form{display:flex;flex-direction:column;gap:10px;margin-bottom:20px;background:#fff;border-radius:16px;padding:20px;box-shadow:0 1px 4px rgba(0,0,0,.05)}
|
||||||
|
.form label{font-size:12px;font-weight:600;color:#6b7280}
|
||||||
|
.form input{padding:11px 14px;border:2px solid #e5e7eb;border-radius:10px;font-size:13px;font-family:inherit;transition:border-color .2s}
|
||||||
|
.form input:focus{border-color:#4ade80;outline:none}
|
||||||
|
.form button{display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:11px 22px;border:none;border-radius:10px;background:linear-gradient(135deg,#4ade80,#22c55e);color:#fff;font-size:13px;font-weight:600;cursor:pointer;transition:all .2s}
|
||||||
|
.form button:hover{transform:translateY(-1px)}.form button:disabled{opacity:.5;cursor:not-allowed;transform:none}
|
||||||
|
.quote{background:linear-gradient(135deg,#f0fdf4,#ecfdf5);border-radius:20px;padding:36px 28px;text-align:center;border:1px solid rgba(34,197,94,.12);position:relative}
|
||||||
|
.quote::before{content:'"';position:absolute;top:10px;left:20px;font-size:60px;color:rgba(34,197,94,.15);font-family:serif;line-height:1}
|
||||||
|
.quote-text{font-size:16px;line-height:2;color:#374151;font-weight:500}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:40px 0;color:#9ca3af;gap:10px;font-size:13px}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:20px;color:#ef4444;font-size:14px}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>📖 发病文学</h1>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="form">
|
||||||
|
<label>对象名</label>
|
||||||
|
<input id="nameInput" placeholder="输入名字" onkeydown="if(event.key==='Enter')query()">
|
||||||
|
<button onclick="query()" id="submitBtn">🔍 生成</button>
|
||||||
|
</div>
|
||||||
|
<div id="result"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function query(){
|
||||||
|
const name=document.getElementById('nameInput').value.trim();
|
||||||
|
if(!name){document.getElementById('result').innerHTML='<div class="err">请输入名字</div>';return}
|
||||||
|
const el=document.getElementById('result');
|
||||||
|
const btn=document.getElementById('submitBtn');
|
||||||
|
btn.disabled=true;btn.textContent='生成中...';
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>生成中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(`${window.__SIXTY_API_BASE__}/v2/fabing?encoding=json&name=${encodeURIComponent(name)}`,{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data!==undefined?json.data:json;
|
||||||
|
const text=typeof d==='string'?d:d?.content||d?.text||JSON.stringify(d);
|
||||||
|
el.innerHTML=`<div class="quote"><div class="quote-text">${text}</div></div>`;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">生成失败:${e.message}</div>`}
|
||||||
|
finally{btn.disabled=false;btn.textContent='🔍 生成'}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
56
InfoGenie-frontend/public/60sapi/娱乐消遣/疯狂星期四.html
Normal file
56
InfoGenie-frontend/public/60sapi/娱乐消遣/疯狂星期四.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>疯狂星期四</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px;display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 68px)}
|
||||||
|
.quote{background:linear-gradient(135deg,#f0fdf4,#ecfdf5);border-radius:20px;padding:36px 28px;text-align:center;border:1px solid rgba(34,197,94,.12);position:relative;width:100%}
|
||||||
|
.quote::before{content:'"';position:absolute;top:10px;left:20px;font-size:60px;color:rgba(34,197,94,.15);font-family:serif;line-height:1}
|
||||||
|
.quote-text{font-size:18px;line-height:2;color:#374151;font-weight:500}
|
||||||
|
.quote-src{font-size:12px;color:#9ca3af;margin-top:16px}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px;width:100%}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px;width:100%}
|
||||||
|
@media(max-width:640px){.quote-text{font-size:15px}.quote{padding:28px 20px}}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>🍗 疯狂星期四</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(window.__SIXTY_API_BASE__+'/v2/kfc?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data!==undefined?json.data:json;
|
||||||
|
let text='',source='';
|
||||||
|
if(typeof d==='string'){text=d}
|
||||||
|
else if(d&&typeof d==='object'){
|
||||||
|
for(const k of['content','text','hitokoto','duanzi','joke','saying','answer','kfc','data']){if(typeof d[k]==='string'){text=d[k];break}}
|
||||||
|
if(!text){for(const v of Object.values(d)){if(typeof v==='string'&&v.length>5){text=v;break}}}
|
||||||
|
if(!text)text=JSON.stringify(d,null,2);
|
||||||
|
source=d.from||d.source||d.author||'';
|
||||||
|
}else{text=String(d??'')}
|
||||||
|
el.innerHTML=`<div class="quote"><div class="quote-text">${text}</div>${source?`<div class="quote-src">—— ${source}</div>`:''}</div>`;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
56
InfoGenie-frontend/public/60sapi/娱乐消遣/答案之书.html
Normal file
56
InfoGenie-frontend/public/60sapi/娱乐消遣/答案之书.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>答案之书</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px;display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 68px)}
|
||||||
|
.quote{background:linear-gradient(135deg,#f0fdf4,#ecfdf5);border-radius:20px;padding:36px 28px;text-align:center;border:1px solid rgba(34,197,94,.12);position:relative;width:100%}
|
||||||
|
.quote::before{content:'"';position:absolute;top:10px;left:20px;font-size:60px;color:rgba(34,197,94,.15);font-family:serif;line-height:1}
|
||||||
|
.quote-text{font-size:18px;line-height:2;color:#374151;font-weight:500}
|
||||||
|
.quote-src{font-size:12px;color:#9ca3af;margin-top:16px}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px;width:100%}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px;width:100%}
|
||||||
|
@media(max-width:640px){.quote-text{font-size:15px}.quote{padding:28px 20px}}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>📘 答案之书</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(window.__SIXTY_API_BASE__+'/v2/answer?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data!==undefined?json.data:json;
|
||||||
|
let text='',source='';
|
||||||
|
if(typeof d==='string'){text=d}
|
||||||
|
else if(d&&typeof d==='object'){
|
||||||
|
for(const k of['content','text','hitokoto','duanzi','joke','saying','answer','kfc','data']){if(typeof d[k]==='string'){text=d[k];break}}
|
||||||
|
if(!text){for(const v of Object.values(d)){if(typeof v==='string'&&v.length>5){text=v;break}}}
|
||||||
|
if(!text)text=JSON.stringify(d,null,2);
|
||||||
|
source=d.from||d.source||d.author||'';
|
||||||
|
}else{text=String(d??'')}
|
||||||
|
el.innerHTML=`<div class="quote"><div class="quote-text">${text}</div>${source?`<div class="quote-src">—— ${source}</div>`:''}</div>`;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
56
InfoGenie-frontend/public/60sapi/娱乐消遣/英文冷笑话.html
Normal file
56
InfoGenie-frontend/public/60sapi/娱乐消遣/英文冷笑话.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>英文冷笑话</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px;display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 68px)}
|
||||||
|
.quote{background:linear-gradient(135deg,#f0fdf4,#ecfdf5);border-radius:20px;padding:36px 28px;text-align:center;border:1px solid rgba(34,197,94,.12);position:relative;width:100%}
|
||||||
|
.quote::before{content:'"';position:absolute;top:10px;left:20px;font-size:60px;color:rgba(34,197,94,.15);font-family:serif;line-height:1}
|
||||||
|
.quote-text{font-size:18px;line-height:2;color:#374151;font-weight:500}
|
||||||
|
.quote-src{font-size:12px;color:#9ca3af;margin-top:16px}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px;width:100%}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px;width:100%}
|
||||||
|
@media(max-width:640px){.quote-text{font-size:15px}.quote{padding:28px 20px}}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>😄 英文冷笑话</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(window.__SIXTY_API_BASE__+'/v2/dad-joke?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data!==undefined?json.data:json;
|
||||||
|
let text='',source='';
|
||||||
|
if(typeof d==='string'){text=d}
|
||||||
|
else if(d&&typeof d==='object'){
|
||||||
|
for(const k of['content','text','hitokoto','duanzi','joke','saying','answer','kfc','data']){if(typeof d[k]==='string'){text=d[k];break}}
|
||||||
|
if(!text){for(const v of Object.values(d)){if(typeof v==='string'&&v.length>5){text=v;break}}}
|
||||||
|
if(!text)text=JSON.stringify(d,null,2);
|
||||||
|
source=d.from||d.source||d.author||'';
|
||||||
|
}else{text=String(d??'')}
|
||||||
|
el.innerHTML=`<div class="quote"><div class="quote-text">${text}</div>${source?`<div class="quote-src">—— ${source}</div>`:''}</div>`;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
/* 背景样式文件 */
|
|
||||||
|
|
||||||
/* 主体背景 */
|
|
||||||
body {
|
|
||||||
background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 50%, #a5d6a7 100%);
|
|
||||||
background-attachment: fixed;
|
|
||||||
background-size: 400% 400%;
|
|
||||||
animation: gradientShift 15s ease infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 背景动画 */
|
|
||||||
@keyframes gradientShift {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 装饰性背景元素 */
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-image:
|
|
||||||
radial-gradient(circle at 20% 80%, rgba(76, 175, 80, 0.1) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 80% 20%, rgba(129, 199, 132, 0.1) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 40% 40%, rgba(165, 214, 167, 0.08) 0%, transparent 50%);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 浮动装饰圆点 */
|
|
||||||
body::after {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-image:
|
|
||||||
radial-gradient(2px 2px at 20px 30px, rgba(76, 175, 80, 0.3), transparent),
|
|
||||||
radial-gradient(2px 2px at 40px 70px, rgba(129, 199, 132, 0.2), transparent),
|
|
||||||
radial-gradient(1px 1px at 90px 40px, rgba(165, 214, 167, 0.3), transparent),
|
|
||||||
radial-gradient(1px 1px at 130px 80px, rgba(76, 175, 80, 0.2), transparent),
|
|
||||||
radial-gradient(2px 2px at 160px 30px, rgba(129, 199, 132, 0.3), transparent);
|
|
||||||
background-repeat: repeat;
|
|
||||||
background-size: 200px 100px;
|
|
||||||
animation: float 20s linear infinite;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: -1;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
0% {
|
|
||||||
transform: translateY(0px);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(0px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 题目容器背景增强 */
|
|
||||||
.question-container {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
box-shadow:
|
|
||||||
0 8px 32px rgba(26, 77, 26, 0.1),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 错误容器背景 */
|
|
||||||
.error-container {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 结果容器背景 */
|
|
||||||
.result-container {
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
-webkit-backdrop-filter: blur(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 代码块背景 */
|
|
||||||
.code-block {
|
|
||||||
background: rgba(248, 249, 250, 0.9);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
-webkit-backdrop-filter: blur(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 选项背景 */
|
|
||||||
.option {
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
-webkit-backdrop-filter: blur(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option:hover {
|
|
||||||
background: rgba(76, 175, 80, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option.selected {
|
|
||||||
background: linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(76, 175, 80, 0.08));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 按钮背景增强 */
|
|
||||||
.submit-btn {
|
|
||||||
background: linear-gradient(135deg, #4caf50, #45a049);
|
|
||||||
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-answer-btn {
|
|
||||||
background: linear-gradient(135deg, #2196f3, #1976d2);
|
|
||||||
box-shadow: 0 4px 15px rgba(33, 150, 243, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-btn {
|
|
||||||
background: linear-gradient(135deg, #ff9800, #f57c00);
|
|
||||||
box-shadow: 0 4px 15px rgba(255, 152, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn {
|
|
||||||
background: linear-gradient(135deg, #4caf50, #45a049);
|
|
||||||
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移动端背景优化 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
body {
|
|
||||||
background-attachment: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::after {
|
|
||||||
opacity: 0.4;
|
|
||||||
background-size: 150px 75px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-container {
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
-webkit-backdrop-filter: blur(8px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 高对比度模式支持 */
|
|
||||||
@media (prefers-contrast: high) {
|
|
||||||
body {
|
|
||||||
background: #f0f8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::before,
|
|
||||||
body::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-container {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 2px solid #4caf50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 减少动画模式支持 */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
body {
|
|
||||||
animation: none;
|
|
||||||
background: #e8f5e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::after {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn:hover {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,597 +0,0 @@
|
|||||||
/* 基础样式重置 */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #2d5a27;
|
|
||||||
min-height: 100vh;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 容器布局 */
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 头部样式 */
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
padding: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
color: #1a4d1a;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
text-shadow: 0 2px 4px rgba(26, 77, 26, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: #4a7c59;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主内容区域 */
|
|
||||||
.main-content {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 加载动画 */
|
|
||||||
.loading {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border: 4px solid #e8f5e8;
|
|
||||||
border-top: 4px solid #4caf50;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin: 0 auto 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading p {
|
|
||||||
color: #4a7c59;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 题目容器 */
|
|
||||||
.question-container {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 40px;
|
|
||||||
box-shadow: 0 10px 30px rgba(26, 77, 26, 0.1);
|
|
||||||
border: 2px solid rgba(76, 175, 80, 0.2);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 题目头部 */
|
|
||||||
.question-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
border-bottom: 2px solid #e8f5e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-id {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: #4caf50;
|
|
||||||
font-weight: 600;
|
|
||||||
background: linear-gradient(135deg, #e8f5e8, #c8e6c9);
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn {
|
|
||||||
background: linear-gradient(135deg, #4caf50, #45a049);
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 45px;
|
|
||||||
height: 45px;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn:hover {
|
|
||||||
transform: rotate(180deg) scale(1.1);
|
|
||||||
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 题目文本 */
|
|
||||||
.question-text h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: #1a4d1a;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 代码块 */
|
|
||||||
.code-block {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 2px solid #e8f5e8;
|
|
||||||
border-radius: 12px;
|
|
||||||
margin: 25px 0;
|
|
||||||
overflow-x: auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-block pre {
|
|
||||||
margin: 0;
|
|
||||||
padding: 25px;
|
|
||||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #2d5a27;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-block code {
|
|
||||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 代码高亮自定义样式 - 丰富的语法高亮 */
|
|
||||||
.code-block .hljs {
|
|
||||||
background: transparent !important;
|
|
||||||
color: #333333 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* JavaScript 关键字 - 蓝色 */
|
|
||||||
.code-block .hljs-keyword {
|
|
||||||
color: #0066cc !important;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 字符串 - 绿色 */
|
|
||||||
.code-block .hljs-string {
|
|
||||||
color: #22aa22 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 数字 - 橙色 */
|
|
||||||
.code-block .hljs-number {
|
|
||||||
color: #ff6600 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 函数名 - 紫色 */
|
|
||||||
.code-block .hljs-function,
|
|
||||||
.code-block .hljs-title.function_ {
|
|
||||||
color: #9933cc !important;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 变量名 - 深蓝色 */
|
|
||||||
.code-block .hljs-variable,
|
|
||||||
.code-block .hljs-name {
|
|
||||||
color: #0066aa !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 注释 - 灰色 */
|
|
||||||
.code-block .hljs-comment {
|
|
||||||
color: #888888 !important;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 内置对象和方法 - 深紫色 */
|
|
||||||
.code-block .hljs-built_in {
|
|
||||||
color: #663399 !important;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 字面量 (true, false, null) - 红色 */
|
|
||||||
.code-block .hljs-literal {
|
|
||||||
color: #cc0000 !important;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 操作符 - 深灰色 */
|
|
||||||
.code-block .hljs-operator {
|
|
||||||
color: #666666 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 标点符号 - 深灰色 */
|
|
||||||
.code-block .hljs-punctuation {
|
|
||||||
color: #666666 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 属性名 - 深蓝色 */
|
|
||||||
.code-block .hljs-property,
|
|
||||||
.code-block .hljs-attr {
|
|
||||||
color: #0066aa !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 类名和构造函数 - 深绿色 */
|
|
||||||
.code-block .hljs-title.class_,
|
|
||||||
.code-block .hljs-title {
|
|
||||||
color: #228833 !important;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 参数 - 深蓝色 */
|
|
||||||
.code-block .hljs-params {
|
|
||||||
color: #0066aa !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 正则表达式 - 深红色 */
|
|
||||||
.code-block .hljs-regexp {
|
|
||||||
color: #aa0066 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 模板字符串 - 深绿色 */
|
|
||||||
.code-block .hljs-template-variable,
|
|
||||||
.code-block .hljs-template-tag {
|
|
||||||
color: #228833 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 选项容器 */
|
|
||||||
.options-container {
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option {
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
border: 2px solid #e8f5e8;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 15px 20px;
|
|
||||||
margin: 12px 0;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-size: 1rem;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option:hover {
|
|
||||||
border-color: #4caf50;
|
|
||||||
background: rgba(76, 175, 80, 0.05);
|
|
||||||
transform: translateX(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option.selected {
|
|
||||||
border-color: #4caf50;
|
|
||||||
background: linear-gradient(135deg, rgba(76, 175, 80, 0.1), rgba(76, 175, 80, 0.05));
|
|
||||||
color: #1a4d1a;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option.correct {
|
|
||||||
border-color: #4caf50;
|
|
||||||
background: linear-gradient(135deg, rgba(76, 175, 80, 0.2), rgba(76, 175, 80, 0.1));
|
|
||||||
color: #1a4d1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option.incorrect {
|
|
||||||
border-color: #f44336;
|
|
||||||
background: linear-gradient(135deg, rgba(244, 67, 54, 0.1), rgba(244, 67, 54, 0.05));
|
|
||||||
color: #c62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 按钮样式 */
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
margin: 30px 0;
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn, .show-answer-btn, .retry-btn, .export-btn {
|
|
||||||
padding: 12px 30px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 25px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
min-width: 120px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn {
|
|
||||||
background: linear-gradient(135deg, #4caf50, #45a049);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn:hover:not(:disabled) {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn:disabled {
|
|
||||||
background: #cccccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-answer-btn {
|
|
||||||
background: linear-gradient(135deg, #2196f3, #1976d2);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-answer-btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 15px rgba(33, 150, 243, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-btn {
|
|
||||||
background: linear-gradient(135deg, #ff9800, #f57c00);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 15px rgba(255, 152, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.export-btn {
|
|
||||||
background: linear-gradient(135deg, #9c27b0, #7b1fa2);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.export-btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 15px rgba(156, 39, 176, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.export-btn svg {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 结果容器 */
|
|
||||||
.result-container {
|
|
||||||
margin-top: 30px;
|
|
||||||
padding: 25px;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
border-radius: 15px;
|
|
||||||
border: 2px solid #e8f5e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
border-bottom: 1px solid #e8f5e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-status {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-status.correct {
|
|
||||||
color: #4caf50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-status.incorrect {
|
|
||||||
color: #f44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
.correct-answer {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #4caf50;
|
|
||||||
background: rgba(76, 175, 80, 0.1);
|
|
||||||
padding: 5px 12px;
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.explanation {
|
|
||||||
color: #2d5a27;
|
|
||||||
line-height: 1.7;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.explanation pre {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 1px solid #e8f5e8;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 15px;
|
|
||||||
margin: 15px 0;
|
|
||||||
overflow-x: auto;
|
|
||||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 错误容器 */
|
|
||||||
.error-container {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 2px solid rgba(244, 67, 54, 0.2);
|
|
||||||
max-width: 500px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-icon {
|
|
||||||
font-size: 3rem;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-container h3 {
|
|
||||||
color: #f44336;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-container p {
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 底部 */
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
padding: 30px 0;
|
|
||||||
margin-top: 40px;
|
|
||||||
color: #4a7c59;
|
|
||||||
opacity: 0.7;
|
|
||||||
border-top: 1px solid rgba(76, 175, 80, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 平板端适配 (768px - 1024px) */
|
|
||||||
@media (max-width: 1024px) and (min-width: 768px) {
|
|
||||||
.container {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 2.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-container {
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 手机端适配 (最大768px) */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.container {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
margin-bottom: 25px;
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-container {
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-text h2 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-block pre {
|
|
||||||
padding: 15px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option {
|
|
||||||
padding: 12px 15px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn, .show-answer-btn, .retry-btn {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.explanation {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.explanation pre {
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 小屏手机适配 (最大480px) */
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.container {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-container {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-id {
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 6px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-block pre {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option {
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn, .show-answer-btn, .retry-btn {
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>随机JavaScript趣味题</title>
|
|
||||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
|
||||||
<link rel="dns-prefetch" href="https://60s.api.shumengya.top">
|
|
||||||
<link rel="stylesheet" href="css/style.css">
|
|
||||||
<link rel="stylesheet" href="css/background.css">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<header class="header">
|
|
||||||
<h1>JavaScript趣味题</h1>
|
|
||||||
<p class="subtitle">测试你的JavaScript知识</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="main-content">
|
|
||||||
<div class="loading" id="loading">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<p>正在加载题目...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="question-container" id="questionContainer" style="display: none;">
|
|
||||||
<div class="question-header">
|
|
||||||
<span class="question-id" id="questionId">题目 #1</span>
|
|
||||||
<button class="refresh-btn" id="refreshBtn" title="获取新题目">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<polyline points="23 4 23 10 17 10"></polyline>
|
|
||||||
<polyline points="1 20 1 14 7 14"></polyline>
|
|
||||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="question-text" id="questionText">
|
|
||||||
<h2>输出是什么?</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="code-block" id="codeBlock">
|
|
||||||
<pre><code id="codeContent" class="language-javascript"></code></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="options-container" id="optionsContainer">
|
|
||||||
<!-- 选项将通过JavaScript动态生成 -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="action-buttons">
|
|
||||||
<button class="submit-btn" id="submitBtn" disabled>提交答案</button>
|
|
||||||
<button class="show-answer-btn" id="showAnswerBtn">查看答案</button>
|
|
||||||
<button class="export-btn" id="exportBtn" title="导出为Markdown文件">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
||||||
<polyline points="7 10 12 15 17 10"></polyline>
|
|
||||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
||||||
</svg>
|
|
||||||
导出MD
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="result-container" id="resultContainer" style="display: none;">
|
|
||||||
<div class="result-header">
|
|
||||||
<span class="result-status" id="resultStatus"></span>
|
|
||||||
<span class="correct-answer" id="correctAnswer"></span>
|
|
||||||
</div>
|
|
||||||
<div class="explanation" id="explanation">
|
|
||||||
<!-- 解析内容 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="error-container" id="errorContainer" style="display: none;">
|
|
||||||
<div class="error-icon">⚠️</div>
|
|
||||||
<h3>加载失败</h3>
|
|
||||||
<p id="errorMessage">网络连接异常,请稍后重试</p>
|
|
||||||
<button class="retry-btn" id="retryBtn">重新加载</button>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="footer">
|
|
||||||
<p>JavaScript趣味题集合</p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="js/script.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,582 +0,0 @@
|
|||||||
// JavaScript趣味题应用
|
|
||||||
class JSQuizApp {
|
|
||||||
constructor() {
|
|
||||||
this.apiEndpoints = [
|
|
||||||
'https://60s.api.shumengya.top',
|
|
||||||
];
|
|
||||||
this.currentApiIndex = 0;
|
|
||||||
this.currentQuestion = null;
|
|
||||||
this.selectedOption = null;
|
|
||||||
this.isAnswered = false;
|
|
||||||
this.loadStartTime = null;
|
|
||||||
|
|
||||||
this.initElements();
|
|
||||||
this.bindEvents();
|
|
||||||
this.preloadResources();
|
|
||||||
this.loadQuestion();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化DOM元素
|
|
||||||
initElements() {
|
|
||||||
this.elements = {
|
|
||||||
loading: document.getElementById('loading'),
|
|
||||||
questionContainer: document.getElementById('questionContainer'),
|
|
||||||
errorContainer: document.getElementById('errorContainer'),
|
|
||||||
questionId: document.getElementById('questionId'),
|
|
||||||
questionText: document.getElementById('questionText'),
|
|
||||||
codeContent: document.getElementById('codeContent'),
|
|
||||||
optionsContainer: document.getElementById('optionsContainer'),
|
|
||||||
submitBtn: document.getElementById('submitBtn'),
|
|
||||||
showAnswerBtn: document.getElementById('showAnswerBtn'),
|
|
||||||
refreshBtn: document.getElementById('refreshBtn'),
|
|
||||||
retryBtn: document.getElementById('retryBtn'),
|
|
||||||
exportBtn: document.getElementById('exportBtn'),
|
|
||||||
resultContainer: document.getElementById('resultContainer'),
|
|
||||||
resultStatus: document.getElementById('resultStatus'),
|
|
||||||
correctAnswer: document.getElementById('correctAnswer'),
|
|
||||||
explanation: document.getElementById('explanation'),
|
|
||||||
errorMessage: document.getElementById('errorMessage')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预加载资源
|
|
||||||
preloadResources() {
|
|
||||||
// 预连接API服务器
|
|
||||||
this.apiEndpoints.forEach(endpoint => {
|
|
||||||
const link = document.createElement('link');
|
|
||||||
link.rel = 'preconnect';
|
|
||||||
link.href = endpoint;
|
|
||||||
document.head.appendChild(link);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绑定事件
|
|
||||||
bindEvents() {
|
|
||||||
this.elements.submitBtn.addEventListener('click', () => this.submitAnswer());
|
|
||||||
this.elements.showAnswerBtn.addEventListener('click', () => this.showAnswer());
|
|
||||||
this.elements.refreshBtn.addEventListener('click', () => this.loadQuestion());
|
|
||||||
this.elements.retryBtn.addEventListener('click', () => this.loadQuestion());
|
|
||||||
this.elements.exportBtn.addEventListener('click', () => this.exportToMarkdown());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示加载状态
|
|
||||||
showLoading() {
|
|
||||||
this.elements.loading.style.display = 'block';
|
|
||||||
this.elements.questionContainer.style.display = 'none';
|
|
||||||
this.elements.errorContainer.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示题目
|
|
||||||
showQuestion() {
|
|
||||||
this.elements.loading.style.display = 'none';
|
|
||||||
this.elements.questionContainer.style.display = 'block';
|
|
||||||
this.elements.errorContainer.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示错误
|
|
||||||
showError(message) {
|
|
||||||
this.elements.loading.style.display = 'none';
|
|
||||||
this.elements.questionContainer.style.display = 'none';
|
|
||||||
this.elements.errorContainer.style.display = 'block';
|
|
||||||
this.elements.errorMessage.textContent = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前API地址
|
|
||||||
getCurrentApiUrl() {
|
|
||||||
return `${this.apiEndpoints[this.currentApiIndex]}/v2/awesome-js`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换到下一个API
|
|
||||||
switchToNextApi() {
|
|
||||||
this.currentApiIndex = (this.currentApiIndex + 1) % this.apiEndpoints.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载题目
|
|
||||||
async loadQuestion() {
|
|
||||||
this.loadStartTime = Date.now();
|
|
||||||
this.showLoading();
|
|
||||||
this.resetQuestion();
|
|
||||||
|
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = this.apiEndpoints.length;
|
|
||||||
|
|
||||||
while (attempts < maxAttempts) {
|
|
||||||
try {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
||||||
|
|
||||||
const response = await fetch(this.getCurrentApiUrl(), {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
signal: controller.signal
|
|
||||||
});
|
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.code === 200 && data.data) {
|
|
||||||
this.currentQuestion = data.data;
|
|
||||||
const loadTime = Date.now() - this.loadStartTime;
|
|
||||||
console.log(`题目加载完成,耗时: ${loadTime}ms`);
|
|
||||||
this.displayQuestion();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
throw new Error(data.message || '数据格式错误');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`API ${this.getCurrentApiUrl()} 请求失败:`, error.message);
|
|
||||||
attempts++;
|
|
||||||
|
|
||||||
if (attempts < maxAttempts) {
|
|
||||||
this.switchToNextApi();
|
|
||||||
console.log(`切换到备用API: ${this.getCurrentApiUrl()}`);
|
|
||||||
} else {
|
|
||||||
this.showError(`所有API接口都无法访问,请检查网络连接后重试。\n最后尝试的错误: ${error.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置题目状态
|
|
||||||
resetQuestion() {
|
|
||||||
this.selectedOption = null;
|
|
||||||
this.isAnswered = false;
|
|
||||||
this.elements.resultContainer.style.display = 'none';
|
|
||||||
this.elements.submitBtn.disabled = true;
|
|
||||||
this.elements.submitBtn.textContent = '提交答案';
|
|
||||||
this.elements.showAnswerBtn.style.display = 'inline-block';
|
|
||||||
|
|
||||||
// 清空选项容器,防止重复显示
|
|
||||||
this.elements.optionsContainer.innerHTML = '';
|
|
||||||
|
|
||||||
// 移除所有选项的事件监听器
|
|
||||||
const existingOptions = document.querySelectorAll('.option');
|
|
||||||
existingOptions.forEach(option => {
|
|
||||||
option.removeEventListener('click', this.selectOption);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示题目内容
|
|
||||||
displayQuestion() {
|
|
||||||
const question = this.currentQuestion;
|
|
||||||
|
|
||||||
console.log('显示题目:', question);
|
|
||||||
|
|
||||||
// 设置题目ID
|
|
||||||
this.elements.questionId.textContent = `题目 #${question.id}`;
|
|
||||||
|
|
||||||
// 设置题目文本
|
|
||||||
this.elements.questionText.innerHTML = `<h2>${this.escapeHtml(question.question)}</h2>`;
|
|
||||||
|
|
||||||
// 设置代码内容并应用语法高亮
|
|
||||||
this.elements.codeContent.textContent = question.code;
|
|
||||||
|
|
||||||
// 应用语法高亮
|
|
||||||
if (typeof hljs !== 'undefined') {
|
|
||||||
hljs.highlightElement(this.elements.codeContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保选项容器已清空
|
|
||||||
this.elements.optionsContainer.innerHTML = '';
|
|
||||||
|
|
||||||
// 生成选项
|
|
||||||
this.generateOptions(question.options);
|
|
||||||
|
|
||||||
this.showQuestion();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成选项
|
|
||||||
generateOptions(options) {
|
|
||||||
// 确保清空容器
|
|
||||||
this.elements.optionsContainer.innerHTML = '';
|
|
||||||
|
|
||||||
// 验证选项数据
|
|
||||||
if (!Array.isArray(options) || options.length === 0) {
|
|
||||||
console.error('选项数据无效:', options);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除可能存在的重复选项
|
|
||||||
const uniqueOptions = [...new Set(options)];
|
|
||||||
|
|
||||||
uniqueOptions.forEach((option, index) => {
|
|
||||||
const optionElement = document.createElement('div');
|
|
||||||
optionElement.className = 'option';
|
|
||||||
optionElement.textContent = option;
|
|
||||||
optionElement.dataset.index = index;
|
|
||||||
optionElement.dataset.value = option.charAt(0); // A, B, C, D
|
|
||||||
|
|
||||||
optionElement.addEventListener('click', () => this.selectOption(optionElement));
|
|
||||||
|
|
||||||
this.elements.optionsContainer.appendChild(optionElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('生成选项:', uniqueOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选择选项
|
|
||||||
selectOption(optionElement) {
|
|
||||||
if (this.isAnswered) return;
|
|
||||||
|
|
||||||
// 移除之前的选中状态
|
|
||||||
document.querySelectorAll('.option.selected').forEach(el => {
|
|
||||||
el.classList.remove('selected');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置当前选中
|
|
||||||
optionElement.classList.add('selected');
|
|
||||||
this.selectedOption = optionElement.dataset.value;
|
|
||||||
|
|
||||||
// 启用提交按钮
|
|
||||||
this.elements.submitBtn.disabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交答案
|
|
||||||
submitAnswer() {
|
|
||||||
if (!this.selectedOption || this.isAnswered) return;
|
|
||||||
|
|
||||||
this.isAnswered = true;
|
|
||||||
this.elements.submitBtn.disabled = true;
|
|
||||||
this.elements.submitBtn.textContent = '已提交';
|
|
||||||
this.elements.showAnswerBtn.style.display = 'none';
|
|
||||||
|
|
||||||
const isCorrect = this.selectedOption === this.currentQuestion.answer;
|
|
||||||
|
|
||||||
// 显示结果
|
|
||||||
this.showResult(isCorrect);
|
|
||||||
|
|
||||||
// 标记选项
|
|
||||||
this.markOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示答案
|
|
||||||
showAnswer() {
|
|
||||||
this.isAnswered = true;
|
|
||||||
this.elements.submitBtn.disabled = true;
|
|
||||||
this.elements.submitBtn.textContent = '已显示答案';
|
|
||||||
this.elements.showAnswerBtn.style.display = 'none';
|
|
||||||
|
|
||||||
// 显示结果(不判断对错)
|
|
||||||
this.showResult(null);
|
|
||||||
|
|
||||||
// 标记正确答案
|
|
||||||
this.markCorrectAnswer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示结果
|
|
||||||
showResult(isCorrect) {
|
|
||||||
const resultContainer = this.elements.resultContainer;
|
|
||||||
const resultStatus = this.elements.resultStatus;
|
|
||||||
const correctAnswer = this.elements.correctAnswer;
|
|
||||||
const explanation = this.elements.explanation;
|
|
||||||
|
|
||||||
// 设置结果状态
|
|
||||||
if (isCorrect === true) {
|
|
||||||
resultStatus.textContent = '✅ 回答正确!';
|
|
||||||
resultStatus.className = 'result-status correct';
|
|
||||||
} else if (isCorrect === false) {
|
|
||||||
resultStatus.textContent = '❌ 回答错误';
|
|
||||||
resultStatus.className = 'result-status incorrect';
|
|
||||||
} else {
|
|
||||||
resultStatus.textContent = '💡 答案解析';
|
|
||||||
resultStatus.className = 'result-status';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置正确答案
|
|
||||||
correctAnswer.textContent = `正确答案: ${this.currentQuestion.answer}`;
|
|
||||||
|
|
||||||
// 设置解析内容
|
|
||||||
explanation.innerHTML = this.formatExplanation(this.currentQuestion.explanation);
|
|
||||||
|
|
||||||
// 显示结果容器
|
|
||||||
resultContainer.style.display = 'block';
|
|
||||||
|
|
||||||
// 滚动到结果区域
|
|
||||||
setTimeout(() => {
|
|
||||||
resultContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标记选项
|
|
||||||
markOptions() {
|
|
||||||
const options = document.querySelectorAll('.option');
|
|
||||||
const correctAnswer = this.currentQuestion.answer;
|
|
||||||
|
|
||||||
options.forEach(option => {
|
|
||||||
const optionValue = option.dataset.value;
|
|
||||||
|
|
||||||
if (optionValue === correctAnswer) {
|
|
||||||
option.classList.add('correct');
|
|
||||||
} else if (option.classList.contains('selected')) {
|
|
||||||
option.classList.add('incorrect');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 禁用点击
|
|
||||||
option.style.pointerEvents = 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标记正确答案
|
|
||||||
markCorrectAnswer() {
|
|
||||||
const options = document.querySelectorAll('.option');
|
|
||||||
const correctAnswer = this.currentQuestion.answer;
|
|
||||||
|
|
||||||
options.forEach(option => {
|
|
||||||
const optionValue = option.dataset.value;
|
|
||||||
|
|
||||||
if (optionValue === correctAnswer) {
|
|
||||||
option.classList.add('correct');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 禁用点击
|
|
||||||
option.style.pointerEvents = 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化解析内容
|
|
||||||
formatExplanation(explanation) {
|
|
||||||
// 转义HTML
|
|
||||||
let formatted = this.escapeHtml(explanation);
|
|
||||||
|
|
||||||
// 处理代码块
|
|
||||||
formatted = formatted.replace(/```js\n([\s\S]*?)\n```/g, '<pre><code>$1</code></pre>');
|
|
||||||
formatted = formatted.replace(/```javascript\n([\s\S]*?)\n```/g, '<pre><code>$1</code></pre>');
|
|
||||||
formatted = formatted.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
|
||||||
|
|
||||||
// 处理行内代码
|
|
||||||
formatted = formatted.replace(/`([^`]+)`/g, '<code style="background: #f0f0f0; padding: 2px 4px; border-radius: 3px; font-family: monospace;">$1</code>');
|
|
||||||
|
|
||||||
// 处理换行
|
|
||||||
formatted = formatted.replace(/\n\n/g, '</p><p>');
|
|
||||||
formatted = formatted.replace(/\n/g, '<br>');
|
|
||||||
|
|
||||||
// 包装段落
|
|
||||||
if (!formatted.includes('<p>')) {
|
|
||||||
formatted = '<p>' + formatted + '</p>';
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTML转义
|
|
||||||
escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出为Markdown
|
|
||||||
exportToMarkdown() {
|
|
||||||
if (!this.currentQuestion) {
|
|
||||||
alert('请先加载题目后再导出!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const question = this.currentQuestion;
|
|
||||||
const timestamp = new Date().toLocaleString('zh-CN');
|
|
||||||
|
|
||||||
// 构建Markdown内容
|
|
||||||
let markdown = `# JavaScript趣味题 #${question.id}\n\n`;
|
|
||||||
markdown += `> 导出时间: ${timestamp}\n\n`;
|
|
||||||
|
|
||||||
// 题目部分
|
|
||||||
markdown += `## 题目\n\n`;
|
|
||||||
markdown += `${question.question}\n\n`;
|
|
||||||
|
|
||||||
// 代码部分
|
|
||||||
markdown += `## 代码\n\n`;
|
|
||||||
markdown += `\`\`\`javascript\n${question.code}\n\`\`\`\n\n`;
|
|
||||||
|
|
||||||
// 选项部分
|
|
||||||
markdown += `## 选项\n\n`;
|
|
||||||
question.options.forEach((option, index) => {
|
|
||||||
const letter = String.fromCharCode(65 + index); // A, B, C, D
|
|
||||||
const isCorrect = letter === question.answer;
|
|
||||||
markdown += `${letter}. ${option}${isCorrect ? ' ✅' : ''}\n`;
|
|
||||||
});
|
|
||||||
markdown += `\n`;
|
|
||||||
|
|
||||||
// 答案部分
|
|
||||||
markdown += `## 正确答案\n\n`;
|
|
||||||
markdown += `**${question.answer}**\n\n`;
|
|
||||||
|
|
||||||
// 解析部分
|
|
||||||
markdown += `## 答案解析\n\n`;
|
|
||||||
// 清理解析内容中的HTML标签,转换为Markdown格式
|
|
||||||
let explanation = question.explanation;
|
|
||||||
explanation = explanation.replace(/<br\s*\/?>/gi, '\n');
|
|
||||||
explanation = explanation.replace(/<p>/gi, '\n');
|
|
||||||
explanation = explanation.replace(/<\/p>/gi, '\n');
|
|
||||||
explanation = explanation.replace(/<code[^>]*>/gi, '`');
|
|
||||||
explanation = explanation.replace(/<\/code>/gi, '`');
|
|
||||||
explanation = explanation.replace(/<pre><code>/gi, '\n```javascript\n');
|
|
||||||
explanation = explanation.replace(/<\/code><\/pre>/gi, '\n```\n');
|
|
||||||
explanation = explanation.replace(/<[^>]*>/g, ''); // 移除其他HTML标签
|
|
||||||
explanation = explanation.replace(/\n\s*\n/g, '\n\n'); // 清理多余空行
|
|
||||||
markdown += explanation.trim() + '\n\n';
|
|
||||||
|
|
||||||
// 添加页脚
|
|
||||||
markdown += `---\n\n`;
|
|
||||||
markdown += `*本题目来源于JavaScript趣味题集合*\n`;
|
|
||||||
markdown += `*导出工具: JavaScript趣味题网页版*\n`;
|
|
||||||
|
|
||||||
// 创建下载
|
|
||||||
this.downloadMarkdown(markdown, `JavaScript趣味题_${question.id}_${new Date().getTime()}.md`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下载Markdown文件
|
|
||||||
downloadMarkdown(content, filename) {
|
|
||||||
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = filename;
|
|
||||||
link.style.display = 'none';
|
|
||||||
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
|
|
||||||
// 清理URL对象
|
|
||||||
setTimeout(() => {
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
// 显示成功提示
|
|
||||||
this.showExportSuccess(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示导出成功提示
|
|
||||||
showExportSuccess(filename) {
|
|
||||||
// 创建临时提示元素
|
|
||||||
const toast = document.createElement('div');
|
|
||||||
toast.style.cssText = `
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
background: linear-gradient(135deg, #4caf50, #45a049);
|
|
||||||
color: white;
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
|
||||||
z-index: 10000;
|
|
||||||
font-size: 14px;
|
|
||||||
max-width: 300px;
|
|
||||||
word-wrap: break-word;
|
|
||||||
animation: slideInRight 0.3s ease-out;
|
|
||||||
`;
|
|
||||||
|
|
||||||
toast.innerHTML = `
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<div style="font-weight: 600;">导出成功!</div>
|
|
||||||
<div style="font-size: 12px; opacity: 0.9; margin-top: 2px;">${filename}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 添加动画样式
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
@keyframes slideInRight {
|
|
||||||
from {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes slideOutRight {
|
|
||||||
from {
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
|
|
||||||
document.body.appendChild(toast);
|
|
||||||
|
|
||||||
// 3秒后自动消失
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.style.animation = 'slideOutRight 0.3s ease-in';
|
|
||||||
setTimeout(() => {
|
|
||||||
if (toast.parentNode) {
|
|
||||||
document.body.removeChild(toast);
|
|
||||||
}
|
|
||||||
if (style.parentNode) {
|
|
||||||
document.head.removeChild(style);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载完成后初始化应用
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
new JSQuizApp();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加键盘快捷键支持
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
// 按R键刷新题目
|
|
||||||
if (e.key.toLowerCase() === 'r' && !e.ctrlKey && !e.metaKey) {
|
|
||||||
const refreshBtn = document.getElementById('refreshBtn');
|
|
||||||
if (refreshBtn && !document.querySelector('.loading').style.display !== 'none') {
|
|
||||||
refreshBtn.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按数字键1-4选择选项
|
|
||||||
if (['1', '2', '3', '4'].includes(e.key)) {
|
|
||||||
const options = document.querySelectorAll('.option');
|
|
||||||
const index = parseInt(e.key) - 1;
|
|
||||||
if (options[index] && !options[index].style.pointerEvents) {
|
|
||||||
options[index].click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按Enter键提交答案
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
const submitBtn = document.getElementById('submitBtn');
|
|
||||||
if (submitBtn && !submitBtn.disabled) {
|
|
||||||
submitBtn.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加触摸设备支持
|
|
||||||
if ('ontouchstart' in window) {
|
|
||||||
document.addEventListener('touchstart', () => {}, { passive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加网络状态监听
|
|
||||||
if ('navigator' in window && 'onLine' in navigator) {
|
|
||||||
window.addEventListener('online', () => {
|
|
||||||
console.log('网络连接已恢复');
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('offline', () => {
|
|
||||||
console.log('网络连接已断开');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[
|
|
||||||
"https://60s.api.shumengya.top"
|
|
||||||
]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
|
||||||
"data": {
|
|
||||||
"id": 11,
|
|
||||||
"question": "输出是什么?",
|
|
||||||
"code": "function Person(firstName, lastName) {\n this.firstName = firstName;\n this.lastName = lastName;\n}\n\nconst member = new Person(\"Lydia\", \"Hallie\");\nPerson.getFullName = function () {\n return `${this.firstName} ${this.lastName}`;\n}\n\nconsole.log(member.getFullName());",
|
|
||||||
"options": [
|
|
||||||
"A: `TypeError`",
|
|
||||||
"B: `SyntaxError`",
|
|
||||||
"C: `Lydia Hallie`",
|
|
||||||
"D: `undefined` `undefined`"
|
|
||||||
],
|
|
||||||
"answer": "A",
|
|
||||||
"explanation": "你不能像常规对象那样,给构造函数添加属性。如果你想一次性给所有实例添加特性,你应该使用原型。因此本例中,使用如下方式:\n\n```js\nPerson.prototype.getFullName = function () {\n return `${this.firstName} ${this.lastName}`;\n}\n```\n\n这才会使 `member.getFullName()` 起作用。为什么这么做有益的?假设我们将这个方法添加到构造函数本身里。也许不是每个 `Person` 实例都需要这个方法。这将浪费大量内存空间,因为它们仍然具有该属性,这将占用每个实例的内存空间。相反,如果我们只将它添加到原型中,那么它只存在于内存中的一个位置,但是所有实例都可以访问它!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
/* 背景样式文件 */
|
|
||||||
body {
|
|
||||||
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 25%, #ffd3a5 50%, #a8e6cf 75%, #88d8a3 100%);
|
|
||||||
background-size: 400% 400%;
|
|
||||||
animation: gradientShift 15s ease infinite;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 背景动画 */
|
|
||||||
@keyframes gradientShift {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 背景装饰元素 */
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-image:
|
|
||||||
radial-gradient(circle at 20% 80%, rgba(39, 174, 96, 0.1) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 80% 20%, rgba(46, 204, 113, 0.1) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 40% 40%, rgba(26, 188, 156, 0.05) 0%, transparent 50%);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 浮动装饰圆点 */
|
|
||||||
body::after {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-image:
|
|
||||||
radial-gradient(2px 2px at 20px 30px, rgba(39, 174, 96, 0.3), transparent),
|
|
||||||
radial-gradient(2px 2px at 40px 70px, rgba(46, 204, 113, 0.2), transparent),
|
|
||||||
radial-gradient(1px 1px at 90px 40px, rgba(26, 188, 156, 0.3), transparent),
|
|
||||||
radial-gradient(1px 1px at 130px 80px, rgba(39, 174, 96, 0.2), transparent),
|
|
||||||
radial-gradient(2px 2px at 160px 30px, rgba(46, 204, 113, 0.3), transparent);
|
|
||||||
background-repeat: repeat;
|
|
||||||
background-size: 200px 100px;
|
|
||||||
animation: floatDots 20s linear infinite;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes floatDots {
|
|
||||||
0% {
|
|
||||||
transform: translateY(0px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(-100px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式背景调整 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
body::after {
|
|
||||||
background-size: 150px 75px;
|
|
||||||
animation-duration: 25s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
body::after {
|
|
||||||
background-size: 100px 50px;
|
|
||||||
animation-duration: 30s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,339 +0,0 @@
|
|||||||
/* 基础样式重置 */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #2c3e50;
|
|
||||||
min-height: 100vh;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 容器布局 */
|
|
||||||
.container {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 头部样式 */
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
padding: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #27ae60;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
text-shadow: 0 2px 4px rgba(39, 174, 96, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: #7f8c8d;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主要内容区域 */
|
|
||||||
.main {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-card {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 40px;
|
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid rgba(39, 174, 96, 0.1);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* KFC文案内容 */
|
|
||||||
.kfc-content {
|
|
||||||
min-height: 200px;
|
|
||||||
padding: 30px;
|
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|
||||||
border-radius: 15px;
|
|
||||||
border-left: 5px solid #27ae60;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kfc-content::before {
|
|
||||||
content: '"';
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
left: 15px;
|
|
||||||
font-size: 3rem;
|
|
||||||
color: #27ae60;
|
|
||||||
opacity: 0.3;
|
|
||||||
font-family: serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kfc-content p {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
line-height: 1.8;
|
|
||||||
color: #2c3e50;
|
|
||||||
margin-left: 20px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
text-align: center;
|
|
||||||
color: #7f8c8d;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 按钮组 */
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-btn, .copy-btn {
|
|
||||||
padding: 15px 30px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-btn {
|
|
||||||
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 20px rgba(39, 174, 96, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-btn:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-btn:disabled {
|
|
||||||
opacity: 0.7;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-btn {
|
|
||||||
background: linear-gradient(135deg, #3498db 0%, #5dade2 100%);
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 编号信息 */
|
|
||||||
.index-info {
|
|
||||||
text-align: center;
|
|
||||||
padding: 10px;
|
|
||||||
background: rgba(39, 174, 96, 0.1);
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid rgba(39, 174, 96, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.index-text {
|
|
||||||
color: #27ae60;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#indexNumber {
|
|
||||||
color: #2c3e50;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 底部 */
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px 0;
|
|
||||||
color: #7f8c8d;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 提示框 */
|
|
||||||
.toast {
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
background: #27ae60;
|
|
||||||
color: white;
|
|
||||||
padding: 15px 25px;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
||||||
transform: translateX(400px);
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
z-index: 1000;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast.show {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 平板端适配 (768px - 1024px) */
|
|
||||||
@media (max-width: 1024px) and (min-width: 768px) {
|
|
||||||
.container {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 2.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-card {
|
|
||||||
padding: 35px;
|
|
||||||
max-width: 550px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kfc-content {
|
|
||||||
padding: 25px;
|
|
||||||
min-height: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-btn, .copy-btn {
|
|
||||||
padding: 12px 25px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 手机端适配 (最大768px) */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.container {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-card {
|
|
||||||
padding: 25px;
|
|
||||||
margin: 0 5px;
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kfc-content {
|
|
||||||
padding: 20px;
|
|
||||||
min-height: 150px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kfc-content::before {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
top: 5px;
|
|
||||||
left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kfc-content p {
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-btn, .copy-btn {
|
|
||||||
padding: 12px 20px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast {
|
|
||||||
right: 10px;
|
|
||||||
left: 10px;
|
|
||||||
transform: translateY(-100px);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast.show {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 小屏手机适配 (最大480px) */
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.title {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-card {
|
|
||||||
padding: 20px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kfc-content {
|
|
||||||
padding: 15px;
|
|
||||||
min-height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kfc-content p {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-btn, .copy-btn {
|
|
||||||
padding: 10px 15px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>随机KFC文案生成器</title>
|
|
||||||
<link rel="stylesheet" href="css/style.css">
|
|
||||||
<link rel="stylesheet" href="css/background.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<header class="header">
|
|
||||||
<h1 class="title">🍗 随机KFC文案生成器</h1>
|
|
||||||
<p class="subtitle">疯狂星期四,文案来一套!</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="main">
|
|
||||||
<div class="content-card">
|
|
||||||
<div class="kfc-content" id="kfcContent">
|
|
||||||
<p class="loading-text">点击按钮获取随机KFC文案...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="button-group">
|
|
||||||
<button class="generate-btn" id="generateBtn">
|
|
||||||
<span class="btn-text">生成文案</span>
|
|
||||||
<span class="btn-loading" style="display: none;">生成中...</span>
|
|
||||||
</button>
|
|
||||||
<button class="copy-btn" id="copyBtn" style="display: none;">复制文案</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="index-info" id="indexInfo" style="display: none;">
|
|
||||||
<span class="index-text">文案编号: <span id="indexNumber"></span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="footer">
|
|
||||||
<p>© 2024 KFC文案生成器 | 让每个星期四都疯狂起来</p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toast" id="toast"></div>
|
|
||||||
|
|
||||||
<script src="js/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
// KFC文案生成器主要功能
|
|
||||||
class KFCGenerator {
|
|
||||||
constructor() {
|
|
||||||
this.apiEndpoints = [];
|
|
||||||
this.currentApiIndex = 0;
|
|
||||||
this.isLoading = false;
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
async init() {
|
|
||||||
await this.loadApiEndpoints();
|
|
||||||
this.bindEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载API接口列表
|
|
||||||
async loadApiEndpoints() {
|
|
||||||
try {
|
|
||||||
// 直接硬编码API端点,避免CORS问题
|
|
||||||
this.apiEndpoints = ["https://60s.api.shumengya.top"];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载API接口列表失败:', error);
|
|
||||||
this.showToast('加载接口配置失败', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绑定事件
|
|
||||||
bindEvents() {
|
|
||||||
const generateBtn = document.getElementById('generateBtn');
|
|
||||||
const copyBtn = document.getElementById('copyBtn');
|
|
||||||
|
|
||||||
generateBtn.addEventListener('click', () => this.generateKFC());
|
|
||||||
copyBtn.addEventListener('click', () => this.copyContent());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成KFC文案
|
|
||||||
async generateKFC() {
|
|
||||||
if (this.isLoading) return;
|
|
||||||
|
|
||||||
this.setLoadingState(true);
|
|
||||||
|
|
||||||
let success = false;
|
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = this.apiEndpoints.length;
|
|
||||||
|
|
||||||
while (!success && attempts < maxAttempts) {
|
|
||||||
try {
|
|
||||||
const apiUrl = this.apiEndpoints[this.currentApiIndex];
|
|
||||||
const data = await this.fetchKFCData(apiUrl);
|
|
||||||
|
|
||||||
if (data && data.code === 200 && data.data && data.data.kfc) {
|
|
||||||
this.displayKFC(data.data);
|
|
||||||
success = true;
|
|
||||||
} else {
|
|
||||||
throw new Error('API返回数据格式错误');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`API ${this.currentApiIndex + 1} 请求失败:`, error);
|
|
||||||
this.currentApiIndex = (this.currentApiIndex + 1) % this.apiEndpoints.length;
|
|
||||||
attempts++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
this.showError('所有API接口都无法访问,请稍后重试');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setLoadingState(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 请求KFC数据
|
|
||||||
async fetchKFCData(apiUrl) {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${apiUrl}/v2/kfc`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
signal: controller.signal
|
|
||||||
});
|
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示KFC文案
|
|
||||||
displayKFC(data) {
|
|
||||||
const contentElement = document.getElementById('kfcContent');
|
|
||||||
const indexElement = document.getElementById('indexNumber');
|
|
||||||
const indexInfo = document.getElementById('indexInfo');
|
|
||||||
const copyBtn = document.getElementById('copyBtn');
|
|
||||||
|
|
||||||
// 显示文案内容
|
|
||||||
contentElement.innerHTML = `<p>${this.escapeHtml(data.kfc)}</p>`;
|
|
||||||
|
|
||||||
// 显示编号信息
|
|
||||||
if (data.index) {
|
|
||||||
indexElement.textContent = data.index;
|
|
||||||
indexInfo.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
indexInfo.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示复制按钮
|
|
||||||
copyBtn.style.display = 'inline-block';
|
|
||||||
|
|
||||||
// 添加显示动画
|
|
||||||
contentElement.style.opacity = '0';
|
|
||||||
contentElement.style.transform = 'translateY(20px)';
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
contentElement.style.transition = 'all 0.5s ease';
|
|
||||||
contentElement.style.opacity = '1';
|
|
||||||
contentElement.style.transform = 'translateY(0)';
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示错误信息
|
|
||||||
showError(message) {
|
|
||||||
const contentElement = document.getElementById('kfcContent');
|
|
||||||
contentElement.innerHTML = `<p class="loading-text" style="color: #e74c3c;">${this.escapeHtml(message)}</p>`;
|
|
||||||
|
|
||||||
const copyBtn = document.getElementById('copyBtn');
|
|
||||||
const indexInfo = document.getElementById('indexInfo');
|
|
||||||
copyBtn.style.display = 'none';
|
|
||||||
indexInfo.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复制文案内容
|
|
||||||
async copyContent() {
|
|
||||||
const contentElement = document.getElementById('kfcContent');
|
|
||||||
const textContent = contentElement.querySelector('p')?.textContent;
|
|
||||||
|
|
||||||
if (!textContent || textContent.includes('点击按钮获取') || textContent.includes('失败')) {
|
|
||||||
this.showToast('没有可复制的内容', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
|
||||||
await navigator.clipboard.writeText(textContent);
|
|
||||||
} else {
|
|
||||||
// 降级方案
|
|
||||||
const textArea = document.createElement('textarea');
|
|
||||||
textArea.value = textContent;
|
|
||||||
textArea.style.position = 'fixed';
|
|
||||||
textArea.style.left = '-999999px';
|
|
||||||
textArea.style.top = '-999999px';
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.focus();
|
|
||||||
textArea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
textArea.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showToast('文案已复制到剪贴板', 'success');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('复制失败:', error);
|
|
||||||
this.showToast('复制失败,请手动选择复制', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置加载状态
|
|
||||||
setLoadingState(loading) {
|
|
||||||
this.isLoading = loading;
|
|
||||||
const generateBtn = document.getElementById('generateBtn');
|
|
||||||
const btnText = generateBtn.querySelector('.btn-text');
|
|
||||||
const btnLoading = generateBtn.querySelector('.btn-loading');
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
generateBtn.disabled = true;
|
|
||||||
btnText.style.display = 'none';
|
|
||||||
btnLoading.style.display = 'inline';
|
|
||||||
} else {
|
|
||||||
generateBtn.disabled = false;
|
|
||||||
btnText.style.display = 'inline';
|
|
||||||
btnLoading.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示提示消息
|
|
||||||
showToast(message, type = 'success') {
|
|
||||||
const toast = document.getElementById('toast');
|
|
||||||
toast.textContent = message;
|
|
||||||
toast.className = `toast ${type}`;
|
|
||||||
toast.classList.add('show');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.remove('show');
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTML转义
|
|
||||||
escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载完成后初始化
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const generator = new KFCGenerator();
|
|
||||||
// 页面加载完成后自动生成一条文案
|
|
||||||
setTimeout(() => {
|
|
||||||
generator.generateKFC();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加键盘快捷键支持
|
|
||||||
document.addEventListener('keydown', (event) => {
|
|
||||||
// 按空格键生成文案
|
|
||||||
if (event.code === 'Space' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') {
|
|
||||||
event.preventDefault();
|
|
||||||
document.getElementById('generateBtn').click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+C 复制文案
|
|
||||||
if (event.ctrlKey && event.key === 'c' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA') {
|
|
||||||
const copyBtn = document.getElementById('copyBtn');
|
|
||||||
if (copyBtn.style.display !== 'none') {
|
|
||||||
event.preventDefault();
|
|
||||||
copyBtn.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[
|
|
||||||
"https://60s.api.shumengya.top"
|
|
||||||
]
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
|
||||||
"data": {
|
|
||||||
"index": 78,
|
|
||||||
"kfc": "我叫夯大力 立冬给我准备了糖炒栗子了没有 没准备的自动绝交 再 v 我 50 吃疯狂星期四 然后再给我点杯奶茶 再给我两万块钱 懂吗你们"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
56
InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言.html
Normal file
56
InfoGenie-frontend/public/60sapi/娱乐消遣/随机一言.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>随机一言</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px;display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 68px)}
|
||||||
|
.quote{background:linear-gradient(135deg,#f0fdf4,#ecfdf5);border-radius:20px;padding:36px 28px;text-align:center;border:1px solid rgba(34,197,94,.12);position:relative;width:100%}
|
||||||
|
.quote::before{content:'"';position:absolute;top:10px;left:20px;font-size:60px;color:rgba(34,197,94,.15);font-family:serif;line-height:1}
|
||||||
|
.quote-text{font-size:18px;line-height:2;color:#374151;font-weight:500}
|
||||||
|
.quote-src{font-size:12px;color:#9ca3af;margin-top:16px}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px;width:100%}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px;width:100%}
|
||||||
|
@media(max-width:640px){.quote-text{font-size:15px}.quote{padding:28px 20px}}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>💭 随机一言</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(window.__SIXTY_API_BASE__+'/v2/hitokoto?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data!==undefined?json.data:json;
|
||||||
|
let text='',source='';
|
||||||
|
if(typeof d==='string'){text=d}
|
||||||
|
else if(d&&typeof d==='object'){
|
||||||
|
for(const k of['content','text','hitokoto','duanzi','joke','saying','answer','kfc','data']){if(typeof d[k]==='string'){text=d[k];break}}
|
||||||
|
if(!text){for(const v of Object.values(d)){if(typeof v==='string'&&v.length>5){text=v;break}}}
|
||||||
|
if(!text)text=JSON.stringify(d,null,2);
|
||||||
|
source=d.from||d.source||d.author||'';
|
||||||
|
}else{text=String(d??'')}
|
||||||
|
el.innerHTML=`<div class="quote"><div class="quote-text">${text}</div>${source?`<div class="quote-src">—— ${source}</div>`:''}</div>`;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
/* 背景样式文件 - 金色光辉主题 */
|
|
||||||
|
|
||||||
/* 主背景 */
|
|
||||||
body {
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
#f1f8e9 0%,
|
|
||||||
#dcedc8 25%,
|
|
||||||
#c8e6c8 50%,
|
|
||||||
#a5d6a7 75%,
|
|
||||||
#81c784 100%
|
|
||||||
);
|
|
||||||
background-size: 400% 400%;
|
|
||||||
animation: gradientShift 15s ease infinite;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 背景装饰层 */
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 20% 80%, rgba(129, 199, 132, 0.1) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 80% 20%, rgba(165, 214, 167, 0.1) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 40% 40%, rgba(102, 187, 106, 0.05) 0%, transparent 50%);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 动态光点效果 */
|
|
||||||
body::after {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-image:
|
|
||||||
radial-gradient(2px 2px at 20px 30px, rgba(129, 199, 132, 0.3), transparent),
|
|
||||||
radial-gradient(2px 2px at 40px 70px, rgba(165, 214, 167, 0.2), transparent),
|
|
||||||
radial-gradient(1px 1px at 90px 40px, rgba(102, 187, 106, 0.4), transparent),
|
|
||||||
radial-gradient(1px 1px at 130px 80px, rgba(129, 199, 132, 0.2), transparent),
|
|
||||||
radial-gradient(2px 2px at 160px 30px, rgba(165, 214, 167, 0.3), transparent);
|
|
||||||
background-repeat: repeat;
|
|
||||||
background-size: 200px 100px;
|
|
||||||
animation: sparkle 20s linear infinite;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 背景动画 */
|
|
||||||
@keyframes gradientShift {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes sparkle {
|
|
||||||
0% {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(-100vh);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式背景调整 */
|
|
||||||
|
|
||||||
/* 平板端背景 */
|
|
||||||
@media (min-width: 768px) and (max-width: 1024px) {
|
|
||||||
body::after {
|
|
||||||
background-size: 250px 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 电脑端背景 */
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
body {
|
|
||||||
background-size: 300% 300%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::after {
|
|
||||||
background-size: 300px 150px;
|
|
||||||
animation-duration: 25s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 手机端背景优化 */
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
body {
|
|
||||||
background-size: 200% 200%;
|
|
||||||
animation-duration: 10s;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::before {
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 30% 70%, rgba(129, 199, 132, 0.08) 0%, transparent 40%),
|
|
||||||
radial-gradient(circle at 70% 30%, rgba(165, 214, 167, 0.08) 0%, transparent 40%);
|
|
||||||
}
|
|
||||||
|
|
||||||
body::after {
|
|
||||||
background-size: 150px 80px;
|
|
||||||
animation-duration: 15s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 超小屏幕背景 */
|
|
||||||
@media (max-width: 479px) {
|
|
||||||
body {
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
#f1f8e9 0%,
|
|
||||||
#dcedc8 50%,
|
|
||||||
#c8e6c8 100%
|
|
||||||
);
|
|
||||||
background-size: 150% 150%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::after {
|
|
||||||
background-size: 120px 60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 深色模式支持 */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
#1b2e1b 0%,
|
|
||||||
#2e4a2e 25%,
|
|
||||||
#3e5e3e 50%,
|
|
||||||
#4e6e4e 75%,
|
|
||||||
#5e7e5e 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
body::before {
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 20% 80%, rgba(129, 199, 132, 0.05) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 80% 20%, rgba(165, 214, 167, 0.05) 0%, transparent 50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 减少动画效果(用户偏好) */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
body,
|
|
||||||
body::before,
|
|
||||||
body::after {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: linear-gradient(135deg, #f1f8e9 0%, #dcedc8 50%, #c8e6c8 100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,357 +0,0 @@
|
|||||||
/* 基础样式重置 */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #2e7d32;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 容器布局 */
|
|
||||||
.container {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 头部样式 */
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 3rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #2e7d32;
|
|
||||||
text-shadow:
|
|
||||||
0 0 10px rgba(129, 199, 132, 0.8),
|
|
||||||
0 0 20px rgba(129, 199, 132, 0.6),
|
|
||||||
0 0 30px rgba(129, 199, 132, 0.4);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
animation: titleGlow 3s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: #388e3c;
|
|
||||||
opacity: 0.9;
|
|
||||||
text-shadow: 0 0 5px rgba(102, 187, 106, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主内容区域 */
|
|
||||||
.main-content {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 一言容器 */
|
|
||||||
.quote-container {
|
|
||||||
background: linear-gradient(135deg, rgba(129, 199, 132, 0.1), rgba(165, 214, 167, 0.05));
|
|
||||||
border: 2px solid rgba(102, 187, 106, 0.3);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 40px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
box-shadow:
|
|
||||||
0 8px 32px rgba(102, 187, 106, 0.2),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-container::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -2px;
|
|
||||||
left: -2px;
|
|
||||||
right: -2px;
|
|
||||||
bottom: -2px;
|
|
||||||
background: linear-gradient(45deg, #81c784, #a5d6a7, #81c784, #a5d6a7);
|
|
||||||
border-radius: 22px;
|
|
||||||
z-index: -1;
|
|
||||||
animation: borderGlow 4s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 加载状态 */
|
|
||||||
.loading {
|
|
||||||
display: none;
|
|
||||||
text-align: center;
|
|
||||||
color: #2e7d32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading.show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 4px solid rgba(102, 187, 106, 0.3);
|
|
||||||
border-top: 4px solid #2e7d32;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin: 0 auto 15px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 一言显示 */
|
|
||||||
.quote-display {
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-display.hide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-text {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
line-height: 1.8;
|
|
||||||
color: #2e7d32;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-shadow: 0 1px 2px rgba(102, 187, 106, 0.1);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-index {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #388e3c;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 错误信息 */
|
|
||||||
.error-message {
|
|
||||||
display: none;
|
|
||||||
text-align: center;
|
|
||||||
color: #66bb6a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message.show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-icon {
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-text {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 控制按钮 */
|
|
||||||
.controls {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn {
|
|
||||||
background: linear-gradient(135deg, #81c784, #a5d6a7);
|
|
||||||
border: none;
|
|
||||||
border-radius: 50px;
|
|
||||||
padding: 15px 30px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #2e7d32;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 15px rgba(102, 187, 106, 0.3),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow:
|
|
||||||
0 6px 20px rgba(102, 187, 106, 0.4),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn:hover .btn-icon {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 底部 */
|
|
||||||
.footer {
|
|
||||||
margin-top: 40px;
|
|
||||||
text-align: center;
|
|
||||||
color: #388e3c;
|
|
||||||
opacity: 0.8;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 动画效果 */
|
|
||||||
@keyframes titleGlow {
|
|
||||||
0% {
|
|
||||||
text-shadow:
|
|
||||||
0 0 10px rgba(129, 199, 132, 0.8),
|
|
||||||
0 0 20px rgba(129, 199, 132, 0.6),
|
|
||||||
0 0 30px rgba(129, 199, 132, 0.4);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
text-shadow:
|
|
||||||
0 0 15px rgba(129, 199, 132, 1),
|
|
||||||
0 0 25px rgba(129, 199, 132, 0.8),
|
|
||||||
0 0 35px rgba(129, 199, 132, 0.6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes borderGlow {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 平板端适配 (768px - 1024px) */
|
|
||||||
@media (min-width: 768px) and (max-width: 1024px) {
|
|
||||||
.container {
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 3.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-container {
|
|
||||||
padding: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-text {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 电脑端适配 (1024px+) */
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.container {
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-container {
|
|
||||||
padding: 60px;
|
|
||||||
max-width: 900px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-text {
|
|
||||||
font-size: 2.2rem;
|
|
||||||
line-height: 1.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn {
|
|
||||||
padding: 18px 36px;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 手机端适配 (小于768px) */
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.container {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-container {
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-text {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn {
|
|
||||||
padding: 12px 24px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
margin-top: 30px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 超小屏幕适配 (小于480px) */
|
|
||||||
@media (max-width: 479px) {
|
|
||||||
.title {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-container {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-text {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn {
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>随机一言 - 金色光辉</title>
|
|
||||||
<link rel="stylesheet" href="css/style.css">
|
|
||||||
<link rel="stylesheet" href="css/background.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<header class="header">
|
|
||||||
<h1 class="title">随机一言</h1>
|
|
||||||
<p class="subtitle">每一句话都是心灵的光芒</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="main-content">
|
|
||||||
<div class="quote-container">
|
|
||||||
<div class="loading" id="loading">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
<p>正在获取一言...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="quote-display" id="quoteDisplay">
|
|
||||||
<div class="quote-text" id="quoteText">
|
|
||||||
点击下方按钮获取一言
|
|
||||||
</div>
|
|
||||||
<div class="quote-index" id="quoteIndex"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="error-message" id="errorMessage">
|
|
||||||
<div class="error-icon">⚠️</div>
|
|
||||||
<div class="error-text" id="errorText"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<button class="refresh-btn" id="refreshBtn">
|
|
||||||
<span class="btn-icon">🔄</span>
|
|
||||||
<span class="btn-text">获取新一言</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="footer">
|
|
||||||
<p>愿每一句话都能温暖你的心</p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="js/script.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
// 随机一言 JavaScript 功能实现
|
|
||||||
|
|
||||||
class HitokotoApp {
|
|
||||||
constructor() {
|
|
||||||
// API接口列表
|
|
||||||
this.apiEndpoints = [
|
|
||||||
"https://60s.api.shumengya.top"
|
|
||||||
];
|
|
||||||
|
|
||||||
this.currentEndpointIndex = 0;
|
|
||||||
this.isLoading = false;
|
|
||||||
|
|
||||||
// DOM 元素
|
|
||||||
this.elements = {
|
|
||||||
loading: document.getElementById('loading'),
|
|
||||||
quoteDisplay: document.getElementById('quoteDisplay'),
|
|
||||||
quoteText: document.getElementById('quoteText'),
|
|
||||||
quoteIndex: document.getElementById('quoteIndex'),
|
|
||||||
errorMessage: document.getElementById('errorMessage'),
|
|
||||||
errorText: document.getElementById('errorText'),
|
|
||||||
refreshBtn: document.getElementById('refreshBtn')
|
|
||||||
};
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化应用
|
|
||||||
init() {
|
|
||||||
this.bindEvents();
|
|
||||||
this.hideAllStates();
|
|
||||||
this.showQuoteDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绑定事件
|
|
||||||
bindEvents() {
|
|
||||||
this.elements.refreshBtn.addEventListener('click', () => {
|
|
||||||
this.fetchHitokoto();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 键盘快捷键支持
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.code === 'Space' && !this.isLoading) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.fetchHitokoto();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏所有状态
|
|
||||||
hideAllStates() {
|
|
||||||
this.elements.loading.classList.remove('show');
|
|
||||||
this.elements.quoteDisplay.classList.remove('hide');
|
|
||||||
this.elements.errorMessage.classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示加载状态
|
|
||||||
showLoading() {
|
|
||||||
this.hideAllStates();
|
|
||||||
this.elements.loading.classList.add('show');
|
|
||||||
this.elements.quoteDisplay.classList.add('hide');
|
|
||||||
this.elements.refreshBtn.disabled = true;
|
|
||||||
this.isLoading = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示一言内容
|
|
||||||
showQuoteDisplay() {
|
|
||||||
this.hideAllStates();
|
|
||||||
this.elements.quoteDisplay.classList.remove('hide');
|
|
||||||
this.elements.refreshBtn.disabled = false;
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示错误信息
|
|
||||||
showError(message) {
|
|
||||||
this.hideAllStates();
|
|
||||||
this.elements.errorMessage.classList.add('show');
|
|
||||||
this.elements.errorText.textContent = message;
|
|
||||||
this.elements.refreshBtn.disabled = false;
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取一言数据
|
|
||||||
async fetchHitokoto() {
|
|
||||||
if (this.isLoading) return;
|
|
||||||
|
|
||||||
this.showLoading();
|
|
||||||
|
|
||||||
// 尝试所有API接口
|
|
||||||
for (let i = 0; i < this.apiEndpoints.length; i++) {
|
|
||||||
const endpointIndex = (this.currentEndpointIndex + i) % this.apiEndpoints.length;
|
|
||||||
const endpoint = this.apiEndpoints[endpointIndex];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await this.tryFetchFromEndpoint(endpoint);
|
|
||||||
if (result.success) {
|
|
||||||
this.currentEndpointIndex = endpointIndex;
|
|
||||||
this.displayHitokoto(result.data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`接口 ${endpoint} 请求失败:`, error.message);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 所有接口都失败
|
|
||||||
this.showError('所有接口都无法访问,请检查网络连接或稍后重试');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试从指定接口获取数据
|
|
||||||
async tryFetchFromEndpoint(endpoint) {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 移除URL中的encoding=text参数,确保返回JSON格式
|
|
||||||
const response = await fetch(`${endpoint}/v2/hitokoto`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
signal: controller.signal
|
|
||||||
});
|
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// 验证返回数据格式
|
|
||||||
if (data.code === 200 && data.data && data.data.hitokoto) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: data.data
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw new Error('返回数据格式不正确');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (error.name === 'AbortError') {
|
|
||||||
throw new Error('请求超时');
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示一言内容
|
|
||||||
displayHitokoto(data) {
|
|
||||||
// 更新一言文本
|
|
||||||
this.elements.quoteText.textContent = data.hitokoto;
|
|
||||||
|
|
||||||
// 更新序号信息
|
|
||||||
if (data.index) {
|
|
||||||
this.elements.quoteIndex.textContent = `第 ${data.index} 条`;
|
|
||||||
} else {
|
|
||||||
this.elements.quoteIndex.textContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加淡入动画效果
|
|
||||||
this.elements.quoteText.style.opacity = '0';
|
|
||||||
this.elements.quoteIndex.style.opacity = '0';
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.elements.quoteText.style.transition = 'opacity 0.5s ease';
|
|
||||||
this.elements.quoteIndex.style.transition = 'opacity 0.5s ease';
|
|
||||||
this.elements.quoteText.style.opacity = '1';
|
|
||||||
this.elements.quoteIndex.style.opacity = '1';
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
this.showQuoteDisplay();
|
|
||||||
|
|
||||||
// 控制台输出调试信息
|
|
||||||
console.log('一言获取成功:', {
|
|
||||||
content: data.hitokoto,
|
|
||||||
index: data.index,
|
|
||||||
endpoint: this.apiEndpoints[this.currentEndpointIndex]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取随机接口(用于负载均衡)
|
|
||||||
getRandomEndpoint() {
|
|
||||||
const randomIndex = Math.floor(Math.random() * this.apiEndpoints.length);
|
|
||||||
return this.apiEndpoints[randomIndex];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载完成后初始化应用
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const app = new HitokotoApp();
|
|
||||||
|
|
||||||
// 添加全局错误处理
|
|
||||||
window.addEventListener('error', (event) => {
|
|
||||||
console.error('页面发生错误:', event.error);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('unhandledrejection', (event) => {
|
|
||||||
console.error('未处理的Promise拒绝:', event.reason);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 页面可见性变化时的处理
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (!document.hidden && !app.isLoading) {
|
|
||||||
// 页面重新可见时,可以选择刷新内容
|
|
||||||
console.log('页面重新可见');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('随机一言应用初始化完成');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 导出应用类(如果需要在其他地方使用)
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
|
||||||
module.exports = HitokotoApp;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[
|
|
||||||
"https://60s.api.shumengya.top"
|
|
||||||
]
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
|
||||||
"data": {
|
|
||||||
"index": 2862,
|
|
||||||
"hitokoto": "你带上罪恶之冠,即使背负上所有罪恶和孤独,绝不让你受伤"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
/* 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;
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
: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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
["https://60s.api.shumengya.top"]
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
|
||||||
"data": {
|
|
||||||
"index": 121,
|
|
||||||
"content": "这个世界上谁最懂猪?蜘蛛(知猪)人。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
["https://60s.api.shumengya.top"]
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
|
||||||
"data": {
|
|
||||||
"index": 347,
|
|
||||||
"duanzi": "我不想读书,主要是因为家里牛啊,猪啊羊啊都没人喂。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
56
InfoGenie-frontend/public/60sapi/娱乐消遣/随机唱歌.html
Normal file
56
InfoGenie-frontend/public/60sapi/娱乐消遣/随机唱歌.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>随机唱歌</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px;display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 68px)}
|
||||||
|
.quote{background:linear-gradient(135deg,#f0fdf4,#ecfdf5);border-radius:20px;padding:36px 28px;text-align:center;border:1px solid rgba(34,197,94,.12);position:relative;width:100%}
|
||||||
|
.quote::before{content:'"';position:absolute;top:10px;left:20px;font-size:60px;color:rgba(34,197,94,.15);font-family:serif;line-height:1}
|
||||||
|
.quote-text{font-size:18px;line-height:2;color:#374151;font-weight:500}
|
||||||
|
.quote-src{font-size:12px;color:#9ca3af;margin-top:16px}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px;width:100%}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px;width:100%}
|
||||||
|
@media(max-width:640px){.quote-text{font-size:15px}.quote{padding:28px 20px}}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>🎤 随机唱歌</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(window.__SIXTY_API_BASE__+'/v2/changya?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data!==undefined?json.data:json;
|
||||||
|
let text='',source='';
|
||||||
|
if(typeof d==='string'){text=d}
|
||||||
|
else if(d&&typeof d==='object'){
|
||||||
|
for(const k of['content','text','hitokoto','duanzi','joke','saying','answer','kfc','data']){if(typeof d[k]==='string'){text=d[k];break}}
|
||||||
|
if(!text){for(const v of Object.values(d)){if(typeof v==='string'&&v.length>5){text=v;break}}}
|
||||||
|
if(!text)text=JSON.stringify(d,null,2);
|
||||||
|
source=d.from||d.source||d.author||'';
|
||||||
|
}else{text=String(d??'')}
|
||||||
|
el.innerHTML=`<div class="quote"><div class="quote-text">${text}</div>${source?`<div class="quote-src">—— ${source}</div>`:''}</div>`;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
/* 随机唱歌音频 - 淡绿色清新风格样式 */
|
|
||||||
|
|
||||||
/* 重置样式 */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 50%, #ffd3a5 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
color: #2d5016;
|
|
||||||
line-height: 1.6;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 头部 */
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
background: rgba(255, 255, 255, 0.85);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 24px;
|
|
||||||
box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
color: #2d5016;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header p {
|
|
||||||
color: #5a7c65;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 用户卡片 */
|
|
||||||
.user-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 15px;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
|
|
||||||
margin-bottom: 15px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
border: 3px solid rgba(129, 199, 132, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nickname {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: #2d5016;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
color: #5a7c65;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 歌曲信息 */
|
|
||||||
.song-card {
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
|
|
||||||
margin-bottom: 15px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-title {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #1b5e20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-meta {
|
|
||||||
color: #5a7c65;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 歌词 */
|
|
||||||
.lyrics {
|
|
||||||
background: rgba(129, 199, 132, 0.1);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 12px;
|
|
||||||
max-height: 220px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyrics p {
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 音频播放器卡片 */
|
|
||||||
.audio-card {
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 18px;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.25s ease;
|
|
||||||
box-shadow: 0 4px 12px rgba(129, 199, 132, 0.35);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 18px rgba(129, 199, 132, 0.45);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
color: #5a7c65;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 加载与错误 */
|
|
||||||
.loading, .error {
|
|
||||||
text-align: center;
|
|
||||||
padding: 30px;
|
|
||||||
background: rgba(255, 255, 255, 0.85);
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border: 4px solid #e8f5e8;
|
|
||||||
border-top: 4px solid #81c784;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin: 0 auto 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 动画 */
|
|
||||||
.fade-in {
|
|
||||||
animation: fadeIn 0.5s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 平板端适配 */
|
|
||||||
@media (max-width: 1024px) and (min-width: 768px) {
|
|
||||||
.container { padding: 16px; }
|
|
||||||
.header h1 { font-size: 1.8rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 手机端优先优化 */
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.container { padding: 12px; }
|
|
||||||
.header { padding: 18px; }
|
|
||||||
.header h1 { font-size: 1.6rem; gap: 8px; }
|
|
||||||
|
|
||||||
.user-card {
|
|
||||||
padding: 16px;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.song-card, .audio-card {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyrics {
|
|
||||||
max-height: 180px;
|
|
||||||
text-align: left;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 200px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<title>随机唱歌音频 - 60s API 集合</title>
|
|
||||||
<meta name="description" content="随机唱歌音频,数据源自 60s.viki.moe,提供用户信息、歌曲信息、歌词与音频播放。" />
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="./css/style.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<header class="header">
|
|
||||||
<h1>
|
|
||||||
🎵 随机唱歌音频
|
|
||||||
</h1>
|
|
||||||
<p>数据来自官方/权威源头,以确保稳定与实时 · 支持本地数据回退</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- 加载与错误状态 -->
|
|
||||||
<section id="loading" class="loading">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<p>正在加载中,请稍候…</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="error" class="error" style="display: none;">
|
|
||||||
<p>获取数据失败,请稍后重试</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
|
||||||
<main id="content" style="display: none;" class="fade-in">
|
|
||||||
<!-- 用户信息 -->
|
|
||||||
<div class="user-card">
|
|
||||||
<img id="avatar" class="avatar" src="" alt="用户头像" />
|
|
||||||
<div class="user-info">
|
|
||||||
<div class="nickname" id="nickname">-</div>
|
|
||||||
<div class="meta">性别:<span id="gender">-</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 歌曲信息 -->
|
|
||||||
<div class="song-card">
|
|
||||||
<div class="song-title" id="song-title">-</div>
|
|
||||||
<div class="song-meta" id="song-meta">-</div>
|
|
||||||
<div class="lyrics" id="lyrics"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 音频播放 -->
|
|
||||||
<div class="audio-card">
|
|
||||||
<audio id="audio" controls style="width: 100%;"></audio>
|
|
||||||
<div class="audio-actions">
|
|
||||||
<button class="btn" id="refresh-btn">换一首</button>
|
|
||||||
<div class="info">
|
|
||||||
❤ 喜欢:<span id="like-count">-</span>
|
|
||||||
· ⏱ 时长:<span id="duration">--:--</span>
|
|
||||||
· 🗓 发布:<span id="publish-time">-</span>
|
|
||||||
· 🔗 <a id="link" href="#" class="btn" style="padding: 6px 10px; border-radius: 8px; background: #81c784;">查看原帖</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="./js/script.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
// 随机唱歌音频 页面脚本
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const API = {
|
|
||||||
endpoints: [],
|
|
||||||
currentIndex: 0,
|
|
||||||
params: {
|
|
||||||
encoding: 'json'
|
|
||||||
},
|
|
||||||
localFallback: '返回接口.json',
|
|
||||||
// 初始化API接口列表
|
|
||||||
async init() {
|
|
||||||
try {
|
|
||||||
const res = await fetch('./接口集合.json');
|
|
||||||
const endpoints = await res.json();
|
|
||||||
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/changya`);
|
|
||||||
} catch (e) {
|
|
||||||
// 如果无法加载接口集合,使用默认接口
|
|
||||||
this.endpoints = ['https://60s.api.shumengya.top/v2/changya'];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 获取当前接口URL
|
|
||||||
getCurrentUrl() {
|
|
||||||
if (this.endpoints.length === 0) return null;
|
|
||||||
const url = new URL(this.endpoints[this.currentIndex]);
|
|
||||||
Object.entries(this.params).forEach(([k, v]) => url.searchParams.append(k, v));
|
|
||||||
return url.toString();
|
|
||||||
},
|
|
||||||
// 切换到下一个接口
|
|
||||||
switchToNext() {
|
|
||||||
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length;
|
|
||||||
return this.currentIndex < this.endpoints.length;
|
|
||||||
},
|
|
||||||
// 重置到第一个接口
|
|
||||||
reset() {
|
|
||||||
this.currentIndex = 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// DOM 元素引用
|
|
||||||
const els = {
|
|
||||||
loading: null,
|
|
||||||
error: null,
|
|
||||||
container: null,
|
|
||||||
avatar: null,
|
|
||||||
nickname: null,
|
|
||||||
gender: null,
|
|
||||||
songTitle: null,
|
|
||||||
songMeta: null,
|
|
||||||
lyrics: null,
|
|
||||||
audio: null,
|
|
||||||
likeCount: null,
|
|
||||||
publishTime: null,
|
|
||||||
link: null,
|
|
||||||
refreshBtn: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
function initDom() {
|
|
||||||
els.loading = document.getElementById('loading');
|
|
||||||
els.error = document.getElementById('error');
|
|
||||||
els.container = document.getElementById('content');
|
|
||||||
|
|
||||||
els.avatar = document.getElementById('avatar');
|
|
||||||
els.nickname = document.getElementById('nickname');
|
|
||||||
els.gender = document.getElementById('gender');
|
|
||||||
els.songTitle = document.getElementById('song-title');
|
|
||||||
els.songMeta = document.getElementById('song-meta');
|
|
||||||
els.lyrics = document.getElementById('lyrics');
|
|
||||||
|
|
||||||
els.audio = document.getElementById('audio');
|
|
||||||
els.likeCount = document.getElementById('like-count');
|
|
||||||
els.publishTime = document.getElementById('publish-time');
|
|
||||||
els.link = document.getElementById('link');
|
|
||||||
els.refreshBtn = document.getElementById('refresh-btn');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLoading() {
|
|
||||||
els.loading.style.display = 'block';
|
|
||||||
els.error.style.display = 'none';
|
|
||||||
els.container.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(msg) {
|
|
||||||
els.loading.style.display = 'none';
|
|
||||||
els.error.style.display = 'block';
|
|
||||||
els.container.style.display = 'none';
|
|
||||||
els.error.querySelector('p').textContent = msg || '获取数据失败,请稍后重试';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showContent() {
|
|
||||||
els.loading.style.display = 'none';
|
|
||||||
els.error.style.display = 'none';
|
|
||||||
els.container.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDuration(ms) {
|
|
||||||
if (!ms && ms !== 0) return '';
|
|
||||||
const totalSeconds = Math.floor(ms / 1000);
|
|
||||||
const m = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
|
|
||||||
const s = (totalSeconds % 60).toString().padStart(2, '0');
|
|
||||||
return `${m}:${s}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeText(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text == null ? '' : String(text);
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchFromAPI() {
|
|
||||||
// 初始化API接口列表
|
|
||||||
await API.init();
|
|
||||||
|
|
||||||
// 重置API索引到第一个接口
|
|
||||||
API.reset();
|
|
||||||
|
|
||||||
// 尝试所有API接口
|
|
||||||
for (let i = 0; i < API.endpoints.length; i++) {
|
|
||||||
try {
|
|
||||||
const url = API.getCurrentUrl();
|
|
||||||
console.log(`尝试接口 ${i + 1}/${API.endpoints.length}: ${url}`);
|
|
||||||
|
|
||||||
const resp = await fetch(url, {
|
|
||||||
cache: 'no-store',
|
|
||||||
timeout: 10000 // 10秒超时
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!resp.ok) {
|
|
||||||
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await resp.json();
|
|
||||||
|
|
||||||
if (data && data.code === 200) {
|
|
||||||
console.log(`接口 ${i + 1} 请求成功`);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(data && data.message ? data.message : '接口返回异常');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`接口 ${i + 1} 失败:`, e.message);
|
|
||||||
|
|
||||||
// 如果不是最后一个接口,切换到下一个
|
|
||||||
if (i < API.endpoints.length - 1) {
|
|
||||||
API.switchToNext();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 所有接口都失败了
|
|
||||||
console.warn('所有远程接口都失败,尝试本地数据');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchFromLocal() {
|
|
||||||
try {
|
|
||||||
const resp = await fetch(API.localFallback + `?t=${Date.now()}`);
|
|
||||||
if (!resp.ok) throw new Error(`本地文件HTTP ${resp.status}`);
|
|
||||||
const data = await resp.json();
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('读取本地返回接口.json失败:', e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(data) {
|
|
||||||
const d = data?.data || {};
|
|
||||||
const user = d.user || {};
|
|
||||||
const song = d.song || {};
|
|
||||||
const audio = d.audio || {};
|
|
||||||
|
|
||||||
// 用户信息
|
|
||||||
els.avatar.src = user.avatar_url || '';
|
|
||||||
els.avatar.alt = (user.nickname || '用户') + ' 头像';
|
|
||||||
els.nickname.textContent = user.nickname || '未知用户';
|
|
||||||
els.gender.textContent = user.gender === 'female' ? '女' : user.gender === 'male' ? '男' : '未知';
|
|
||||||
|
|
||||||
// 歌曲信息
|
|
||||||
els.songTitle.textContent = song.name || '未知歌曲';
|
|
||||||
els.songMeta.textContent = song.singer ? `演唱:${song.singer}` : '';
|
|
||||||
|
|
||||||
els.lyrics.innerHTML = '';
|
|
||||||
if (Array.isArray(song.lyrics)) {
|
|
||||||
const frag = document.createDocumentFragment();
|
|
||||||
song.lyrics.forEach(line => {
|
|
||||||
const p = document.createElement('p');
|
|
||||||
p.innerHTML = safeText(line);
|
|
||||||
frag.appendChild(p);
|
|
||||||
});
|
|
||||||
els.lyrics.appendChild(frag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 音频
|
|
||||||
els.audio.src = audio.url || '';
|
|
||||||
els.audio.preload = 'none';
|
|
||||||
|
|
||||||
// 其他信息
|
|
||||||
els.likeCount.textContent = typeof audio.like_count === 'number' ? audio.like_count : '-';
|
|
||||||
const publish = audio.publish || (audio.publish_at ? new Date(audio.publish_at).toLocaleString() : '');
|
|
||||||
els.publishTime.textContent = publish;
|
|
||||||
els.link.href = audio.link || '#';
|
|
||||||
els.link.target = '_blank';
|
|
||||||
|
|
||||||
// 时长信息
|
|
||||||
const durationEl = document.getElementById('duration');
|
|
||||||
durationEl.textContent = formatDuration(audio.duration);
|
|
||||||
|
|
||||||
showContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
showLoading();
|
|
||||||
try {
|
|
||||||
// 先尝试远程API
|
|
||||||
const data = await fetchFromAPI();
|
|
||||||
if (data) {
|
|
||||||
render(data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 远程API失败,尝试本地数据
|
|
||||||
const localData = await fetchFromLocal();
|
|
||||||
if (localData) {
|
|
||||||
render(localData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 都失败了
|
|
||||||
showError('获取数据失败,请稍后重试');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('加载数据时发生错误:', e);
|
|
||||||
showError('获取数据失败,请稍后重试');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindEvents() {
|
|
||||||
if (els.refreshBtn) {
|
|
||||||
els.refreshBtn.addEventListener('click', load);
|
|
||||||
}
|
|
||||||
// 快捷键 Ctrl+R 刷新(不拦截浏览器默认刷新)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
initDom();
|
|
||||||
bindEvents();
|
|
||||||
load();
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[
|
|
||||||
"https://60s.api.shumengya.top"
|
|
||||||
]
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
|
||||||
"data": {
|
|
||||||
"user": {
|
|
||||||
"nickname": "𝑮𝑺_迷鹿_",
|
|
||||||
"gender": "female",
|
|
||||||
"avatar_url": "http://img-cdn.api.singduck.cn/user-img/6afbebcfae6144478c150d0c1d0d5899.jpg"
|
|
||||||
},
|
|
||||||
"song": {
|
|
||||||
"name": "恶作剧",
|
|
||||||
"singer": "王蓝茵",
|
|
||||||
"lyrics": [
|
|
||||||
"我想我会开始想念你",
|
|
||||||
"可是我刚刚才遇见了你",
|
|
||||||
"我怀疑这奇遇只是个恶作剧",
|
|
||||||
"我想我已慢慢喜欢你",
|
|
||||||
"因为我拥有爱情的勇气",
|
|
||||||
"我任性投入你给的恶作剧",
|
|
||||||
"你给的恶作剧"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"audio": {
|
|
||||||
"url": "http://audio-cdn.api.singduck.cn/ugc/220929_965696173_b822a290c553.wav?auth_key=1755845643-0-0-4029539b73e17337dcac49cc4e0ecfcc",
|
|
||||||
"duration": 35050,
|
|
||||||
"like_count": 955,
|
|
||||||
"link": "https://m.api.singduck.cn/user-piece/toGZlBfZbukck2sHb",
|
|
||||||
"publish": "2022/09/29 18:33:51",
|
|
||||||
"publish_at": 1664447631000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
@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; }
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
["https://60s.api.shumengya.top"]
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
|
||||||
"data": {
|
|
||||||
"index": 347,
|
|
||||||
"duanzi": "我不想读书,主要是因为家里牛啊,猪啊羊啊都没人喂。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
56
InfoGenie-frontend/public/60sapi/娱乐消遣/随机段子.html
Normal file
56
InfoGenie-frontend/public/60sapi/娱乐消遣/随机段子.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>随机段子</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'KaiTi','楷体',serif;background:#f9fafb;color:#1f2937;line-height:1.6;min-height:100vh}
|
||||||
|
.header{background:linear-gradient(135deg,#065f46,#059669);color:#fff;padding:16px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:10}
|
||||||
|
.header h1{flex:1;font-size:16px;font-weight:700}.btn{width:36px;height:36px;border:none;border-radius:10px;background:rgba(255,255,255,.15);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px}.btn:hover{background:rgba(255,255,255,.25)}
|
||||||
|
.body{max-width:720px;margin:0 auto;padding:20px 16px 40px;display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 68px)}
|
||||||
|
.quote{background:linear-gradient(135deg,#f0fdf4,#ecfdf5);border-radius:20px;padding:36px 28px;text-align:center;border:1px solid rgba(34,197,94,.12);position:relative;width:100%}
|
||||||
|
.quote::before{content:'"';position:absolute;top:10px;left:20px;font-size:60px;color:rgba(34,197,94,.15);font-family:serif;line-height:1}
|
||||||
|
.quote-text{font-size:18px;line-height:2;color:#374151;font-weight:500}
|
||||||
|
.quote-src{font-size:12px;color:#9ca3af;margin-top:16px}
|
||||||
|
.loader{display:flex;flex-direction:column;align-items:center;padding:60px 0;color:#9ca3af;gap:10px;font-size:13px;width:100%}
|
||||||
|
.spinner{width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#059669;border-radius:50%;animation:spin 1s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.err{text-align:center;padding:40px 16px;color:#ef4444;font-size:14px;width:100%}
|
||||||
|
@media(max-width:640px){.quote-text{font-size:15px}.quote{padding:28px 20px}}
|
||||||
|
</style>
|
||||||
|
<script src="/60sapi/ig-embed.js"></script>
|
||||||
|
<script src="/60sapi/sixty-runtime.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<button class="btn" onclick="history.back()">←</button>
|
||||||
|
<h1>😂 随机段子</h1>
|
||||||
|
<button class="btn" onclick="loadData()">↻</button>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="content">
|
||||||
|
<div class="loader"><div class="spinner"></div><span>加载中...</span></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function loadData(){
|
||||||
|
const el=document.getElementById('content');
|
||||||
|
el.innerHTML='<div class="loader"><div class="spinner"></div><span>加载中...</span></div>';
|
||||||
|
try{
|
||||||
|
const res=await fetch(window.__SIXTY_API_BASE__+'/v2/duanzi?encoding=json',{headers:{Accept:'application/json'}});
|
||||||
|
const json=await res.json();
|
||||||
|
const d=json.data!==undefined?json.data:json;
|
||||||
|
let text='',source='';
|
||||||
|
if(typeof d==='string'){text=d}
|
||||||
|
else if(d&&typeof d==='object'){
|
||||||
|
for(const k of['content','text','hitokoto','duanzi','joke','saying','answer','kfc','data']){if(typeof d[k]==='string'){text=d[k];break}}
|
||||||
|
if(!text){for(const v of Object.values(d)){if(typeof v==='string'&&v.length>5){text=v;break}}}
|
||||||
|
if(!text)text=JSON.stringify(d,null,2);
|
||||||
|
source=d.from||d.source||d.author||'';
|
||||||
|
}else{text=String(d??'')}
|
||||||
|
el.innerHTML=`<div class="quote"><div class="quote-text">${text}</div>${source?`<div class="quote-src">—— ${source}</div>`:''}</div>`;
|
||||||
|
}catch(e){el.innerHTML=`<div class="err">加载失败:${e.message}</div>`}
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
/* 随机答案之书 - 淡绿色清新风格样式(与随机唱歌音频一致) */
|
|
||||||
|
|
||||||
/* 重置样式 */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 50%, #ffd3a5 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
color: #2d5016;
|
|
||||||
line-height: 1.6;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 头部 */
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
background: rgba(255, 255, 255, 0.85);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 24px;
|
|
||||||
box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
color: #2d5016;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header p {
|
|
||||||
color: #5a7c65;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 按钮 */
|
|
||||||
.btn {
|
|
||||||
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 18px;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.25s ease;
|
|
||||||
box-shadow: 0 4px 12px rgba(129, 199, 132, 0.35);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 18px rgba(129, 199, 132, 0.45);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 加载与错误 */
|
|
||||||
.loading, .error {
|
|
||||||
text-align: center;
|
|
||||||
padding: 30px;
|
|
||||||
background: rgba(255, 255, 255, 0.85);
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border: 4px solid #e8f5e8;
|
|
||||||
border-top: 4px solid #81c784;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin: 0 auto 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 动画 */
|
|
||||||
.fade-in {
|
|
||||||
animation: fadeIn 0.5s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 答案卡片 */
|
|
||||||
.answer-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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 手机端优先优化 */
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.container { padding: 12px; }
|
|
||||||
.header { padding: 18px; }
|
|
||||||
.header h1 { font-size: 1.6rem; gap: 8px; }
|
|
||||||
|
|
||||||
.answer-card { padding: 16px; }
|
|
||||||
.answer-text { font-size: 1.3rem; }
|
|
||||||
.answer-en { font-size: 0.95rem; }
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 220px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<!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>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user