update: 2026-03-28 20:59

This commit is contained in:
2026-03-28 20:59:52 +08:00
parent e21d58e603
commit 1c81d4e6ea
611 changed files with 27847 additions and 65061 deletions

8
.gitignore vendored
View File

@@ -20,8 +20,14 @@ __pycache__/
.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)
InfoGenie-backend/.env
.env.local
.env.*.local

113
CLAUDE.md Normal file
View 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.

View File

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

View File

@@ -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

View File

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

View File

@@ -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"]

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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接口==========================

View File

@@ -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)

View File

@@ -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接口==========================

View File

@@ -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限流

View File

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

View File

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

View File

@@ -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使用记录和萌芽币消费情况
---

View File

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

View File

@@ -1,11 +1,7 @@
# React 开发环境变量
# API URL - 开发环境使用本地后端
# 复制为 .env.local 后可按需覆盖本地开发值
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_VERSION=1.0.0
# 调试模式
REACT_APP_DEBUG=true

View File

@@ -1,13 +1,7 @@
# React 生产环境变量
# 用于前端独立部署
# API URL - 指向后端服务器
# 使用域名方式访问后端 API
# 生产前端环境变量
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_VERSION=1.0.0
# 调试模式(生产环境关闭)
REACT_APP_DEBUG=false

View File

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

View File

@@ -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

View File

@@ -83,7 +83,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@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",
"integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@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",
"integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.1",
"@babel/helper-module-imports": "^7.27.1",
@@ -4682,7 +4679,8 @@
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/q": {
"version": "1.5.8",
@@ -4838,7 +4836,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.4.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",
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
@@ -5262,7 +5258,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5361,7 +5356,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -6327,7 +6321,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001737",
"electron-to-chromium": "^1.5.211",
@@ -6486,9 +6479,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001739",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz",
"integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==",
"version": "1.0.30001780",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz",
"integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==",
"funding": [
{
"type": "opencollective",
@@ -7394,8 +7387,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@@ -8261,7 +8253,6 @@
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -11096,7 +11087,6 @@
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^27.5.1",
"import-local": "^3.0.2",
@@ -15168,7 +15158,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -16356,7 +16345,6 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"license": "MIT",
"peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -16722,7 +16710,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -16873,7 +16860,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -16925,7 +16911,6 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -17444,7 +17429,6 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -17690,7 +17674,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -19396,7 +19379,6 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"license": "(MIT OR CC0-1.0)",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -19826,7 +19808,6 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -19898,7 +19879,6 @@
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz",
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/bonjour": "^3.5.9",
"@types/connect-history-api-fallback": "^1.3.5",
@@ -20311,7 +20291,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",

View File

@@ -1,7 +1,7 @@
{
"name": "infogenie-frontend",
"version": "1.0.0",
"description": "万象口袋 - 前端React应用",
"description": "万象口袋 - 前端React应用",
"keywords": [
"react",
"api",

View 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) { /* 忽略 */ }
})();

View 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;
})();

View 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
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>

View 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,'&amp;').replace(/"/g,'&quot;');
}
function escapeHtml(s){
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
/** 从 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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('网络连接已断开');
});
}

View File

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

View File

@@ -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` 实例都需要这个方法。这将浪费大量内存空间,因为它们仍然具有该属性,这将占用每个实例的内存空间。相反,如果我们只将它添加到原型中,那么它只存在于内存中的一个位置,但是所有实例都可以访问它!"
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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();
}
}
});

View File

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

View File

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

View 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>

View File

@@ -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%);
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

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

View File

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

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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>&copy; 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>

View File

@@ -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();
});

View File

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

View File

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

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

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

View File

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

View 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>

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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();
});
})();

View File

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

View File

@@ -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
}
}
}

View File

@@ -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;
}

View File

@@ -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; }
}

View File

@@ -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>

View File

@@ -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();
});

View File

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

View File

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

View 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>

View File

@@ -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;
}
}

View File

@@ -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