继续更新

This commit is contained in:
2025-10-15 11:11:23 +08:00
parent 7786e5f507
commit c147502b4d
89 changed files with 8116 additions and 2505 deletions

View File

@@ -1,50 +0,0 @@
# Git
.git
.gitignore
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.venv/
pip-log.txt
pip-delete-this-directory.txt
.tox/
.coverage
.coverage.*
.pytest_cache/
# 环境变量文件
.env
.env.production
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# 日志文件
*.log
# 测试文件(可选,如果不想包含在镜像中)
test/
# 文档文件(可选)
*.md
LICENSE
# 启动脚本Windows
*.bat
# 其他临时文件
*.tmp
.cache/

View File

@@ -2,12 +2,11 @@
# 请勿将此文件提交到版本控制系统
# 邮件配置
# 请将下面的邮箱地址替换为您的实际QQ邮箱
MAIL_USERNAME=3205788256@qq.com
MAIL_PASSWORD=szcaxvbftusqddhi
MAIL_USERNAME=shumengya888@foxmail.com
MAIL_PASSWORD=dpdouefloajfdagd
# 数据库配置
MONGO_URI=mongodb://shumengya:tyh%4019900420@47.108.90.0:27018/InfoGenie?authSource=admin
MONGO_URI=mongodb://shumengya:shumengya520@47.108.90.0:27017/InfoGenie?authSource=admin
# 应用密钥
SECRET_KEY=infogenie-secret-key-2025

View File

@@ -1,14 +0,0 @@
# 生产环境配置
# MongoDB配置
MONGO_URI=mongodb://用户名:密码@主机地址:端口/InfoGenie?authSource=admin
# 邮件配置
MAIL_USERNAME=your-email@qq.com
MAIL_PASSWORD=your-app-password
# 应用密钥
SECRET_KEY=infogenie-production-secret-key-2025
# 会话安全配置
HWT_SECURE=True

View File

@@ -1,32 +0,0 @@
# 使用官方Python镜像作为基础镜像
FROM python:3.10-slim
# 设置工作目录
WORKDIR /app
# 安装系统依赖(如果需要)
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# 复制requirements.txt并安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY . .
# 创建非root用户安全最佳实践
RUN useradd --create-home --shell /bin/bash app \
&& chown -R app:app /app
USER app
# 暴露端口
EXPOSE 5002
# 设置环境变量
ENV FLASK_APP=app.py
ENV FLASK_ENV=production
# 启动命令
CMD ["python", "app.py"]

View File

@@ -50,8 +50,10 @@ def create_app():
def index():
"""API根路径"""
return jsonify({
'message': '万象口袋 API 服务运行中',
'version': '1.0.0',
'message': '万象口袋 后端 API 服务运行中',
"description": "提供用户认证、用户管理、聚合API、小游戏接口和AI模型应用接口",
"email":"shumengya666@outlook.com",
'version': '2.2.0',
'timestamp': datetime.now().isoformat(),
'endpoints': {
'auth': '/api/auth',

View File

@@ -1,118 +0,0 @@
#!/bin/bash
# InfoGenie 后端 Docker 镜像构建脚本
# Created by: 万象口袋
# Date: 2025-09-16
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 配置
IMAGE_NAME="infogenie-backend"
IMAGE_TAG="latest"
DOCKERFILE_PATH="."
# 函数:打印信息
print_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 检查Docker是否安装
check_docker() {
if ! command -v docker &> /dev/null; then
print_error "Docker 未安装,请先安装 Docker"
exit 1
fi
print_info "Docker 版本: $(docker --version)"
}
# 检查Dockerfile是否存在
check_dockerfile() {
if [ ! -f "Dockerfile" ]; then
print_error "Dockerfile 不存在"
exit 1
fi
print_info "找到 Dockerfile"
}
# 构建Docker镜像
build_image() {
print_info "开始构建 Docker 镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
# 构建镜像
docker build \
--no-cache \
-t ${IMAGE_NAME}:${IMAGE_TAG} \
-f ${DOCKERFILE_PATH}/Dockerfile \
${DOCKERFILE_PATH}
if [ $? -eq 0 ]; then
print_info "Docker 镜像构建成功!"
print_info "镜像信息:"
docker images ${IMAGE_NAME}:${IMAGE_TAG}
else
print_error "Docker 镜像构建失败"
exit 1
fi
}
# 显示使用说明
show_usage() {
echo ""
print_info "构建完成! 使用方法:"
echo ""
echo "1. 运行容器 (需要MongoDB):"
echo " docker run -d \\"
echo " --name infogenie-backend \\"
echo " -p 5002:5002 \\"
echo " -e MONGO_URI=mongodb://host.docker.internal:27017/InfoGenie \\"
echo " -e SECRET_KEY=your-secret-key \\"
echo " -e MAIL_USERNAME=your-email@qq.com \\"
echo " -e MAIL_PASSWORD=your-app-password \\"
echo " ${IMAGE_NAME}:${IMAGE_TAG}"
echo ""
echo "2. 使用 Docker Compose (推荐):"
echo " 创建 docker-compose.yml 文件并运行:"
echo " docker-compose up -d"
echo ""
echo "3. 查看日志:"
echo " docker logs infogenie-backend"
echo ""
echo "4. 停止容器:"
echo " docker stop infogenie-backend"
echo " docker rm infogenie-backend"
}
# 主函数
main() {
print_info "InfoGenie 后端 Docker 镜像构建脚本"
print_info "=================================="
# 检查环境
check_docker
check_dockerfile
# 构建镜像
build_image
# 显示使用说明
show_usage
print_info "构建脚本执行完成!"
}
# 执行主函数
main "$@"

View File

@@ -1,53 +0,0 @@
version: '3.8'
services:
# InfoGenie 后端服务
infogenie-backend:
build:
context: .
dockerfile: Dockerfile
ports:
- "5002:5002"
environment:
- FLASK_ENV=production
- SECRET_KEY=${SECRET_KEY:-infogenie-secret-key-2025}
- MONGO_URI=mongodb://mongodb:27017/InfoGenie
- MAIL_USERNAME=${MAIL_USERNAME:-your-email@qq.com}
- MAIL_PASSWORD=${MAIL_PASSWORD:-your-app-password}
- HWT_SECURE=false
depends_on:
- mongodb
networks:
- infogenie-network
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5002/api/health"]
interval: 30s
timeout: 10s
retries: 3
# MongoDB 数据库
mongodb:
image: mongo:6.0
ports:
- "27017:27017"
environment:
- MONGO_INITDB_DATABASE=InfoGenie
volumes:
- mongodb_data:/data/db
- ./mongo-init:/docker-entrypoint-initdb.d
networks:
- infogenie-network
restart: unless-stopped
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 30s
timeout: 10s
retries: 3
volumes:
mongodb_data:
networks:
infogenie-network:
driver: bridge

View File

@@ -66,14 +66,14 @@ def send_verification_email(email, verification_type='register'):
# 邮件模板
if verification_type == 'register':
subject = 'InfoGenie】注册验证码'
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;">InfoGenie 万象口袋</h1>
<p style="color: #666; font-size: 14px; margin: 5px 0;">欢迎注册InfoGenie</p>
<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;">

View File

@@ -57,8 +57,21 @@ def login_required(f):
def get_profile():
"""获取用户资料"""
try:
hwt = getattr(request, 'hwt', {})
user_id = hwt.get('user_id')
# 优先从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:
@@ -68,11 +81,16 @@ def get_profile():
}), 404
# 返回用户信息(不包含密码)
profile = {
'account': user['账号'],
'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')
'status': user.get('用户状态', 'active'),
'level': user.get('等级', 1),
'experience': user.get('经验', 0),
'coins': user.get('萌芽币', 0)
}
return jsonify({
'success': True,

View File

@@ -1,166 +1,396 @@
# InfoGenie后端项目专业技术总结
# InfoGenie 后端架构文档
## 项目架构概述
## 项目概述
InfoGenie后端采用了**模块化、松耦合**的设计理念,基于Flask框架构建RESTful API服务实现了前后端完全分离的现代Web应用架构。整体架构遵循了**单一职责原则**和**关注点分离原则**各模块独立封装通过清晰定义的API接口进行交互
InfoGenie(万象口袋)是一个基于前后端分离架构的多功能聚合软件应用。后端采用Flask框架提供RESTful API服务前端通过HTTP请求调用后端API实现数据交互和业务逻辑处理
## 核心技术栈
## 技术栈
### 基础框架
- **Web框架**: Flask 2.3.3(轻量、灵活、可扩展)
- **API设计**: RESTful架构资源导向、无状态通信
- **数据库**: MongoDB适用于文档型数据存储通过Flask-PyMongo 2.3.0集成)
- **认证机制**: JWT TokenPyJWT 2.8.0支持7天有效期
### 核心框架
- **Web框架**: Flask 2.3.3
- **数据库**: MongoDB (Flask-PyMongo 2.3.0)
- **认证**: JWT (PyJWT 2.8.0)
- **跨域**: Flask-CORS 4.0.0
### 中间件与辅助工具
- **CORS支持**: Flask-CORS 4.0.0(解决跨域资源共享问题)
- **密码安全**: Werkzeug 2.3.7(提供高强度密码哈希功能)
- **邮件服务**: 基于SMTP协议的邮件发送使用smtplib直接实现无依赖Flask-Mail
- **环境配置**: python-dotenv 1.0.0(分离配置与代码,增强安全性)
- **API限流**: Flask-Limiter 3.5.0防止API滥用提高系统稳定性
### 辅助工具
- **邮件服务**: Flask-Mail 0.9.1
- **密码加密**: Werkzeug 2.3.7
- **环境配置**: python-dotenv 1.0.0
- **API限流**: Flask-Limiter 3.5.0
## 架构设计亮点
## 架构设计原则
### 1. 应用工厂模式
项目采用**应用工厂模式**Factory Pattern创建Flask应用实例便于测试和多环境部署
```python
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
# 初始化各种扩展和注册蓝图
return app
### 前后端分离
- 后端专注于数据处理和业务逻辑
- 前端负责用户界面和交互体验
- 通过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 # 检查登录状态
```
### 2. 蓝图模块化设计
采用Flask蓝图Blueprint实现功能模块化提高代码复用性和可维护性
- `auth_bp`: 用户认证模块
- `user_bp`: 用户管理模块
- `aimodelapp_bp`: AI模型应用模块
**数据流程**:
1. 前端发送注册/登录请求
2. 后端验证邮箱格式仅支持QQ邮箱
3. 发送验证码邮件到用户邮箱
4. 用户输入验证码完成验证
5. 验证成功后生成JWT Token返回给前端
### 3. 装饰器模式
大量使用装饰器模式实现横切关注点Cross-cutting Concerns如认证、权限验证、萌芽币消费等
```python
@verify_user_coins
def ai_function_endpoint():
# 业务逻辑
**安全特性**:
- 密码使用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 # 删除账户
```
### 4. 统一响应格式
实现了一致的API响应格式便于前端处理
```json
{
"success": true|false,
"data": {},
"message": "操作信息",
"timestamp": "ISO格式时间戳"
}
```
## 安全设计分析
### 1. 多层次认证体系
- **JWT Token认证**: 无状态认证机制,适合分布式部署
- **验证码邮箱认证**: 双因素认证提高安全性
- **QQ邮箱格式验证**: 限制注册邮箱类型,减少垃圾注册
### 2. 数据安全措施
- **密码哈希存储**: 使用Werkzeug提供的高强度哈希算法
- **敏感配置外部化**: 通过环境变量注入敏感配置
- **路径遍历防护**: 静态文件服务实现了路径限制检查
```python
if not os.path.commonpath([base_directory, full_path]) == base_directory:
return jsonify({'error': '非法文件路径'}), 403
```
### 3. 请求安全控制
- **API限流**: 防止暴力攻击和资源耗尽
- **CORS限制**: 生产环境可配置严格的跨域策略
- **请求参数验证**: 严格验证所有客户端输入
## 业务模块分析
### 1. 认证模块auth.py
实现了基于JWT的无状态认证系统通过邮箱验证码进行用户身份确认支持注册、登录和会话管理。设计重点包括
- 验证码5分钟有效期机制
- JWT token 7天有效期管理
- 认证装饰器实现代码复用
### 2. 用户管理模块user_management.py
负责用户资料、签到系统、萌芽币管理等核心业务功能,实现了:
- 用户资料CRUD操作
- 每日签到奖励系统(经验值和萌芽币)
- 用户等级动态计算逻辑
### 3. AI模型应用模块aimodelapp.py
集成多种AI服务DeepSeek、Kimi并实现统一接口调用特点
- 萌芽币消费装饰器模式每次调用消耗100萌芽币
- AI调用带重试机制提高系统稳定性
- 多模型提供商支持(提高可用性和容错性)
### 4. 邮件服务模块email_service.py
负责验证码邮件发送、QQ邮箱格式验证等功能特点
- 直接使用smtplib实现减少依赖
- HTML格式邮件模板支持
- 验证码管理机制内存存储生产环境建议使用Redis
## 数据库设计
采用MongoDB文档型数据库主要集合为`userdata`存储用户相关所有数据。MongoDB的选择优势
- **灵活的数据结构**: 适合存储复杂且不断演化的用户数据
- **文档自包含**: 减少关联查询,提高读取性能
- **水平扩展能力**: 支持未来系统规模扩展需求
用户数据模型设计合理,包含核心字段:
**数据结构**:
```json
{
"邮箱": "user@qq.com",
"用户名": "用户名",
"密码": "哈希密码",
"头像": "QQ头像URL",
"注册时间": "ISO时间格式",
"注册时间": "2025-01-01T00:00:00",
"最后登录": "2025-01-01T00:00:00",
"登录次数": 10,
"用户状态": "active",
"等级": 5,
"经验": 1200,
"萌芽币": 1500,
"签到系统": {
"连续签到天数": 7,
"今日是否已签到": true
"今日是否已签到": true,
"签到时间": "2025-01-01"
}
}
```
## 部署与运维
**业务逻辑**:
- 签到奖励300萌芽币 + 200经验
- 等级升级100 × 1.2^(等级) 经验需求
### 多环境配置支持
实现了开发、测试和生产环境的配置分离:
```python
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig,
'default': DevelopmentConfig
### 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"]
}
}
```
### Docker化部署
提供了完整的Docker化部署方案
- Dockerfile定义应用容器
- docker-compose.yml配置多容器协作
- 支持环境变量注入敏感配置
**调用流程**:
1. 前端发送AI请求包含消息、模型提供商等参数
2. 后端加载AI配置文件
3. 调用对应AI API带重试机制
4. 返回AI响应给前端
## 技术亮点与优化空间
## API设计规范
### 亮点
1. **模块化设计**: 通过Flask蓝图实现功能解耦
2. **装饰器封装**: 横切关注点(cross-cutting concerns)集中处理
3. **统一错误处理**: 全局一致的错误响应机制
4. **AI服务抽象**: 屏蔽不同AI提供商的实现差异
### 请求/响应格式
### 优化空间
1. **缓存机制**: 可引入Redis缓存验证码、热点数据等
2. **异步处理**: 邮件发送、AI调用等耗时操作可改为异步执行
3. **日志系统**: 增强日志记录和监控能力
4. **单元测试**: 增加自动化测试覆盖率
**成功响应**:
```json
{
"success": true,
"data": {...},
"message": "操作成功",
"timestamp": "2025-01-01T00:00:00"
}
```
## 结论
**错误响应**:
```json
{
"success": false,
"message": "错误信息",
"error": "错误详情"
}
```
InfoGenie后端项目展现了良好的软件工程实践采用模块化设计、RESTful API架构和多层次安全控制构建了一个可扩展、可维护的后端系统。该项目不仅满足了当前的业务需求还为未来功能扩展和性能优化预留了空间。
### 认证方式
特别是在AI功能集成方面通过抽象接口和装饰器模式实现了业务逻辑与技术实现的分离体现了良好的软件设计原则。萌芽币消费系统的实现也展示了面向业务模型的领域设计能力。
**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

@@ -19,6 +19,7 @@
"react-icons": "^4.11.0",
"react-router-dom": "^6.15.0",
"react-scripts": "5.0.1",
"react-transition-group": "^4.4.5",
"styled-components": "^6.0.7",
"web-vitals": "^2.1.4"
}
@@ -82,6 +83,7 @@
"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",
@@ -731,6 +733,7 @@
"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"
},
@@ -1614,6 +1617,7 @@
"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",
@@ -4678,8 +4682,7 @@
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/q": {
"version": "1.5.8",
@@ -4835,6 +4838,7 @@
"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",
@@ -4888,6 +4892,7 @@
"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",
@@ -5257,6 +5262,7 @@
"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"
},
@@ -5355,6 +5361,7 @@
"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",
@@ -6320,6 +6327,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001737",
"electron-to-chromium": "^1.5.211",
@@ -7386,7 +7394,8 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@@ -7746,6 +7755,16 @@
"utila": "~0.4"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@@ -8242,6 +8261,7 @@
"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",
@@ -11076,6 +11096,7 @@
"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",
@@ -15147,6 +15168,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -16334,6 +16356,7 @@
"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"
@@ -16699,6 +16722,7 @@
"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"
},
@@ -16849,6 +16873,7 @@
"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"
@@ -16900,6 +16925,7 @@
"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"
}
@@ -17009,6 +17035,22 @@
}
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -17402,6 +17444,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -17647,6 +17690,7 @@
"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",
@@ -19352,6 +19396,7 @@
"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"
},
@@ -19781,6 +19826,7 @@
"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",
@@ -19852,6 +19898,7 @@
"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",
@@ -20264,6 +20311,7 @@
"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

@@ -2,7 +2,12 @@
"name": "infogenie-frontend",
"version": "1.0.0",
"description": "✨ 万象口袋 - 前端React应用",
"keywords": ["react", "api", "mobile-first", "responsive"],
"keywords": [
"react",
"api",
"mobile-first",
"responsive"
],
"author": "万象口袋",
"license": "MIT",
"private": true,
@@ -11,14 +16,15 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.5.2",
"axios": "^1.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-icons": "^4.11.0",
"react-router-dom": "^6.15.0",
"react-scripts": "5.0.1",
"axios": "^1.5.0",
"react-transition-group": "^4.4.5",
"styled-components": "^6.0.7",
"react-icons": "^4.11.0",
"react-hot-toast": "^2.4.1",
"web-vitals": "^2.1.4"
},
"scripts": {

View File

@@ -2,42 +2,32 @@
body {
background: linear-gradient(
135deg,
#ff6b6b 0%,
#4ecdc4 12.5%,
#45b7d1 25%,
#96ceb4 37.5%,
#feca57 50%,
#ff9ff3 62.5%,
#54a0ff 75%,
#5f27cd 87.5%,
#00d2d3 100%
#e8f5e8 0%,
#f1f8e9 25%,
#dcedc8 50%,
#c8e6c8 75%,
#e8f5e8 100%
);
background-size: 400% 400%;
animation: rainbowGradient 15s ease infinite;
background-size: 200% 200%;
animation: gentleGradient 20s ease infinite;
background-attachment: fixed;
min-height: 100vh;
position: relative;
}
@keyframes rainbowGradient {
@keyframes gentleGradient {
0% {
background-position: 0% 50%;
}
25% {
background-position: 100% 50%;
}
50% {
background-position: 100% 100%;
}
75% {
background-position: 0% 100%;
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/* 彩虹装饰层 */
/* 淡雅绿色装饰层 */
body::before {
content: '';
position: fixed;
@@ -46,17 +36,15 @@ body::before {
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 20%, rgba(255, 107, 107, 0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(78, 205, 196, 0.15) 0%, transparent 50%),
radial-gradient(circle at 40% 80%, rgba(69, 183, 209, 0.12) 0%, transparent 40%),
radial-gradient(circle at 60% 20%, rgba(150, 206, 180, 0.12) 0%, transparent 40%),
radial-gradient(circle at 80% 40%, rgba(254, 202, 87, 0.1) 0%, transparent 35%);
radial-gradient(circle at 20% 20%, rgba(129, 199, 132, 0.08) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(165, 214, 167, 0.06) 0%, transparent 50%),
radial-gradient(circle at 40% 80%, rgba(200, 230, 201, 0.05) 0%, transparent 40%),
radial-gradient(circle at 60% 20%, rgba(220, 237, 200, 0.04) 0%, transparent 40%);
pointer-events: none;
z-index: -1;
animation: float 20s ease-in-out infinite alternate;
}
/* 彩虹粒子效果 */
/* 淡雅绿色点缀 */
body::after {
content: '';
position: fixed;
@@ -65,42 +53,13 @@ body::after {
width: 100%;
height: 100%;
background-image:
radial-gradient(circle at 10% 10%, rgba(255, 107, 107, 0.8) 2px, transparent 2px),
radial-gradient(circle at 30% 20%, rgba(78, 205, 196, 0.8) 1.5px, transparent 1.5px),
radial-gradient(circle at 50% 30%, rgba(69, 183, 209, 0.8) 1px, transparent 1px),
radial-gradient(circle at 70% 40%, rgba(150, 206, 180, 0.8) 2px, transparent 2px),
radial-gradient(circle at 90% 50%, rgba(254, 202, 87, 0.8) 1.5px, transparent 1.5px),
radial-gradient(circle at 20% 60%, rgba(255, 159, 243, 0.8) 1px, transparent 1px),
radial-gradient(circle at 40% 70%, rgba(84, 160, 255, 0.8) 2px, transparent 2px),
radial-gradient(circle at 60% 80%, rgba(95, 39, 205, 0.8) 1.5px, transparent 1.5px),
radial-gradient(circle at 80% 90%, rgba(0, 210, 211, 0.8) 1px, transparent 1px);
background-size: 200px 200px, 250px 250px, 180px 180px, 300px 300px, 220px 220px, 160px 160px, 280px 280px, 240px 240px, 200px 200px;
animation: sparkle 25s linear infinite;
radial-gradient(circle at 15% 15%, rgba(129, 199, 132, 0.3) 1px, transparent 1px),
radial-gradient(circle at 45% 25%, rgba(165, 214, 167, 0.25) 1px, transparent 1px),
radial-gradient(circle at 75% 35%, rgba(200, 230, 201, 0.2) 1px, transparent 1px),
radial-gradient(circle at 25% 65%, rgba(220, 237, 200, 0.15) 1px, transparent 1px),
radial-gradient(circle at 85% 75%, rgba(129, 199, 132, 0.2) 1px, transparent 1px);
background-size: 300px 300px, 400px 400px, 350px 350px, 450px 450px, 380px 380px;
pointer-events: none;
z-index: -1;
opacity: 0.6;
}
@keyframes float {
0% {
transform: translateY(0px) rotate(0deg);
}
100% {
transform: translateY(-15px) rotate(2deg);
}
}
@keyframes sparkle {
0%, 100% {
transform: translateX(0) translateY(0) scale(1);
}
25% {
transform: translateX(-10px) translateY(-5px) scale(1.1);
}
50% {
transform: translateX(10px) translateY(-10px) scale(0.9);
}
75% {
transform: translateX(-5px) translateY(-15px) scale(1.05);
}
opacity: 0.4;
}

View File

@@ -18,11 +18,11 @@
height: 200%;
background: linear-gradient(
135deg,
rgba(64, 169, 255, 0.4) 0%,
rgba(120, 192, 255, 0.3) 25%,
rgba(255, 175, 64, 0.2) 50%,
rgba(255, 140, 50, 0.3) 75%,
rgba(255, 122, 69, 0.4) 100%
rgba(129, 199, 132, 0.4) 0%,
rgba(165, 214, 167, 0.3) 25%,
rgba(200, 230, 201, 0.2) 50%,
rgba(220, 237, 200, 0.3) 75%,
rgba(232, 245, 233, 0.4) 100%
);
animation: gradient-flow 20s ease-in-out infinite;
border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%;
@@ -37,11 +37,11 @@
height: 100%;
background: radial-gradient(
circle at 30% 70%,
rgba(64, 169, 255, 0.5) 0%,
rgba(129, 199, 132, 0.5) 0%,
transparent 50%
), radial-gradient(
circle at 70% 30%,
rgba(255, 140, 50, 0.4) 0%,
rgba(165, 214, 167, 0.4) 0%,
transparent 50%
);
animation: pulse-effect 15s ease-in-out infinite alternate;
@@ -123,7 +123,7 @@ header, .header {
}
header h1, .title {
background: linear-gradient(135deg, #4096ff, #ff7a45);
background: linear-gradient(135deg, #66bb6a, #81c784);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
@@ -170,9 +170,9 @@ header h1, .title {
}
.tab-btn.active {
background: linear-gradient(135deg, #4096ff, #40a9ff);
background: linear-gradient(135deg, #66bb6a, #81c784);
color: white;
box-shadow: 0 4px 16px rgba(64, 150, 255, 0.3);
box-shadow: 0 4px 16px rgba(102, 187, 106, 0.3);
}
.tab-icon {
@@ -180,7 +180,7 @@ header h1, .title {
}
.refresh-btn {
background: linear-gradient(135deg, #52c41a, #73d13d);
background: linear-gradient(135deg, #81c784, #a5d6a7);
border: none;
padding: 12px 24px;
border-radius: 25px;
@@ -193,12 +193,12 @@ header h1, .title {
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.3);
box-shadow: 0 4px 12px rgba(129, 199, 132, 0.3);
}
.refresh-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(82, 196, 26, 0.4);
box-shadow: 0 6px 16px rgba(129, 199, 132, 0.4);
}
.btn-icon {
@@ -238,17 +238,17 @@ header h1, .title {
.hot-item:hover {
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
border-color: rgba(64, 169, 255, 0.3);
border-color: rgba(129, 199, 132, 0.3);
}
.hot-rank {
font-size: 1.2rem;
font-weight: bold;
color: #4096ff;
color: #66bb6a;
margin-right: 18px;
min-width: 38px;
text-align: center;
background-color: rgba(64, 169, 255, 0.1);
background-color: rgba(129, 199, 132, 0.1);
border-radius: 50%;
width: 38px;
height: 38px;
@@ -258,18 +258,18 @@ header h1, .title {
}
.hot-rank.top-1 {
background: linear-gradient(135deg, #ff4d4f, #ff7a45);
background: linear-gradient(135deg, #66bb6a, #81c784);
color: white;
}
.hot-rank.top-2 {
background: linear-gradient(135deg, #ff7a45, #ffa940);
background: linear-gradient(135deg, #81c784, #a5d6a7);
color: white;
}
.hot-rank.top-3 {
background: linear-gradient(135deg, #ffa940, #ffec3d);
color: white;
background: linear-gradient(135deg, #a5d6a7, #c8e6c9);
color: #333;
}
.hot-content {
@@ -288,7 +288,7 @@ header h1, .title {
}
.hot-title:hover {
color: #4096ff;
color: #66bb6a;
text-decoration: none;
}
@@ -310,13 +310,10 @@ header h1, .title {
.rainbow-spinner {
width: 50px;
height: 50px;
border: 4px solid transparent;
border-top: 4px solid #4096ff;
border: 4px solid rgba(129, 199, 132, 0.2);
border-top: 4px solid #81c784;
border-radius: 50%;
animation: spin 1s linear infinite;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #feca57, #ff9ff3, #54a0ff, #5f27cd);
background-size: 400% 400%;
animation: spin 1s linear infinite, rainbowGradient 3s ease infinite;
}
@keyframes spin {
@@ -356,7 +353,7 @@ header h1, .title {
.loading-dots span {
width: 8px;
height: 8px;
background: #4096ff;
background: #81c784;
border-radius: 50%;
animation: loadingDots 1.4s ease-in-out infinite both;
}
@@ -396,7 +393,7 @@ header h1, .title {
.news-item:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
border-color: rgba(64, 169, 255, 0.2);
border-color: rgba(129, 199, 132, 0.3);
}
/* 排名容器 */
@@ -423,21 +420,21 @@ header h1, .title {
}
.news-rank.rank-1 {
background: linear-gradient(135deg, #ff4d4f, #ff7a45);
background: linear-gradient(135deg, #66bb6a, #81c784);
color: white;
box-shadow: 0 4px 12px rgba(255, 77, 79, 0.3);
box-shadow: 0 4px 12px rgba(102, 187, 106, 0.3);
}
.news-rank.rank-2 {
background: linear-gradient(135deg, #ff7a45, #ffa940);
background: linear-gradient(135deg, #81c784, #a5d6a7);
color: white;
box-shadow: 0 4px 12px rgba(255, 122, 69, 0.3);
box-shadow: 0 4px 12px rgba(129, 199, 132, 0.3);
}
.news-rank.rank-3 {
background: linear-gradient(135deg, #ffa940, #ffec3d);
background: linear-gradient(135deg, #a5d6a7, #c8e6c9);
color: #333;
box-shadow: 0 4px 12px rgba(255, 169, 64, 0.3);
box-shadow: 0 4px 12px rgba(165, 214, 167, 0.3);
}
.rank-number {
@@ -478,7 +475,7 @@ header h1, .title {
}
.news-title:hover {
color: #4096ff;
color: #66bb6a;
}
/* 元信息行 */
@@ -524,7 +521,7 @@ header h1, .title {
display: flex;
align-items: center;
gap: 4px;
background: linear-gradient(135deg, #ff6b6b, #4ecdc4);
background: linear-gradient(135deg, #81c784, #a5d6a7);
color: white;
padding: 4px 10px;
border-radius: 12px;
@@ -545,7 +542,7 @@ header h1, .title {
display: flex;
align-items: center;
gap: 4px;
background: linear-gradient(135deg, #4096ff, #40a9ff);
background: linear-gradient(135deg, #66bb6a, #81c784);
color: white;
text-decoration: none;
padding: 6px 12px;
@@ -553,13 +550,13 @@ header h1, .title {
font-size: 0.75rem;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(64, 150, 255, 0.3);
box-shadow: 0 2px 6px rgba(102, 187, 106, 0.3);
flex-shrink: 0;
}
.news-link:hover {
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(64, 150, 255, 0.4);
box-shadow: 0 4px 10px rgba(102, 187, 106, 0.4);
text-decoration: none;
color: white;
}
@@ -592,7 +589,7 @@ header h1, .title {
}
.error-content h3 {
color: #ff4d4f;
color: #66bb6a;
margin: 0;
font-size: 1.3rem;
}
@@ -604,7 +601,7 @@ header h1, .title {
}
.retry-btn {
background: linear-gradient(135deg, #ff4d4f, #ff7a45);
background: linear-gradient(135deg, #66bb6a, #81c784);
border: none;
padding: 12px 24px;
border-radius: 25px;
@@ -616,12 +613,12 @@ header h1, .title {
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 12px rgba(255, 77, 79, 0.3);
box-shadow: 0 4px 12px rgba(102, 187, 106, 0.3);
}
.retry-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(255, 77, 79, 0.4);
box-shadow: 0 6px 16px rgba(102, 187, 106, 0.4);
}
footer {
@@ -855,9 +852,9 @@ footer {
.modern-gradient {
background: linear-gradient(
135deg,
rgba(64, 169, 255, 0.3) 0%,
rgba(255, 175, 64, 0.2) 50%,
rgba(255, 122, 69, 0.25) 100%
rgba(129, 199, 132, 0.3) 0%,
rgba(200, 230, 201, 0.2) 50%,
rgba(232, 245, 233, 0.25) 100%
);
}
}

View File

@@ -3,39 +3,33 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🔥 HackerNews 热门榜单</title>
<title>HackerNews 热门榜单</title>
<link rel="stylesheet" href="css/background.css">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container">
<header class="header">
<div class="header-icon">🌈</div>
<h1 class="title">🔥 HackerNews 热门榜单 💻</h1>
<h1 class="title">HackerNews 热门榜单</h1>
<p class="subtitle">全球技术社区 · 实时热门话题</p>
<div class="tab-container">
<button class="tab-btn active" data-type="top">
<span class="tab-icon">🏆</span>
热门榜
</button>
<button class="tab-btn" data-type="new">
<span class="tab-icon">🆕</span>
最新榜
</button>
<button class="tab-btn" data-type="best">
<span class="tab-icon"></span>
最佳榜
</button>
</div>
<div class="update-time">
<span class="time-icon"></span>
<span id="updateTime">加载中...</span>
</div>
<button id="refreshBtn" class="refresh-btn">
<span class="btn-icon">🔄</span>
刷新数据
</button>
</header>
@@ -44,7 +38,6 @@
<div class="loading-content">
<div class="rainbow-spinner"></div>
<div class="loading-text">
<span class="loading-emoji">🚀</span>
<p>正在获取最新榜单...</p>
<div class="loading-dots">
<span></span>
@@ -61,11 +54,9 @@
<div class="error-message" id="errorMessage" style="display: none;">
<div class="error-content">
<div class="error-icon">💥</div>
<h3>加载失败了</h3>
<p>网络连接出现问题,请稍后重试</p>
<button onclick="loadNewsList()" class="retry-btn">
<span>🔄</span>
重新加载
</button>
</div>

View File

@@ -118,35 +118,26 @@ function createNewsItem(item, rank) {
const formattedScore = formatScore(item.score);
const formattedTime = formatTime(item.created);
// 根据排名添加特殊标识
let rankEmoji = '';
if (rank === 1) rankEmoji = '🏆';
else if (rank === 2) rankEmoji = '🥈';
else if (rank === 3) rankEmoji = '🥉';
// 根据评分添加热度指示
let heatLevel = '';
if (item.score >= 1000) heatLevel = '🔥🔥🔥';
else if (item.score >= 500) heatLevel = '🔥🔥';
else if (item.score >= 100) heatLevel = '🔥';
else heatLevel = '💫';
if (item.score >= 1000) heatLevel = 'HOT';
else if (item.score >= 500) heatLevel = 'WARM';
else if (item.score >= 100) heatLevel = 'COOL';
else heatLevel = 'NEW';
newsItem.innerHTML = `
<div class="news-rank-container">
<div class="${rankClass}">
<span class="rank-number">${rank}</span>
<span class="rank-emoji">${rankEmoji}</span>
</div>
</div>
<div class="news-content-wrapper">
<h3 class="news-title">${escapeHtml(item.title)}</h3>
<div class="news-meta-row">
<div class="news-author">
<span class="meta-icon">👤</span>
<span class="meta-text">${escapeHtml(item.author)}</span>
</div>
<div class="news-time">
<span class="meta-icon">🕒</span>
<span class="meta-text">${formattedTime}</span>
</div>
</div>
@@ -156,7 +147,6 @@ function createNewsItem(item, rank) {
<span class="score-text">${formattedScore} 分</span>
</div>
<a href="${item.link}" target="_blank" class="news-link">
<span class="link-icon">🚀</span>
<span class="link-text">阅读全文</span>
</a>
</div>

View File

@@ -16,11 +16,11 @@
height: 200%;
background: linear-gradient(
135deg,
rgba(240, 20, 20, 0.3) 0%,
rgba(255, 60, 60, 0.2) 25%,
rgba(255, 100, 100, 0.1) 50%,
rgba(255, 150, 150, 0.2) 75%,
rgba(240, 20, 20, 0.3) 100%
rgba(168, 230, 207, 0.3) 0%,
rgba(220, 237, 193, 0.2) 25%,
rgba(200, 245, 200, 0.1) 50%,
rgba(180, 235, 180, 0.2) 75%,
rgba(168, 230, 207, 0.3) 100%
);
animation: green-flow 20s ease-in-out infinite;
}
@@ -34,11 +34,11 @@
height: 100%;
background: radial-gradient(
circle at 30% 70%,
rgba(255, 45, 45, 0.4) 0%,
rgba(129, 199, 132, 0.3) 0%,
transparent 50%
), radial-gradient(
circle at 70% 30%,
rgba(255, 100, 100, 0.3) 0%,
rgba(165, 214, 167, 0.25) 0%,
transparent 50%
);
animation: green-pulse 15s ease-in-out infinite alternate;

View File

@@ -18,11 +18,11 @@
height: 200%;
background: linear-gradient(
135deg,
rgba(64, 169, 255, 0.4) 0%,
rgba(120, 192, 255, 0.3) 25%,
rgba(255, 175, 64, 0.2) 50%,
rgba(255, 140, 50, 0.3) 75%,
rgba(255, 122, 69, 0.4) 100%
rgba(168, 230, 207, 0.3) 0%,
rgba(220, 237, 193, 0.25) 25%,
rgba(200, 245, 200, 0.15) 50%,
rgba(180, 235, 180, 0.25) 75%,
rgba(168, 230, 207, 0.3) 100%
);
animation: gradient-flow 20s ease-in-out infinite;
border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%;
@@ -37,11 +37,11 @@
height: 100%;
background: radial-gradient(
circle at 30% 70%,
rgba(64, 169, 255, 0.5) 0%,
rgba(129, 199, 132, 0.4) 0%,
transparent 50%
), radial-gradient(
circle at 70% 30%,
rgba(255, 140, 50, 0.4) 0%,
rgba(165, 214, 167, 0.3) 0%,
transparent 50%
);
animation: pulse-effect 15s ease-in-out infinite alternate;
@@ -106,13 +106,13 @@ body {
}
.geometric-decoration {
font-size: 20px;
color: #f04040;
margin: 0 15px;
font-weight: bold;
letter-spacing: 5px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
animation: float-effect 3s ease-in-out infinite alternate;
font-size: 16px;
color: #81c784;
margin: 0 10px;
font-weight: normal;
letter-spacing: 3px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
opacity: 0.6;
}
.geometric-decoration.left {
@@ -140,10 +140,13 @@ body {
}
.time-decoration {
color: #f04040;
font-size: 18px;
margin: 0 10px;
animation: pulse 2s infinite;
font-size: 14px;
color: #a5d6a7;
margin: 0 8px;
font-weight: normal;
letter-spacing: 2px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
opacity: 0.5;
}
@keyframes pulse {
@@ -163,11 +166,11 @@ body {
.geometric-header, .geometric-footer {
text-align: center;
color: #f04040;
margin: 15px 0;
font-size: 16px;
letter-spacing: 3px;
opacity: 0.8;
color: #a5d6a7;
margin: 10px 0;
font-size: 14px;
letter-spacing: 2px;
opacity: 0.5;
}
.geometric-header {
@@ -188,7 +191,7 @@ body {
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
backdrop-filter: blur(10px);
border: 2px solid rgba(240, 64, 64, 0.3);
border: 1px solid rgba(129, 199, 132, 0.2);
position: relative;
}
@@ -198,7 +201,7 @@ body {
position: absolute;
width: 30px;
height: 30px;
border-color: #f04040;
border-color: #a5d6a7;
opacity: 0.7;
}
@@ -226,7 +229,7 @@ header {
}
header h1 {
background: linear-gradient(135deg, #4096ff, #ff7a45);
background: linear-gradient(135deg, #66bb6a, #81c784);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
@@ -245,7 +248,7 @@ header h1 {
display: inline-block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
position: relative;
border: 1px dashed rgba(240, 64, 64, 0.3);
border: 1px dashed rgba(129, 199, 132, 0.3);
}
.update-time::before {
@@ -255,7 +258,7 @@ header h1 {
left: -5px;
right: -5px;
bottom: -5px;
border: 1px solid rgba(240, 64, 64, 0.3);
border: 1px solid rgba(129, 199, 132, 0.3);
border-radius: 28px;
animation: pulse-border 2s infinite;
pointer-events: none;
@@ -299,9 +302,9 @@ header h1 {
position: absolute;
top: 5px;
right: 10px;
color: #f04040;
opacity: 0.2;
font-size: 14px;
color: #a5d6a7;
opacity: 0.15;
font-size: 12px;
}
.hot-item::after {
@@ -309,32 +312,33 @@ header h1 {
position: absolute;
bottom: 5px;
left: 10px;
color: #f04040;
opacity: 0.2;
font-size: 14px;
color: #a5d6a7;
opacity: 0.15;
font-size: 12px;
}
.even-item {
border-left: 3px solid #f04040;
border-left: 2px solid #81c784;
}
.odd-item {
border-right: 3px solid #f04040;
border-right: 2px solid #81c784;
}
.title-decoration {
color: #f04040;
font-weight: bold;
color: #81c784;
font-weight: normal;
margin-right: 5px;
display: inline-block;
transform: translateY(1px);
opacity: 0.7;
}
.source-icon, .time-icon {
color: #f04040;
color: #81c784;
font-size: 14px;
margin-right: 3px;
opacity: 0.8;
opacity: 0.6;
}
.hot-title {
@@ -353,17 +357,17 @@ header h1 {
.hot-item:hover {
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
border-color: rgba(64, 169, 255, 0.3);
border-color: rgba(129, 199, 132, 0.4);
}
.hot-rank {
font-size: 1.2rem;
font-weight: bold;
color: #4096ff;
color: #66bb6a;
margin-right: 18px;
min-width: 38px;
text-align: center;
background-color: rgba(64, 169, 255, 0.1);
background-color: rgba(129, 199, 132, 0.1);
border-radius: 50%;
width: 38px;
height: 38px;
@@ -373,17 +377,17 @@ header h1 {
}
.hot-rank.top-1 {
background: linear-gradient(135deg, #ff4d4f, #ff7a45);
background: linear-gradient(135deg, #4caf50, #66bb6a);
color: white;
}
.hot-rank.top-2 {
background: linear-gradient(135deg, #ff7a45, #ffa940);
background: linear-gradient(135deg, #66bb6a, #81c784);
color: white;
}
.hot-rank.top-3 {
background: linear-gradient(135deg, #ffa940, #ffec3d);
background: linear-gradient(135deg, #81c784, #a5d6a7);
color: white;
}
@@ -403,7 +407,7 @@ header h1 {
}
.hot-title:hover {
color: #4096ff;
color: #66bb6a;
text-decoration: none;
}
@@ -431,11 +435,10 @@ footer {
}
.geo-symbol {
color: #f04040;
font-size: 16px;
opacity: 0.7;
color: #a5d6a7;
font-size: 14px;
opacity: 0.5;
transition: all 0.3s ease;
animation: color-shift 5s infinite alternate;
}
.geo-symbol:hover {
@@ -443,17 +446,7 @@ footer {
transform: scale(1.2) rotate(15deg);
}
@keyframes color-shift {
0% {
color: #f04040;
}
50% {
color: #ff7a45;
}
100% {
color: #ff4d4f;
}
}
/* 响应式设计 */
@media (max-width: 1024px) and (min-width: 768px) {
@@ -570,9 +563,9 @@ footer {
.modern-gradient {
background: linear-gradient(
135deg,
rgba(64, 169, 255, 0.3) 0%,
rgba(255, 175, 64, 0.2) 50%,
rgba(255, 122, 69, 0.25) 100%
rgba(168, 230, 207, 0.25) 0%,
rgba(200, 245, 200, 0.15) 50%,
rgba(180, 235, 180, 0.2) 100%
);
}
}

View File

@@ -1,12 +1,12 @@
/* 背景相关样式 */
body {
background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 25%, #a5d6a7 50%, #81c784 75%, #66bb6a 100%);
background: linear-gradient(135deg, #f1f8e9 0%, #dcedc8 25%, #c8e6c9 50%, #a8e6cf 75%, #81c784 100%);
background-attachment: fixed;
min-height: 100vh;
position: relative;
}
/* 背景装饰元素 */
/* 简化的背景装饰元素 */
body::before {
content: '';
position: fixed;
@@ -15,15 +15,13 @@ body::before {
width: 100%;
height: 100%;
background-image:
radial-gradient(circle at 20% 80%, rgba(120, 200, 120, 0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(100, 180, 100, 0.15) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(140, 220, 140, 0.1) 0%, transparent 50%),
radial-gradient(circle at 60% 70%, rgba(160, 240, 160, 0.08) 0%, transparent 40%);
radial-gradient(circle at 20% 80%, rgba(76, 175, 80, 0.08) 0%, transparent 40%),
radial-gradient(circle at 80% 20%, rgba(129, 199, 132, 0.06) 0%, transparent 40%);
pointer-events: none;
z-index: -1;
}
/* 浮动装饰圆点 */
/* 简化的浮动装饰 */
body::after {
content: '';
position: fixed;
@@ -32,12 +30,10 @@ body::after {
width: 100%;
height: 100%;
background-image:
radial-gradient(circle at 10% 10%, rgba(76, 175, 80, 0.1) 2px, transparent 2px),
radial-gradient(circle at 90% 90%, rgba(76, 175, 80, 0.08) 1px, transparent 1px),
radial-gradient(circle at 30% 80%, rgba(76, 175, 80, 0.06) 1.5px, transparent 1.5px),
radial-gradient(circle at 70% 20%, rgba(76, 175, 80, 0.05) 1px, transparent 1px);
background-size: 100px 100px, 150px 150px, 80px 80px, 120px 120px;
animation: float 20s ease-in-out infinite alternate;
radial-gradient(circle at 30% 70%, rgba(76, 175, 80, 0.04) 1px, transparent 1px),
radial-gradient(circle at 70% 30%, rgba(129, 199, 132, 0.03) 1px, transparent 1px);
background-size: 120px 120px, 180px 180px;
animation: float 25s ease-in-out infinite alternate;
pointer-events: none;
z-index: -1;
}

View File

@@ -18,14 +18,14 @@
height: 200%;
background: linear-gradient(
135deg,
rgba(64, 169, 255, 0.4) 0%,
rgba(120, 192, 255, 0.3) 25%,
rgba(255, 175, 64, 0.2) 50%,
rgba(255, 140, 50, 0.3) 75%,
rgba(255, 122, 69, 0.4) 100%
rgba(76, 175, 80, 0.15) 0%,
rgba(129, 199, 132, 0.1) 25%,
rgba(165, 214, 167, 0.08) 50%,
rgba(200, 230, 201, 0.06) 75%,
rgba(232, 245, 233, 0.05) 100%
);
animation: gradient-flow 20s ease-in-out infinite;
border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%;
animation: gradient-flow 30s ease-in-out infinite;
border-radius: 40% 60% 60% 40% / 40% 40% 60% 60%;
}
.modern-gradient::before {
@@ -37,15 +37,15 @@
height: 100%;
background: radial-gradient(
circle at 30% 70%,
rgba(64, 169, 255, 0.5) 0%,
transparent 50%
rgba(76, 175, 80, 0.1) 0%,
transparent 40%
), radial-gradient(
circle at 70% 30%,
rgba(255, 140, 50, 0.4) 0%,
transparent 50%
rgba(129, 199, 132, 0.08) 0%,
transparent 40%
);
animation: pulse-effect 15s ease-in-out infinite alternate;
border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%;
animation: pulse-effect 25s ease-in-out infinite alternate;
border-radius: 40% 60% 60% 40% / 40% 40% 60% 60%;
}
@keyframes gradient-flow {
@@ -103,21 +103,22 @@ body {
padding: 24px;
position: relative;
z-index: 1;
background-color: rgba(255, 255, 255, 0.85);
background-color: rgba(255, 255, 255, 0.9);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
backdrop-filter: blur(10px);
box-shadow: 0 8px 24px rgba(76, 175, 80, 0.08);
backdrop-filter: blur(12px);
border: 1px solid rgba(76, 175, 80, 0.1);
}
header {
text-align: center;
margin-bottom: 28px;
padding-bottom: 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
border-bottom: 1px solid rgba(76, 175, 80, 0.15);
}
header h1 {
background: linear-gradient(135deg, #4096ff, #ff7a45);
background: linear-gradient(135deg, #2e7d32, #4caf50, #66bb6a);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
@@ -128,13 +129,56 @@ header h1 {
}
.update-time {
color: #666;
color: #4caf50;
font-size: 0.9rem;
background-color: rgba(0, 0, 0, 0.03);
background-color: rgba(76, 175, 80, 0.08);
padding: 8px 16px;
border-radius: 24px;
display: inline-block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.15);
}
.refresh-btn {
background: linear-gradient(135deg, #4caf50, #66bb6a);
color: white;
border: none;
padding: 10px 20px;
border-radius: 24px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 12px;
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.25);
display: inline-flex;
align-items: center;
gap: 6px;
}
.refresh-btn:hover {
background: linear-gradient(135deg, #388e3c, #4caf50);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(76, 175, 80, 0.35);
}
.refresh-btn:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}
.btn-icon {
font-size: 1rem;
animation: rotate 2s linear infinite paused;
}
.refresh-btn:hover .btn-icon {
animation-play-state: running;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 热搜列表 - 移动端优先设计 */
@@ -143,13 +187,13 @@ header h1 {
}
.hot-item {
background: white;
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.08);
transition: all 0.3s ease;
border: 1px solid rgba(0, 0, 0, 0.05);
border: 1px solid rgba(76, 175, 80, 0.1);
display: flex;
align-items: center;
gap: 12px;
@@ -159,8 +203,9 @@ header h1 {
.hot-item:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
border-color: rgba(64, 169, 255, 0.2);
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.15);
border-color: rgba(76, 175, 80, 0.25);
background: rgba(255, 255, 255, 1);
}
/* 排名容器 */
@@ -186,21 +231,21 @@ header h1 {
}
.hot-rank.rank-1 {
background: linear-gradient(135deg, #ff4d4f, #ff7a45);
background: linear-gradient(135deg, #4caf50, #66bb6a);
color: white;
box-shadow: 0 4px 12px rgba(255, 77, 79, 0.3);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
}
.hot-rank.rank-2 {
background: linear-gradient(135deg, #ff7a45, #ffa940);
background: linear-gradient(135deg, #66bb6a, #81c784);
color: white;
box-shadow: 0 4px 12px rgba(255, 122, 69, 0.3);
box-shadow: 0 4px 12px rgba(102, 187, 106, 0.3);
}
.hot-rank.rank-3 {
background: linear-gradient(135deg, #ffa940, #ffec3d);
color: #333;
box-shadow: 0 4px 12px rgba(255, 169, 64, 0.3);
background: linear-gradient(135deg, #81c784, #a5d6a7);
color: #2e7d32;
box-shadow: 0 4px 12px rgba(129, 199, 132, 0.3);
}
.rank-number {
@@ -242,7 +287,7 @@ header h1 {
}
.hot-title:hover {
color: #4096ff;
color: #4caf50;
}
/* 底部行 */
@@ -277,13 +322,13 @@ header h1 {
display: flex;
align-items: center;
gap: 4px;
background: linear-gradient(135deg, #ff6b6b, #4ecdc4);
color: white;
background: linear-gradient(135deg, #81c784, #a5d6a7);
color: #2e7d32;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 6px rgba(76, 175, 80, 0.15);
flex-shrink: 0;
}
@@ -309,7 +354,7 @@ header h1 {
display: flex;
align-items: center;
gap: 4px;
background: linear-gradient(135deg, #4096ff, #40a9ff);
background: linear-gradient(135deg, #4caf50, #66bb6a);
color: white;
text-decoration: none;
padding: 6px 12px;
@@ -317,15 +362,16 @@ header h1 {
font-size: 0.75rem;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(64, 150, 255, 0.3);
box-shadow: 0 2px 6px rgba(76, 175, 80, 0.3);
flex-shrink: 0;
}
.hot-link:hover {
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(64, 150, 255, 0.4);
box-shadow: 0 4px 10px rgba(76, 175, 80, 0.4);
text-decoration: none;
color: white;
background: linear-gradient(135deg, #388e3c, #4caf50);
}
.link-icon {
@@ -339,16 +385,31 @@ header h1 {
.loading {
text-align: center;
padding: 40px;
color: #666;
color: #4caf50;
font-size: 1.1rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(76, 175, 80, 0.2);
border-top: 4px solid #4caf50;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
color: #666;
border-top: 1px solid rgba(76, 175, 80, 0.15);
color: #4caf50;
font-size: 0.9rem;
}

View File

@@ -10,15 +10,12 @@
<body>
<div class="container">
<header class="header">
<div class="header-icon">🔥</div>
<h1 class="title">📱 抖音热搜榜 🎵</h1>
<h1 class="title">抖音热搜榜</h1>
<p class="subtitle">实时热门话题 · 紧跟潮流趋势</p>
<div class="update-time">
<span class="time-icon"></span>
<span id="updateTime">加载中...</span>
</div>
<button id="refreshBtn" class="refresh-btn">
<span class="btn-icon">🔄</span>
刷新数据
</button>
</header>
@@ -48,7 +45,6 @@
<h3>加载失败了</h3>
<p>网络连接出现问题,请稍后重试</p>
<button onclick="loadHotList()" class="retry-btn">
<span>🔄</span>
重新加载
</button>
</div>

View File

@@ -137,8 +137,8 @@ function createHotItem(item, rank) {
// 根据热度值添加火焰等级
let fireLevel = '';
if (item.hot_value >= 10000000) fireLevel = '🔥🔥🔥';
else if (item.hot_value >= 5000000) fireLevel = '🔥🔥';
if (item.hot_value >= 10000000) fireLevel = '🔥';
else if (item.hot_value >= 5000000) fireLevel = '🔥';
else fireLevel = '🔥';
hotItem.innerHTML = `
@@ -153,7 +153,6 @@ function createHotItem(item, rank) {
<div class="hot-title">${escapeHtml(item.title)}</div>
<div class="hot-bottom-row">
<div class="hot-time">
<span class="meta-icon">⏰</span>
<span class="meta-text">${formattedTime}</span>
</div>
<div class="hot-value">
@@ -161,7 +160,6 @@ function createHotItem(item, rank) {
<span class="value-text">${formattedHotValue}</span>
</div>
<a href="${item.link}" target="_blank" class="hot-link">
<span class="link-icon">🎬</span>
<span class="link-text">观看视频</span>
</a>
</div>

View File

@@ -0,0 +1,79 @@
.background-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
overflow: hidden;
background: linear-gradient(180deg, #f0f9f2 0%, #f7fff8 55%, #eef7f1 100%);
}
.floating-blob {
position: absolute;
width: 420px;
height: 420px;
border-radius: 55% 45% 60% 40% / 50% 50% 45% 55%;
filter: blur(0px);
opacity: 0.28;
background: radial-gradient(circle at 30% 30%, rgba(129, 199, 132, 0.6), rgba(129, 199, 132, 0));
animation: drift 36s ease-in-out infinite;
}
.blob-1 {
top: -120px;
left: -160px;
animation-delay: 0s;
}
.blob-2 {
right: -120px;
bottom: -160px;
animation-delay: 8s;
background: radial-gradient(circle at 70% 70%, rgba(76, 175, 80, 0.5), rgba(76, 175, 80, 0));
}
.blob-3 {
top: 45%;
left: 55%;
animation-delay: 16s;
background: radial-gradient(circle at 40% 60%, rgba(165, 214, 167, 0.5), rgba(165, 214, 167, 0));
}
@keyframes drift {
0% {
transform: translate3d(0, 0, 0) scale(1);
}
33% {
transform: translate3d(30px, -40px, 0) scale(1.05);
}
66% {
transform: translate3d(-25px, 30px, 0) scale(0.95);
}
100% {
transform: translate3d(0, 0, 0) scale(1);
}
}
@media (max-width: 768px) {
.floating-blob {
width: 260px;
height: 260px;
opacity: 0.22;
}
.blob-1 {
top: -80px;
left: -120px;
}
.blob-2 {
right: -140px;
bottom: -140px;
}
.blob-3 {
top: 55%;
left: 48%;
}
}

View File

@@ -0,0 +1,432 @@
:root {
--primary-50: #f0f9f2;
--primary-100: #d8f3d8;
--primary-200: #bce5c1;
--primary-300: #a0d8a8;
--primary-400: #7fcf8e;
--primary-500: #66bb6a;
--primary-600: #5aa75f;
--primary-700: #4a8c50;
--primary-text: #103a2b;
--muted-text: #49705d;
--card-bg: rgba(255, 255, 255, 0.85);
--border-soft: rgba(102, 187, 106, 0.18);
--shadow-soft: 0 12px 40px rgba(48, 94, 60, 0.12);
--shadow-hover: 0 18px 50px rgba(48, 94, 60, 0.16);
color-scheme: light;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
background: transparent;
color: var(--primary-text);
line-height: 1.6;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
.page {
position: relative;
margin: 0 auto;
width: min(100%, 960px);
padding: 20px 16px 72px;
}
.page-header {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px 18px;
border-radius: 18px;
background: var(--card-bg);
box-shadow: var(--shadow-soft);
border: 1px solid var(--border-soft);
backdrop-filter: blur(18px);
}
.title-block {
display: flex;
flex-direction: column;
gap: 6px;
}
.label-pill {
align-self: flex-start;
padding: 4px 12px;
font-size: 0.82rem;
font-weight: 600;
color: var(--primary-700);
background: rgba(102, 187, 106, 0.15);
border-radius: 999px;
letter-spacing: 0.06em;
}
.page-header h1 {
font-size: 1.6rem;
font-weight: 700;
line-height: 1.3;
}
.meta-block {
display: flex;
flex-direction: column;
gap: 10px;
}
.update-time {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
font-size: 0.9rem;
color: var(--muted-text);
background: rgba(255, 255, 255, 0.7);
border-radius: 10px;
border: 1px solid rgba(102, 187, 106, 0.22);
}
.refresh-button {
align-self: flex-start;
padding: 10px 18px;
font-size: 0.92rem;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
border: none;
border-radius: 999px;
cursor: pointer;
box-shadow: 0 10px 24px rgba(102, 187, 106, 0.35);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.refresh-button:active {
transform: scale(0.97);
}
.summary-section {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 20px;
}
.summary-card {
padding: 18px 16px;
background: var(--card-bg);
border-radius: 16px;
border: 1px solid var(--border-soft);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(12px);
display: flex;
flex-direction: column;
gap: 8px;
}
.card-label {
font-size: 0.92rem;
color: var(--muted-text);
letter-spacing: 0.04em;
}
.card-value {
font-size: 1.4rem;
font-weight: 700;
display: flex;
align-items: baseline;
gap: 6px;
color: var(--primary-700);
}
.card-value .unit {
font-size: 0.88rem;
font-weight: 500;
color: var(--muted-text);
}
.list-section {
margin-top: 28px;
padding: 22px 18px 26px;
background: var(--card-bg);
border-radius: 20px;
border: 1px solid var(--border-soft);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
.section-header {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 20px;
}
.section-header h2 {
font-size: 1.3rem;
font-weight: 700;
}
.section-subtitle {
font-size: 0.88rem;
color: var(--muted-text);
}
.movie-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.loading,
.error-message {
padding: 18px 16px;
text-align: center;
color: var(--muted-text);
background: rgba(255, 255, 255, 0.7);
border-radius: 14px;
border: 1px dashed rgba(102, 187, 106, 0.35);
}
.movie-item {
display: grid;
grid-template-columns: auto 1fr;
gap: 14px;
padding: 16px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(102, 187, 106, 0.18);
box-shadow: 0 12px 28px rgba(48, 94, 60, 0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.movie-item:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
.movie-rank {
width: 46px;
height: 46px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.1rem;
color: #fff;
background: var(--primary-500);
}
.movie-rank.top-1 {
background: linear-gradient(135deg, #4caf50, #43a047);
}
.movie-rank.top-2 {
background: linear-gradient(135deg, #66bb6a, #5aa75f);
}
.movie-rank.top-3 {
background: linear-gradient(135deg, #81c784, #66bb6a);
}
.movie-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.movie-heading {
display: flex;
flex-direction: column;
gap: 6px;
}
.movie-title {
font-size: 1.1rem;
font-weight: 700;
color: var(--primary-text);
}
.release-info {
font-size: 0.9rem;
color: var(--muted-text);
}
.movie-stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 14px;
}
.stat {
display: flex;
flex-direction: column;
gap: 2px;
}
.stat-label {
font-size: 0.83rem;
color: var(--muted-text);
letter-spacing: 0.02em;
}
.stat-value {
font-size: 1rem;
font-weight: 600;
color: var(--primary-700);
}
.progress-metrics {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 6px;
}
.progress-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.progress-label {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--muted-text);
}
.progress-bar {
width: 100%;
height: 6px;
border-radius: 6px;
background: rgba(102, 187, 106, 0.16);
overflow: hidden;
}
.progress-bar span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(135deg, rgba(102, 187, 106, 0.9), rgba(76, 175, 80, 0.85));
width: 0;
transition: width 0.5s ease;
}
/* Tablet */
@media (min-width: 600px) {
.page {
padding: 28px 20px 84px;
}
.page-header {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.title-block h1 {
font-size: 1.8rem;
}
.meta-block {
align-items: flex-end;
}
.summary-section {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.movie-item {
grid-template-columns: 80px 1fr;
padding: 18px 20px;
}
.movie-rank {
width: 54px;
height: 54px;
font-size: 1.25rem;
}
.movie-heading {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.release-info {
font-size: 0.88rem;
}
.movie-stats {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.progress-metrics {
flex-direction: row;
gap: 16px;
}
.progress-group {
flex: 1;
}
}
/* Desktop */
@media (min-width: 1024px) {
.page {
padding: 36px 24px 96px;
}
.page-header {
padding: 26px 30px;
}
.page-header h1 {
font-size: 2.1rem;
}
.summary-card {
padding: 20px 22px;
}
.card-value {
font-size: 1.6rem;
}
.list-section {
padding: 28px 30px 34px;
}
.movie-item {
grid-template-columns: 96px 1fr;
padding: 22px 26px;
border-radius: 20px;
}
.movie-title {
font-size: 1.25rem;
}
.movie-stats {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.progress-metrics {
gap: 18px;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

View File

@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>猫眼电影实时票房</title>
<link rel="stylesheet" href="./css/background.css">
<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<div class="background-container">
<div class="floating-blob blob-1"></div>
<div class="floating-blob blob-2"></div>
<div class="floating-blob blob-3"></div>
</div>
<div class="page">
<header class="page-header">
<div class="title-block">
<span class="label-pill">实时票房</span>
<h1>猫眼电影实时票房</h1>
</div>
<div class="meta-block">
<span id="updateTime" class="update-time">正在获取最新更新...</span>
<button type="button" id="refreshButton" class="refresh-button">手动刷新</button>
</div>
</header>
<section class="summary-section" id="summarySection">
<div class="summary-card">
<p class="card-label" id="summaryTitle">实时大盘</p>
<p class="card-value">
<span id="totalBoxOffice">--</span>
<span class="unit" id="totalBoxOfficeUnit"></span>
</p>
</div>
<div class="summary-card">
<p class="card-label">综合票房</p>
<p class="card-value">
<span id="combinedBoxOffice">--</span>
<span class="unit" id="combinedBoxOfficeUnit"></span>
</p>
</div>
<div class="summary-card">
<p class="card-label">排片场次</p>
<p class="card-value" id="showCount">--</p>
</div>
<div class="summary-card">
<p class="card-label">观影人次</p>
<p class="card-value" id="viewCount">--</p>
</div>
</section>
<section class="list-section">
<div class="section-header">
<h2>影片实时表现</h2>
<span class="section-subtitle">数据每 5 秒同步一次</span>
</div>
<div id="movieList" class="movie-list">
<div class="loading">正在加载实时票房...</div>
</div>
</section>
</div>
<script src="./js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,297 @@
const API_ENDPOINTS = [
"https://60s.api.shumengya.top/v2/maoyan/realtime/movie"
];
const FALLBACK_ENDPOINT = "./返回接口.json";
const REFRESH_INTERVAL = 5000;
const MAX_MOVIES_TO_RENDER = 40;
const updateTimeEl = document.getElementById("updateTime");
const refreshButton = document.getElementById("refreshButton");
const summaryTitleEl = document.getElementById("summaryTitle");
const totalBoxOfficeEl = document.getElementById("totalBoxOffice");
const totalBoxOfficeUnitEl = document.getElementById("totalBoxOfficeUnit");
const combinedBoxOfficeEl = document.getElementById("combinedBoxOffice");
const combinedBoxOfficeUnitEl = document.getElementById("combinedBoxOfficeUnit");
const showCountEl = document.getElementById("showCount");
const viewCountEl = document.getElementById("viewCount");
const movieListEl = document.getElementById("movieList");
let autoRefreshTimer = null;
let isLoading = false;
function escapeHtml(value) {
if (value === undefined || value === null) {
return "";
}
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function safeText(value) {
if (value === undefined || value === null || value === "") {
return "--";
}
return escapeHtml(value);
}
function parseRate(rateText) {
if (!rateText || typeof rateText !== "string") {
return { text: "--", ratio: 0 };
}
const trimmed = rateText.trim();
const numeric = parseFloat(trimmed.replace(/[^0-9.]/g, ""));
let ratio = Number.isFinite(numeric) ? Math.max(0, Math.min(numeric, 100)) : 0;
if (trimmed.startsWith("<")) {
ratio = Math.max(3, ratio);
}
return { text: escapeHtml(trimmed), ratio };
}
function formatUpdateTime(data) {
if (data && typeof data.updated === "string" && data.updated.trim().length > 0) {
return data.updated.trim();
}
if (data && typeof data.updated_at === "number" && !Number.isNaN(data.updated_at)) {
return new Date(data.updated_at).toLocaleString("zh-CN", {
hour12: false
});
}
return new Date().toLocaleString("zh-CN", { hour12: false });
}
function renderSummary(data) {
summaryTitleEl.textContent = data?.title ? data.title : "实时大盘";
totalBoxOfficeEl.textContent = data?.split_box_office ? data.split_box_office : "--";
totalBoxOfficeUnitEl.textContent = data?.split_box_office_unit ? data.split_box_office_unit : "";
combinedBoxOfficeEl.textContent = data?.box_office ? data.box_office : "--";
combinedBoxOfficeUnitEl.textContent = data?.box_office_unit ? data.box_office_unit : "";
showCountEl.textContent = data?.show_count_desc ? data.show_count_desc : "--";
viewCountEl.textContent = data?.view_count_desc ? data.view_count_desc : "--";
}
function createStat(label, value) {
return `
<div class="stat">
<span class="stat-label">${label}</span>
<span class="stat-value">${safeText(value)}</span>
</div>
`;
}
function createMovieItem(movie, index) {
const item = document.createElement("article");
item.className = "movie-item";
const topClass = index < 3 ? ` top-${index + 1}` : "";
const name = safeText(movie?.movie_name || "未命名影片");
const releaseInfo = movie?.release_info ? `<div class="release-info">${safeText(movie.release_info)}</div>` : "";
const boxOfficeDesc = movie?.box_office_desc || (movie?.box_office ? `${movie.box_office}${movie.box_office_unit || ""}` : "--");
const splitBoxOfficeDesc = movie?.split_box_office_desc || (movie?.split_box_office ? `${movie.split_box_office}${movie.split_box_office_unit || ""}` : "--");
const totalBoxOfficeDesc = movie?.sum_box_desc ?? "--";
const totalSplitBoxOfficeDesc = movie?.sum_split_box_desc ?? "--";
let showCountText = "--";
if (movie?.show_count !== undefined && movie.show_count !== null && movie.show_count !== "") {
const numericShowCount = Number(movie.show_count);
showCountText = Number.isFinite(numericShowCount)
? `${numericShowCount.toLocaleString("zh-CN")}`
: movie.show_count;
}
const avgShowView = movie?.avg_show_view ?? "--";
const avgSeatView = movie?.avg_seat_view ?? "--";
const boxRate = parseRate(movie?.box_office_rate);
const showRate = parseRate(movie?.show_count_rate);
item.innerHTML = `
<div class="movie-rank${topClass}">${index + 1}</div>
<div class="movie-body">
<div class="movie-heading">
<div>
<div class="movie-title">${name}</div>
${releaseInfo}
</div>
</div>
<div class="movie-stats">
${createStat("单日综合票房", boxOfficeDesc)}
${createStat("单日分账票房", splitBoxOfficeDesc)}
${createStat("累计综合票房", totalBoxOfficeDesc)}
${createStat("累计分账票房", totalSplitBoxOfficeDesc)}
${createStat("排片场次", showCountText)}
${createStat("场均人次", avgShowView)}
${createStat("上座率", avgSeatView)}
</div>
<div class="progress-metrics">
<div class="progress-group">
<div class="progress-label">
<span>综合票房占比</span>
<span>${boxRate.text}</span>
</div>
<div class="progress-bar"><span></span></div>
</div>
<div class="progress-group">
<div class="progress-label">
<span>排片占比</span>
<span>${showRate.text}</span>
</div>
<div class="progress-bar"><span></span></div>
</div>
</div>
</div>
`;
const progressBars = item.querySelectorAll(".progress-bar span");
if (progressBars[0]) {
progressBars[0].style.width = `${boxRate.ratio}%`;
}
if (progressBars[1]) {
progressBars[1].style.width = `${showRate.ratio}%`;
}
return item;
}
function renderMovieList(list) {
movieListEl.innerHTML = "";
if (!Array.isArray(list) || list.length === 0) {
const empty = document.createElement("div");
empty.className = "error-message";
empty.textContent = "暂时没有可展示的实时票房数据";
movieListEl.appendChild(empty);
return;
}
const sliced = list.slice(0, MAX_MOVIES_TO_RENDER);
sliced.forEach((movie, index) => {
movieListEl.appendChild(createMovieItem(movie, index));
});
}
async function requestJson(url) {
const response = await fetch(url, {
cache: "no-store"
});
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`);
}
return response.json();
}
async function retrieveData() {
for (const endpoint of API_ENDPOINTS) {
try {
const result = await requestJson(endpoint);
if (result?.code === 200 && result?.data) {
return result.data;
}
} catch (error) {
console.warn("主接口请求失败", error);
}
}
try {
const fallbackResult = await requestJson(FALLBACK_ENDPOINT);
if (fallbackResult?.data) {
return fallbackResult.data;
}
} catch (error) {
console.warn("本地示例数据读取失败", error);
}
return null;
}
async function loadData(isManual = false) {
if (isLoading) {
return;
}
isLoading = true;
if (isManual) {
refreshButton.disabled = true;
refreshButton.textContent = "刷新中...";
}
if (!movieListEl.children.length) {
movieListEl.innerHTML = '<div class="loading">正在加载实时票房...</div>';
}
try {
const data = await retrieveData();
if (!data) {
throw new Error("无法获取数据");
}
renderSummary(data);
renderMovieList(Array.isArray(data.list) ? data.list : []);
updateTimeEl.textContent = `最近更新 ${formatUpdateTime(data)}`;
} catch (error) {
console.error("加载数据失败", error);
movieListEl.innerHTML = "";
const err = document.createElement("div");
err.className = "error-message";
err.textContent = "数据获取暂时遇到问题,系统会稍后自动重试";
movieListEl.appendChild(err);
updateTimeEl.textContent = "最近更新 --";
renderSummary(null);
} finally {
if (isManual) {
refreshButton.disabled = false;
refreshButton.textContent = "手动刷新";
}
isLoading = false;
}
}
function startAutoRefresh() {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
}
autoRefreshTimer = setInterval(() => {
loadData(false);
}, REFRESH_INTERVAL);
}
refreshButton.addEventListener("click", () => {
loadData(true);
});
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
}
} else {
startAutoRefresh();
loadData(false);
}
});
function init() {
loadData(false);
startAutoRefresh();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}

View File

@@ -0,0 +1,79 @@
.background-layer {
position: fixed;
inset: 0;
overflow: hidden;
z-index: -1;
background: linear-gradient(180deg, #e8f5e8 0%, #f0f8e8 55%, #e8f5e8 100%);
}
.aurora {
position: absolute;
width: 480px;
height: 480px;
border-radius: 58% 42% 53% 47% / 52% 46% 54% 48%;
filter: blur(0px);
opacity: 0.28;
background: radial-gradient(circle at 40% 40%, rgba(168, 230, 207, 0.4), rgba(168, 230, 207, 0));
animation: float 32s ease-in-out infinite;
}
.aurora-1 {
top: -160px;
left: -140px;
animation-delay: 0s;
}
.aurora-2 {
top: 50%;
left: 60%;
animation-delay: 6s;
background: radial-gradient(circle at 60% 60%, rgba(220, 237, 193, 0.35), rgba(220, 237, 193, 0));
}
.aurora-3 {
bottom: -180px;
right: -160px;
animation-delay: 12s;
background: radial-gradient(circle at 50% 50%, rgba(129, 199, 132, 0.3), rgba(129, 199, 132, 0));
}
@keyframes float {
0% {
transform: translate3d(0, 0, 0) scale(1);
}
25% {
transform: translate3d(40px, -30px, 0) scale(1.05);
}
50% {
transform: translate3d(-35px, 25px, 0) scale(0.95);
}
75% {
transform: translate3d(20px, 35px, 0) scale(1.08);
}
100% {
transform: translate3d(0, 0, 0) scale(1);
}
}
@media (max-width: 768px) {
.aurora {
width: 280px;
height: 280px;
opacity: 0.24;
}
.aurora-1 {
top: -110px;
left: -130px;
}
.aurora-2 {
top: 45%;
left: 35%;
}
.aurora-3 {
bottom: -140px;
right: -120px;
}
}

View File

@@ -0,0 +1,414 @@
:root {
--bg-base: rgba(255, 255, 255, 0.85);
--panel-bg: rgba(248, 252, 248, 0.9);
--panel-border: rgba(129, 199, 132, 0.25);
--accent-1: #4caf50;
--accent-2: #81c784;
--accent-3: #a5d6a7;
--text-primary: #2e7d32;
--text-secondary: #558b2f;
--chip-bg: rgba(76, 175, 80, 0.15);
--shadow-soft: 0 16px 40px rgba(46, 125, 50, 0.15);
color-scheme: light;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: transparent;
color: var(--text-primary);
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
.screen {
width: min(100%, 840px);
margin: 0 auto;
padding: 18px 16px 72px;
display: flex;
flex-direction: column;
gap: 18px;
}
.screen-header {
display: flex;
flex-direction: column;
gap: 18px;
padding: 20px 18px;
border-radius: 20px;
background: var(--bg-base);
border: 1px solid var(--panel-border);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(18px);
}
.title-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
font-size: 0.8rem;
letter-spacing: 0.08em;
text-transform: uppercase;
border-radius: 999px;
color: var(--accent-1);
background: var(--chip-bg);
}
.screen-header h1 {
font-size: 1.6rem;
font-weight: 700;
}
.tagline {
font-size: 0.95rem;
color: var(--text-secondary);
}
.actions {
display: flex;
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
.refresh {
padding: 10px 20px;
border-radius: 999px;
border: none;
background: linear-gradient(135deg, rgba(76, 175, 80, 0.9), rgba(129, 199, 132, 0.9));
color: var(--text-primary);
font-size: 0.92rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0 12px 30px rgba(76, 175, 80, 0.25);
}
.refresh:active {
transform: scale(0.97);
}
.refresh:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.timestamp {
font-size: 0.85rem;
color: var(--text-secondary);
}
.insights {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.insight-card {
padding: 16px 18px;
background: var(--panel-bg);
border-radius: 16px;
border: 1px solid rgba(129, 199, 132, 0.3);
box-shadow: var(--shadow-soft);
display: flex;
flex-direction: column;
gap: 6px;
}
.insight-label {
font-size: 0.85rem;
color: var(--text-secondary);
letter-spacing: 0.02em;
}
.insight-value {
font-size: 1.3rem;
font-weight: 700;
display: flex;
align-items: baseline;
gap: 4px;
color: var(--text-primary);
}
.insight-value .unit {
font-size: 0.9rem;
font-weight: 500;
color: var(--text-secondary);
}
.ranking {
padding: 22px 18px 28px;
background: var(--bg-base);
border-radius: 24px;
border: 1px solid var(--panel-border);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(22px);
display: flex;
flex-direction: column;
gap: 18px;
}
.ranking-header {
display: flex;
flex-direction: column;
gap: 6px;
}
.ranking-header h2 {
font-size: 1.25rem;
font-weight: 700;
}
.subtitle {
font-size: 0.88rem;
color: var(--text-secondary);
}
.programme-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.loading,
.error-message,
.empty-message {
padding: 18px 16px;
text-align: center;
color: var(--text-secondary);
border-radius: 14px;
border: 1px dashed rgba(129, 199, 132, 0.4);
background: rgba(248, 252, 248, 0.6);
}
.programme-item {
display: grid;
grid-template-columns: auto 1fr;
gap: 14px;
padding: 16px;
border-radius: 18px;
background: rgba(248, 252, 248, 0.95);
border: 1px solid rgba(129, 199, 132, 0.3);
box-shadow: 0 14px 30px rgba(46, 125, 50, 0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.programme-item:hover {
transform: translateY(-2px);
box-shadow: 0 18px 36px rgba(46, 125, 50, 0.15);
}
.rank-badge {
width: 44px;
height: 44px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.05rem;
color: #ffffff;
background: linear-gradient(135deg, rgba(76, 175, 80, 0.9), rgba(129, 199, 132, 0.9));
}
.rank-badge.top-1 {
background: linear-gradient(135deg, #2e7d32, #4caf50);
}
.rank-badge.top-2 {
background: linear-gradient(135deg, #388e3c, #66bb6a);
}
.rank-badge.top-3 {
background: linear-gradient(135deg, #4caf50, #81c784);
}
.programme-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.programme-head {
display: flex;
flex-direction: column;
gap: 6px;
}
.programme-name {
font-size: 1.05rem;
font-weight: 700;
}
.channel-name {
font-size: 0.9rem;
color: var(--text-secondary);
}
.metric-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.metric {
display: flex;
flex-direction: column;
gap: 2px;
}
.metric-label {
font-size: 0.78rem;
color: var(--text-secondary);
letter-spacing: 0.04em;
text-transform: uppercase;
}
.metric-value {
font-size: 0.98rem;
font-weight: 600;
}
.progress-trend {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 4px;
}
.progress-row {
display: flex;
flex-direction: column;
gap: 4px;
}
.progress-label {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--text-secondary);
}
.progress-bar {
width: 100%;
height: 6px;
border-radius: 6px;
background: rgba(116, 210, 255, 0.16);
overflow: hidden;
}
.progress-bar span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(135deg, rgba(116, 210, 255, 0.95), rgba(122, 185, 255, 0.95));
width: 0;
transition: width 0.5s ease;
}
.progress-row.attention .progress-bar span {
background: linear-gradient(135deg, rgba(244, 156, 224, 0.95), rgba(116, 210, 255, 0.9));
}
/* Tablet layout */
@media (min-width: 600px) {
.screen {
padding: 24px 20px 88px;
gap: 20px;
}
.screen-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 26px 28px;
}
.screen-header h1 {
font-size: 1.9rem;
}
.tagline {
font-size: 1rem;
}
.actions {
align-items: flex-end;
}
.insights {
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
}
.programme-item {
grid-template-columns: 72px 1fr;
padding: 18px 20px;
}
.rank-badge {
width: 54px;
height: 54px;
font-size: 1.25rem;
}
.metric-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
/* Desktop layout */
@media (min-width: 1024px) {
.screen {
width: min(100%, 960px);
padding: 32px 28px 104px;
}
.insight-card {
padding: 20px 22px;
}
.insight-value {
font-size: 1.6rem;
}
.ranking {
padding: 30px 32px 36px;
}
.programme-item {
grid-template-columns: 96px 1fr;
padding: 22px 26px;
border-radius: 22px;
}
.programme-name {
font-size: 1.25rem;
}
.metric-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>猫眼电视收视排行</title>
<link rel="stylesheet" href="./css/background.css">
<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<div class="background-layer">
<div class="aurora aurora-1"></div>
<div class="aurora aurora-2"></div>
<div class="aurora aurora-3"></div>
</div>
<div class="screen">
<header class="screen-header">
<div class="title-group">
<span class="eyebrow">实时收视</span>
<h1>猫眼电视收视排行</h1>
<p class="tagline">聚焦全国频道实时关注度,让你不错过热门节目</p>
</div>
<div class="actions">
<button id="refreshButton" class="refresh">手动刷新</button>
<span id="updateTime" class="timestamp">正在同步最新数据...</span>
</div>
</header>
<section class="insights" id="insightPanel">
<div class="insight-card">
<p class="insight-label">节目数量</p>
<p class="insight-value" id="programmeCount">--</p>
</div>
<div class="insight-card">
<p class="insight-label">最高市场份额</p>
<p class="insight-value"><span id="topMarketRate">--</span><span class="unit">%</span></p>
</div>
<div class="insight-card">
<p class="insight-label">最高关注指数</p>
<p class="insight-value"><span id="topAttentionRate">--</span><span class="unit">%</span></p>
</div>
<div class="insight-card">
<p class="insight-label">官方刷新频率</p>
<p class="insight-value" id="refreshGap">--</p>
</div>
</section>
<section class="ranking">
<div class="ranking-header">
<h2>频道节目排行榜</h2>
<span class="subtitle">实时榜单,数据持续刷新</span>
</div>
<div id="programmeList" class="programme-list">
<div class="loading">正在载入电视收视排行...</div>
</div>
</section>
</div>
<script src="./js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,290 @@
const API_ENDPOINTS = [
"https://60s.api.shumengya.top/v2/maoyan/realtime/tv"
];
const FALLBACK_ENDPOINT = "./返回接口.json";
const REFRESH_INTERVAL = 4000;
const MAX_ITEMS = 40;
const refreshButton = document.getElementById("refreshButton");
const updateTimeEl = document.getElementById("updateTime");
const programmeListEl = document.getElementById("programmeList");
const programmeCountEl = document.getElementById("programmeCount");
const topMarketRateEl = document.getElementById("topMarketRate");
const topAttentionRateEl = document.getElementById("topAttentionRate");
const refreshGapEl = document.getElementById("refreshGap");
let isLoading = false;
let autoTimer = null;
function escapeHtml(value) {
if (value === undefined || value === null) {
return "";
}
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function safeText(value, fallback = "--") {
if (value === undefined || value === null || value === "") {
return fallback;
}
return escapeHtml(value);
}
function formatNumber(value, fractionDigits = 2) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return "--";
}
return numeric.toFixed(fractionDigits);
}
function formatGapText(seconds) {
const numeric = Number(seconds);
if (!Number.isFinite(numeric) || numeric <= 0) {
return "--";
}
if (numeric < 60) {
return `约每 ${Math.round(numeric)}`;
}
const minutes = Math.floor(numeric / 60);
const remaining = Math.round(numeric % 60);
if (remaining === 0) {
return `约每 ${minutes} 分钟`;
}
return `约每 ${minutes}${remaining}`;
}
function parseRate(value) {
const numeric = Number(value);
if (Number.isFinite(numeric) && numeric >= 0) {
return {
text: numeric.toFixed(4).replace(/0+$/, "").replace(/\.$/, ""),
ratio: Math.max(0, Math.min(numeric, 100))
};
}
return { text: "--", ratio: 0 };
}
function formatUpdateTime(data) {
if (data && typeof data.updated === "string" && data.updated.trim().length > 0) {
return data.updated.trim();
}
if (data && typeof data.updated_at === "number" && Number.isFinite(data.updated_at)) {
return new Date(data.updated_at).toLocaleString("zh-CN", { hour12: false });
}
return new Date().toLocaleString("zh-CN", { hour12: false });
}
function renderInsights(list, gapSecond) {
const total = Array.isArray(list) ? list.length : 0;
programmeCountEl.textContent = total ? total.toString() : "--";
if (total) {
const topMarket = list.reduce((max, item) => {
const value = Number(item?.market_rate);
return value > max ? value : max;
}, 0);
const topAttention = list.reduce((max, item) => {
const value = Number(item?.attention_rate);
return value > max ? value : max;
}, 0);
topMarketRateEl.textContent = topMarket ? topMarket.toFixed(2) : "--";
topAttentionRateEl.textContent = topAttention ? topAttention.toFixed(2) : "--";
} else {
topMarketRateEl.textContent = "--";
topAttentionRateEl.textContent = "--";
}
refreshGapEl.textContent = formatGapText(gapSecond);
}
function createMetric(label, value) {
return `
<div class="metric">
<span class="metric-label">${label}</span>
<span class="metric-value">${safeText(value)}</span>
</div>
`;
}
function createProgrammeItem(programme, index) {
const article = document.createElement("article");
article.className = "programme-item";
const topClass = index < 3 ? ` top-${index + 1}` : "";
const name = safeText(programme?.programme_name || "未命名节目");
const channel = safeText(programme?.channel_name || "--");
const market = parseRate(programme?.market_rate);
const attention = parseRate(programme?.attention_rate);
const marketDesc = safeText(programme?.market_rate_desc || formatNumber(programme?.market_rate));
const attentionDesc = safeText(programme?.attention_rate_desc || formatNumber(programme?.attention_rate));
article.innerHTML = `
<div class="rank-badge${topClass}">${index + 1}</div>
<div class="programme-body">
<div class="programme-head">
<div class="programme-name">${name}</div>
<div class="channel-name">${channel}</div>
</div>
<div class="metric-grid">
${createMetric("市场占有率", marketDesc)}
${createMetric("关注指数", attentionDesc)}
${createMetric("排序位置", `${index + 1}`)}
${createMetric("排名趋势", programme?.rank_trend ? safeText(programme.rank_trend) : "--")}
</div>
<div class="progress-trend">
<div class="progress-row market">
<div class="progress-label">
<span>市场份额</span>
<span>${market.text === "--" ? "--" : `${market.text}%`}</span>
</div>
<div class="progress-bar"><span style="width: ${market.ratio}%"></span></div>
</div>
<div class="progress-row attention">
<div class="progress-label">
<span>关注份额</span>
<span>${attention.text === "--" ? "--" : `${attention.text}%`}</span>
</div>
<div class="progress-bar"><span style="width: ${attention.ratio}%"></span></div>
</div>
</div>
</div>
`;
return article;
}
function renderProgrammeList(list) {
programmeListEl.innerHTML = "";
if (!Array.isArray(list) || list.length === 0) {
const empty = document.createElement("div");
empty.className = "empty-message";
empty.textContent = "暂时没有可展示的节目数据";
programmeListEl.appendChild(empty);
return;
}
list.slice(0, MAX_ITEMS).forEach((item, index) => {
programmeListEl.appendChild(createProgrammeItem(item, index));
});
}
async function requestJson(url) {
const response = await fetch(url, { cache: "no-store" });
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`);
}
return response.json();
}
async function retrieveData() {
for (const endpoint of API_ENDPOINTS) {
try {
const result = await requestJson(endpoint);
if (result?.code === 200 && result?.data) {
return result.data;
}
} catch (error) {
console.warn("主接口请求失败", error);
}
}
try {
const fallbackResult = await requestJson(FALLBACK_ENDPOINT);
if (fallbackResult?.data) {
return fallbackResult.data;
}
} catch (fallbackError) {
console.warn("本地示例数据读取失败", fallbackError);
}
return null;
}
async function loadData(isManual = false) {
if (isLoading) {
return;
}
isLoading = true;
if (isManual) {
refreshButton.disabled = true;
refreshButton.textContent = "刷新中...";
}
if (!programmeListEl.children.length) {
programmeListEl.innerHTML = '<div class="loading">正在载入电视收视排行...</div>';
}
try {
const data = await retrieveData();
if (!data) {
throw new Error("无法获取数据");
}
renderProgrammeList(Array.isArray(data.list) ? data.list : []);
renderInsights(data.list, data.update_gap_second);
updateTimeEl.textContent = `最近更新 ${formatUpdateTime(data)}`;
} catch (error) {
console.error("加载数据失败", error);
programmeListEl.innerHTML = '';
const errorBox = document.createElement("div");
errorBox.className = "error-message";
errorBox.textContent = "数据获取暂时不可用,系统稍后会自动重试";
programmeListEl.appendChild(errorBox);
updateTimeEl.textContent = "最近更新 --";
renderInsights([], 0);
} finally {
if (isManual) {
refreshButton.disabled = false;
refreshButton.textContent = "手动刷新";
}
isLoading = false;
}
}
function startAutoRefresh() {
if (autoTimer) {
clearInterval(autoTimer);
}
autoTimer = setInterval(() => {
loadData(false);
}, REFRESH_INTERVAL);
}
refreshButton.addEventListener("click", () => {
loadData(true);
});
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
if (autoTimer) {
clearInterval(autoTimer);
autoTimer = null;
}
} else {
startAutoRefresh();
loadData(false);
}
});
function init() {
loadData(false);
startAutoRefresh();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}

View File

@@ -0,0 +1,435 @@
{
"code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": {
"update_gap_second": 3,
"updated": "2025-09-26 16:22:45",
"updated_at": 1758874965018,
"list": [
{
"programme_name": "六姊妹37",
"channel_name": "CCTV-1",
"market_rate": 16.1709,
"market_rate_desc": "16.1709%",
"attention_rate": 1.2816,
"attention_rate_desc": "1.2816%"
},
{
"programme_name": "太行山上6",
"channel_name": "CCTV-4",
"market_rate": 8.2684,
"market_rate_desc": "8.2684%",
"attention_rate": 0.6553,
"attention_rate_desc": "0.6553%"
},
{
"programme_name": "星推荐",
"channel_name": "CCTV-8",
"market_rate": 7.3725,
"market_rate_desc": "7.3725%",
"attention_rate": 0.5843,
"attention_rate_desc": "0.5843%"
},
{
"programme_name": "炮兵司令朱瑞",
"channel_name": "CCTV-6",
"market_rate": 7.3315,
"market_rate_desc": "7.3315%",
"attention_rate": 0.5811,
"attention_rate_desc": "0.5811%"
},
{
"programme_name": "新闻直播间",
"channel_name": "CCTV-13",
"market_rate": 4.4396,
"market_rate_desc": "4.4396%",
"attention_rate": 0.3519,
"attention_rate_desc": "0.3519%"
},
{
"programme_name": "百姓剧场二:征服15",
"channel_name": "浙江卫视",
"market_rate": 4.3651,
"market_rate_desc": "4.3651%",
"attention_rate": 0.346,
"attention_rate_desc": "0.346%"
},
{
"programme_name": "新相亲大会精华版",
"channel_name": "江苏卫视",
"market_rate": 4.0919,
"market_rate_desc": "4.0919%",
"attention_rate": 0.3243,
"attention_rate_desc": "0.3243%"
},
{
"programme_name": "青春独播剧场:底线11",
"channel_name": "湖南卫视",
"market_rate": 3.9014,
"market_rate_desc": "3.9014%",
"attention_rate": 0.3092,
"attention_rate_desc": "0.3092%"
},
{
"programme_name": "全景自然:黄石公园-飞翔的生命",
"channel_name": "CCTV-9",
"market_rate": 2.504,
"market_rate_desc": "2.504%",
"attention_rate": 0.1985,
"attention_rate_desc": "0.1985%"
},
{
"programme_name": "运动一起赢",
"channel_name": "CCTV-5",
"market_rate": 2.0781,
"market_rate_desc": "2.0781%",
"attention_rate": 0.1647,
"attention_rate_desc": "0.1647%"
},
{
"programme_name": "小日子19",
"channel_name": "东方卫视",
"market_rate": 1.9804,
"market_rate_desc": "1.9804%",
"attention_rate": 0.157,
"attention_rate_desc": "0.157%"
},
{
"programme_name": "向幸福出发",
"channel_name": "CCTV-3",
"market_rate": 1.8832,
"market_rate_desc": "1.8832%",
"attention_rate": 0.1493,
"attention_rate_desc": "0.1493%"
},
{
"programme_name": "浴血十四年16",
"channel_name": "CCTV-7",
"market_rate": 1.8485,
"market_rate_desc": "1.8485%",
"attention_rate": 0.1465,
"attention_rate_desc": "0.1465%"
},
{
"programme_name": "正点财经",
"channel_name": "CCTV-2",
"market_rate": 1.7255,
"market_rate_desc": "1.7255%",
"attention_rate": 0.1368,
"attention_rate_desc": "0.1368%"
},
{
"programme_name": "热播剧场:太行山上6",
"channel_name": "深圳卫视",
"market_rate": 1.6132,
"market_rate_desc": "1.6132%",
"attention_rate": 0.1279,
"attention_rate_desc": "0.1279%"
},
{
"programme_name": "爱家剧场:父母爱情37",
"channel_name": "山东卫视",
"market_rate": 1.5558,
"market_rate_desc": "1.5558%",
"attention_rate": 0.1233,
"attention_rate_desc": "0.1233%"
},
{
"programme_name": "刘家媳妇4",
"channel_name": "CCTV-17",
"market_rate": 1.3905,
"market_rate_desc": "1.3905%",
"attention_rate": 0.1102,
"attention_rate_desc": "0.1102%"
},
{
"programme_name": "午茶剧场:归队8",
"channel_name": "北京卫视",
"market_rate": 1.1078,
"market_rate_desc": "1.1078%",
"attention_rate": 0.0878,
"attention_rate_desc": "0.0878%"
},
{
"programme_name": "吉视剧场:武工队传奇46",
"channel_name": "吉林卫视",
"market_rate": 0.9407,
"market_rate_desc": "0.9407%",
"attention_rate": 0.0745,
"attention_rate_desc": "0.0745%"
},
{
"programme_name": "情感剧场:朱元璋64",
"channel_name": "河北卫视",
"market_rate": 0.9211,
"market_rate_desc": "0.9211%",
"attention_rate": 0.073,
"attention_rate_desc": "0.073%"
},
{
"programme_name": "下午剧场:战火青春3",
"channel_name": "广东卫视",
"market_rate": 0.9211,
"market_rate_desc": "0.9211%",
"attention_rate": 0.073,
"attention_rate_desc": "0.073%"
},
{
"programme_name": "探索.发现-奥秘54",
"channel_name": "CCTV-10",
"market_rate": 0.822,
"market_rate_desc": "0.822%",
"attention_rate": 0.0652,
"attention_rate_desc": "0.0652%"
},
{
"programme_name": "一线",
"channel_name": "CCTV-12",
"market_rate": 0.7949,
"market_rate_desc": "0.7949%",
"attention_rate": 0.063,
"attention_rate_desc": "0.063%"
},
{
"programme_name": "海豚真情剧场:重案六组Ⅱ-45",
"channel_name": "安徽卫视",
"market_rate": 0.7684,
"market_rate_desc": "0.7684%",
"attention_rate": 0.0609,
"attention_rate_desc": "0.0609%"
},
{
"programme_name": "中国女子围棋甲级联赛-第10轮",
"channel_name": "CCTV-5+",
"market_rate": 0.7621,
"market_rate_desc": "0.7621%",
"attention_rate": 0.0604,
"attention_rate_desc": "0.0604%"
},
{
"programme_name": "休闲剧场:女子特战队2",
"channel_name": "天津卫视",
"market_rate": 0.6694,
"market_rate_desc": "0.6694%",
"attention_rate": 0.0531,
"attention_rate_desc": "0.0531%"
},
{
"programme_name": "动画大放映:猪猪侠之超星五灵侠第八季",
"channel_name": "CCTV-14",
"market_rate": 0.6637,
"market_rate_desc": "0.6637%",
"attention_rate": 0.0526,
"attention_rate_desc": "0.0526%"
},
{
"programme_name": "昆仑剧场:康熙微服私访记第三部30",
"channel_name": "青海卫视",
"market_rate": 0.6511,
"market_rate_desc": "0.6511%",
"attention_rate": 0.0516,
"attention_rate_desc": "0.0516%"
},
{
"programme_name": "京剧电影工程-大闹天宫",
"channel_name": "CCTV-11",
"market_rate": 0.6069,
"market_rate_desc": "0.6069%",
"attention_rate": 0.0481,
"attention_rate_desc": "0.0481%"
},
{
"programme_name": "温情剧场:神探狄仁杰Ⅱ-14",
"channel_name": "陕西卫视",
"market_rate": 0.571,
"market_rate_desc": "0.571%",
"attention_rate": 0.0453,
"attention_rate_desc": "0.0453%"
},
{
"programme_name": "下午剧场:狙击部队30",
"channel_name": "贵州卫视",
"market_rate": 0.5558,
"market_rate_desc": "0.5558%",
"attention_rate": 0.0441,
"attention_rate_desc": "0.0441%"
},
{
"programme_name": "白天剧场:西游记26",
"channel_name": "湖北卫视",
"market_rate": 0.5463,
"market_rate_desc": "0.5463%",
"attention_rate": 0.0433,
"attention_rate_desc": "0.0433%"
},
{
"programme_name": "中国爱大剧场:神枪21",
"channel_name": "四川卫视",
"market_rate": 0.5388,
"market_rate_desc": "0.5388%",
"attention_rate": 0.0427,
"attention_rate_desc": "0.0427%"
},
{
"programme_name": "全民开麦",
"channel_name": "CCTV-15",
"market_rate": 0.4915,
"market_rate_desc": "0.4915%",
"attention_rate": 0.039,
"attention_rate_desc": "0.039%"
},
{
"programme_name": "下午剧场:仁心俱乐部10",
"channel_name": "东南卫视",
"market_rate": 0.4858,
"market_rate_desc": "0.4858%",
"attention_rate": 0.0385,
"attention_rate_desc": "0.0385%"
},
{
"programme_name": "生活服务",
"channel_name": "江西卫视",
"market_rate": 0.4113,
"market_rate_desc": "0.4113%",
"attention_rate": 0.0326,
"attention_rate_desc": "0.0326%"
},
{
"programme_name": "白天剧场:神医喜来乐25",
"channel_name": "宁夏卫视",
"market_rate": 0.4044,
"market_rate_desc": "0.4044%",
"attention_rate": 0.032,
"attention_rate_desc": "0.032%"
},
{
"programme_name": "传奇剧场:铁血玫瑰15",
"channel_name": "黑龙江卫视",
"market_rate": 0.4031,
"market_rate_desc": "0.4031%",
"attention_rate": 0.032,
"attention_rate_desc": "0.032%"
},
{
"programme_name": "经典剧场:亮剑30",
"channel_name": "广西卫视",
"market_rate": 0.3836,
"market_rate_desc": "0.3836%",
"attention_rate": 0.0304,
"attention_rate_desc": "0.0304%"
},
{
"programme_name": "中国网球公开赛-女单-第二轮",
"channel_name": "CCTV-16",
"market_rate": 0.3539,
"market_rate_desc": "0.3539%",
"attention_rate": 0.0281,
"attention_rate_desc": "0.0281%"
},
{
"programme_name": "生活服务",
"channel_name": "河南卫视",
"market_rate": 0.3413,
"market_rate_desc": "0.3413%",
"attention_rate": 0.0271,
"attention_rate_desc": "0.0271%"
},
{
"programme_name": "生活服务",
"channel_name": "辽宁卫视",
"market_rate": 0.3318,
"market_rate_desc": "0.3318%",
"attention_rate": 0.0263,
"attention_rate_desc": "0.0263%"
},
{
"programme_name": "传奇剧场:狼烟21",
"channel_name": "内蒙古卫视",
"market_rate": 0.3262,
"market_rate_desc": "0.3262%",
"attention_rate": 0.0258,
"attention_rate_desc": "0.0258%"
},
{
"programme_name": "炫酷剧场:薛平贵与王宝钏19",
"channel_name": "云南卫视",
"market_rate": 0.3079,
"market_rate_desc": "0.3079%",
"attention_rate": 0.0244,
"attention_rate_desc": "0.0244%"
},
{
"programme_name": "休闲剧场:历史转折中的邓小平17",
"channel_name": "兵团卫视",
"market_rate": 0.3035,
"market_rate_desc": "0.3035%",
"attention_rate": 0.0241,
"attention_rate_desc": "0.0241%"
},
{
"programme_name": "亮剑12",
"channel_name": "重庆卫视",
"market_rate": 0.2656,
"market_rate_desc": "0.2656%",
"attention_rate": 0.0211,
"attention_rate_desc": "0.0211%"
},
{
"programme_name": "阳光剧场:飞哥大英雄39",
"channel_name": "海南卫视",
"market_rate": 0.2643,
"market_rate_desc": "0.2643%",
"attention_rate": 0.021,
"attention_rate_desc": "0.021%"
},
{
"programme_name": "劫中劫15-17",
"channel_name": "厦门卫视",
"market_rate": 0.246,
"market_rate_desc": "0.246%",
"attention_rate": 0.0195,
"attention_rate_desc": "0.0195%"
},
{
"programme_name": "生活服务",
"channel_name": "甘肃卫视",
"market_rate": 0.2448,
"market_rate_desc": "0.2448%",
"attention_rate": 0.0194,
"attention_rate_desc": "0.0194%"
},
{
"programme_name": "花季剧场:神枪7",
"channel_name": "中国教育台-1",
"market_rate": 0.1703,
"market_rate_desc": "0.1703%",
"attention_rate": 0.0135,
"attention_rate_desc": "0.0135%"
},
{
"programme_name": "雪莲剧场:南来北往21",
"channel_name": "西藏卫视",
"market_rate": 0.1634,
"market_rate_desc": "0.1634%",
"attention_rate": 0.013,
"attention_rate_desc": "0.013%"
},
{
"programme_name": "生活服务",
"channel_name": "山西卫视",
"market_rate": 0.1438,
"market_rate_desc": "0.1438%",
"attention_rate": 0.0114,
"attention_rate_desc": "0.0114%"
},
{
"programme_name": "生活服务",
"channel_name": "新疆卫视",
"market_rate": 0.0656,
"market_rate_desc": "0.0656%",
"attention_rate": 0.0052,
"attention_rate_desc": "0.0052%"
}
]
}
}

View File

@@ -52,7 +52,7 @@ body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
line-height: 1.6;
color: var(--text-primary);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 50%, #ffd3a5 100%);
min-height: 100vh;
position: relative;
overflow-x: hidden;
@@ -67,11 +67,11 @@ body::before {
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.2) 0%, transparent 50%);
radial-gradient(circle at 20% 80%, rgba(168, 230, 207, 0.15) 0%, transparent 40%),
radial-gradient(circle at 80% 20%, rgba(220, 237, 193, 0.12) 0%, transparent 40%),
radial-gradient(circle at 40% 40%, rgba(255, 211, 165, 0.1) 0%, transparent 40%);
z-index: -1;
animation: backgroundShift 20s ease-in-out infinite;
animation: backgroundShift 30s ease-in-out infinite;
}
@keyframes backgroundShift {
@@ -90,11 +90,11 @@ body::before {
max-width: 900px;
margin: var(--space-lg) auto;
padding: var(--space-xl);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(15px);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(76, 175, 80, 0.15);
border: 1px solid rgba(168, 230, 207, 0.3);
position: relative;
animation: slideUp 0.8s ease-out;
}
@@ -127,7 +127,7 @@ body::before {
transform: translateX(-50%);
width: 60px;
height: 4px;
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
background: linear-gradient(90deg, #4caf50, #81c784);
border-radius: 2px;
}
@@ -162,7 +162,7 @@ body::before {
}
.header h1 .title-text {
background: linear-gradient(135deg, var(--primary-color), var(--primary-light));
background: linear-gradient(135deg, #2e7d32, #4caf50);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
@@ -171,12 +171,12 @@ body::before {
.header h1 .update-badge {
font-size: 0.4em;
background: linear-gradient(135deg, var(--accent-color), var(--secondary-color));
background: linear-gradient(135deg, #66bb6a, #a5d6a7);
color: white;
padding: var(--space-xs) var(--space-md);
border-radius: var(--radius-xl);
font-weight: 600;
box-shadow: var(--shadow-md);
box-shadow: 0 4px 6px rgba(76, 175, 80, 0.3);
animation: pulse 3s infinite;
white-space: nowrap;
}
@@ -210,8 +210,8 @@ body::before {
width: 40px;
height: 40px;
margin: 0 auto var(--space-md);
border: 3px solid var(--bg-tertiary);
border-top: 3px solid var(--primary-color);
border: 3px solid rgba(76, 175, 80, 0.2);
border-top: 3px solid #4caf50;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@@ -274,15 +274,15 @@ body::before {
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, transparent 0%, rgba(102, 126, 234, 0.02) 100%);
background: linear-gradient(135deg, transparent 0%, rgba(76, 175, 80, 0.03) 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.movie-item:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: rgba(102, 126, 234, 0.2);
box-shadow: 0 10px 25px rgba(76, 175, 80, 0.15);
border-color: rgba(76, 175, 80, 0.3);
}
.movie-item:hover::before {
@@ -291,18 +291,18 @@ body::before {
/* 特殊排名样式 */
.movie-item.top-1 {
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, var(--bg-primary) 100%);
border-color: rgba(255, 215, 0, 0.3);
background: linear-gradient(135deg, rgba(76, 175, 80, 0.08) 0%, var(--bg-primary) 100%);
border-color: rgba(76, 175, 80, 0.4);
}
.movie-item.top-2 {
background: linear-gradient(135deg, rgba(192, 192, 192, 0.1) 0%, var(--bg-primary) 100%);
border-color: rgba(192, 192, 192, 0.3);
background: linear-gradient(135deg, rgba(129, 199, 132, 0.06) 0%, var(--bg-primary) 100%);
border-color: rgba(129, 199, 132, 0.3);
}
.movie-item.top-3 {
background: linear-gradient(135deg, rgba(205, 127, 50, 0.1) 0%, var(--bg-primary) 100%);
border-color: rgba(205, 127, 50, 0.3);
background: linear-gradient(135deg, rgba(165, 214, 167, 0.05) 0%, var(--bg-primary) 100%);
border-color: rgba(165, 214, 167, 0.3);
}
/* 排名徽章 */
@@ -325,27 +325,27 @@ body::before {
}
.movie-rank.gold {
background: linear-gradient(135deg, #ffd700, #ffb700);
background: linear-gradient(135deg, #2e7d32, #4caf50);
color: white;
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.4);
box-shadow: 0 4px 15px rgba(46, 125, 50, 0.4);
}
.movie-rank.silver {
background: linear-gradient(135deg, #c0c0c0, #a0a0a0);
background: linear-gradient(135deg, #388e3c, #66bb6a);
color: white;
box-shadow: 0 4px 15px rgba(192, 192, 192, 0.4);
box-shadow: 0 4px 15px rgba(56, 142, 60, 0.4);
}
.movie-rank.bronze {
background: linear-gradient(135deg, #cd7f32, #b06728);
background: linear-gradient(135deg, #4caf50, #81c784);
color: white;
box-shadow: 0 4px 15px rgba(205, 127, 50, 0.4);
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.4);
}
.movie-rank.regular {
background: linear-gradient(135deg, var(--primary-color), var(--accent-color));
background: linear-gradient(135deg, #66bb6a, #a5d6a7);
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
box-shadow: 0 4px 15px rgba(102, 187, 106, 0.3);
}
/* 电影内容 */

View File

@@ -0,0 +1,76 @@
.background-canvas {
position: fixed;
inset: 0;
z-index: -1;
overflow: hidden;
background: linear-gradient(180deg, #f4fff6 0%, #e7f8eb 45%, #def1e4 100%);
}
.glow {
position: absolute;
width: 420px;
height: 420px;
border-radius: 55% 45% 60% 40% / 48% 52% 45% 55%;
opacity: 0.25;
filter: blur(0px);
background: radial-gradient(circle at 35% 35%, rgba(140, 214, 167, 0.65), rgba(140, 214, 167, 0));
animation: floaty 32s ease-in-out infinite;
}
.glow-1 {
top: -140px;
left: -160px;
animation-delay: 0s;
}
.glow-2 {
top: 55%;
left: 60%;
animation-delay: 8s;
background: radial-gradient(circle at 60% 60%, rgba(120, 192, 152, 0.55), rgba(120, 192, 152, 0));
}
.glow-3 {
bottom: -160px;
right: -120px;
animation-delay: 16s;
background: radial-gradient(circle at 40% 40%, rgba(176, 229, 197, 0.6), rgba(176, 229, 197, 0));
}
@keyframes floaty {
0% {
transform: translate3d(0, 0, 0) scale(1);
}
30% {
transform: translate3d(35px, -25px, 0) scale(1.05);
}
60% {
transform: translate3d(-30px, 30px, 0) scale(0.95);
}
100% {
transform: translate3d(0, 0, 0) scale(1);
}
}
@media (max-width: 768px) {
.glow {
width: 260px;
height: 260px;
opacity: 0.22;
}
.glow-1 {
top: -110px;
left: -140px;
}
.glow-2 {
top: 48%;
left: 38%;
}
.glow-3 {
bottom: -140px;
right: -120px;
}
}

View File

@@ -0,0 +1,393 @@
:root {
--surface-base: rgba(255, 255, 255, 0.85);
--surface-soft: rgba(248, 253, 249, 0.9);
--border-soft: rgba(120, 192, 152, 0.22);
--accent-strong: #4caf7a;
--accent-soft: #8fd5a4;
--accent-pale: #c6efd5;
--text-primary: #134a32;
--text-muted: #528169;
--chip-bg: rgba(140, 214, 167, 0.18);
--shadow-soft: 0 16px 40px rgba(31, 74, 53, 0.14);
color-scheme: light;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
color: var(--text-primary);
background: transparent;
line-height: 1.6;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
.app {
width: min(100%, 930px);
margin: 0 auto;
padding: 20px 16px 80px;
display: flex;
flex-direction: column;
gap: 18px;
}
.hero {
display: flex;
flex-direction: column;
gap: 18px;
background: var(--surface-base);
border: 1px solid var(--border-soft);
border-radius: 22px;
padding: 22px 18px;
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
.hero-text {
display: flex;
flex-direction: column;
gap: 10px;
}
.badge {
align-self: flex-start;
padding: 4px 12px;
font-size: 0.82rem;
letter-spacing: 0.08em;
border-radius: 999px;
background: var(--chip-bg);
color: var(--accent-strong);
}
.hero h1 {
font-size: 1.65rem;
font-weight: 700;
}
.subtitle {
color: var(--text-muted);
font-size: 0.96rem;
}
.hero-actions {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.refresh {
padding: 10px 20px;
border-radius: 999px;
border: none;
background: linear-gradient(135deg, #66bb86, #4caf7a);
color: #ffffff;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
box-shadow: 0 12px 30px rgba(76, 175, 122, 0.36);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.refresh:active {
transform: scale(0.97);
}
.refresh:disabled {
opacity: 0.65;
cursor: not-allowed;
}
.update-time {
font-size: 0.85rem;
color: var(--text-muted);
}
.quick-stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.stat-card {
background: var(--surface-soft);
border-radius: 18px;
border: 1px solid rgba(143, 213, 164, 0.24);
padding: 16px 18px;
box-shadow: var(--shadow-soft);
display: flex;
flex-direction: column;
gap: 6px;
}
.stat-label {
font-size: 0.84rem;
color: var(--text-muted);
letter-spacing: 0.04em;
}
.stat-value {
font-size: 1.35rem;
font-weight: 700;
color: var(--accent-strong);
display: flex;
align-items: baseline;
gap: 4px;
}
.stat-value .unit {
font-size: 0.9rem;
font-weight: 500;
color: var(--text-muted);
}
.list-section {
background: var(--surface-base);
border: 1px solid var(--border-soft);
border-radius: 24px;
padding: 24px 18px 28px;
box-shadow: var(--shadow-soft);
display: flex;
flex-direction: column;
gap: 18px;
}
.list-header {
display: flex;
flex-direction: column;
gap: 6px;
}
.list-header h2 {
font-size: 1.28rem;
font-weight: 700;
}
.list-tag {
font-size: 0.88rem;
color: var(--text-muted);
}
.series-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.loading,
.error-message,
.empty-message {
padding: 18px 16px;
text-align: center;
color: var(--text-muted);
background: rgba(255, 255, 255, 0.75);
border-radius: 14px;
border: 1px dashed rgba(120, 192, 152, 0.32);
}
.series-item {
display: grid;
grid-template-columns: auto 1fr;
gap: 14px;
background: rgba(255, 255, 255, 0.92);
border-radius: 20px;
border: 1px solid rgba(120, 192, 152, 0.2);
padding: 16px;
box-shadow: 0 14px 32px rgba(31, 74, 53, 0.12);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.series-item:hover {
transform: translateY(-2px);
box-shadow: 0 18px 40px rgba(31, 74, 53, 0.16);
}
.rank-pill {
width: 44px;
height: 44px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.05rem;
color: #ffffff;
background: linear-gradient(135deg, #7ed49b, #5cc88a);
}
.rank-pill.top-1 {
background: linear-gradient(135deg, #5cc88a, #3da36b);
}
.rank-pill.top-2 {
background: linear-gradient(135deg, #72d0a0, #55be85);
}
.rank-pill.top-3 {
background: linear-gradient(135deg, #8fe0b4, #6fd09a);
}
.series-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.series-head {
display: flex;
flex-direction: column;
gap: 6px;
}
.series-name {
font-size: 1.05rem;
font-weight: 700;
color: var(--text-primary);
}
.series-meta {
font-size: 0.9rem;
color: var(--text-muted);
}
.metric-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 14px;
}
.metric {
display: flex;
flex-direction: column;
gap: 2px;
}
.metric-label {
font-size: 0.78rem;
color: var(--text-muted);
letter-spacing: 0.04em;
}
.metric-value {
font-size: 0.98rem;
font-weight: 600;
color: var(--accent-strong);
}
.progress-wrap {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 6px;
}
.progress-row {
display: flex;
flex-direction: column;
gap: 4px;
}
.progress-label {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--text-muted);
}
.progress-bar {
width: 100%;
height: 6px;
border-radius: 6px;
background: rgba(120, 192, 152, 0.18);
overflow: hidden;
}
.progress-bar span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(135deg, rgba(120, 192, 152, 0.9), rgba(76, 175, 122, 0.95));
width: 0;
transition: width 0.45s ease;
}
/* Tablet */
@media (min-width: 600px) {
.app {
padding: 26px 20px 96px;
gap: 22px;
}
.hero {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 26px 28px;
}
.hero h1 {
font-size: 1.9rem;
}
.hero-actions {
align-items: flex-end;
}
.quick-stats {
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
}
.series-item {
grid-template-columns: 70px 1fr;
padding: 18px 22px;
}
.rank-pill {
width: 54px;
height: 54px;
font-size: 1.2rem;
}
.metric-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
/* Desktop */
@media (min-width: 1024px) {
.app {
padding: 34px 24px 110px;
}
.list-section {
padding: 30px 32px 36px;
}
.series-item {
grid-template-columns: 96px 1fr;
padding: 22px 26px;
}
.series-name {
font-size: 1.22rem;
}
.metric-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>猫眼网剧实时热度</title>
<link rel="stylesheet" href="./css/background.css">
<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<div class="background-canvas">
<div class="glow glow-1"></div>
<div class="glow glow-2"></div>
<div class="glow glow-3"></div>
</div>
<div class="app">
<header class="hero">
<div class="hero-text">
<span class="badge">实时热度</span>
<h1>猫眼网剧实时热度</h1>
<p class="subtitle">网剧热度榜单即时更新,洞察全网追剧风向</p>
</div>
<div class="hero-actions">
<button type="button" id="refreshButton" class="refresh">手动刷新</button>
<span id="updateTime" class="update-time">正在获取最新数据...</span>
</div>
</header>
<section class="quick-stats" id="quickStats">
<div class="stat-card">
<p class="stat-label">上榜剧集</p>
<p class="stat-value" id="seriesCount">--</p>
</div>
<div class="stat-card">
<p class="stat-label">最高热度值</p>
<p class="stat-value"><span id="topHeat">--</span><span class="unit">热度</span></p>
</div>
<div class="stat-card">
<p class="stat-label">平均热度值</p>
<p class="stat-value"><span id="avgHeat">--</span><span class="unit">热度</span></p>
</div>
<div class="stat-card">
<p class="stat-label">官方刷新频率</p>
<p class="stat-value" id="refreshGap">--</p>
</div>
</section>
<section class="list-section">
<div class="list-header">
<h2>网剧热度排行</h2>
<span class="list-tag">数据持续刷新</span>
</div>
<div id="seriesList" class="series-list">
<div class="loading">正在载入网剧热度...</div>
</div>
</section>
</div>
<script src="./js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,293 @@
const API_ENDPOINTS = [
"https://60s.api.shumengya.top/v2/maoyan/realtime/web"
];
const FALLBACK_ENDPOINT = "./返回接口.json";
const REFRESH_INTERVAL = 4500;
const MAX_ITEMS = 40;
const refreshButton = document.getElementById("refreshButton");
const updateTimeEl = document.getElementById("updateTime");
const seriesListEl = document.getElementById("seriesList");
const seriesCountEl = document.getElementById("seriesCount");
const topHeatEl = document.getElementById("topHeat");
const avgHeatEl = document.getElementById("avgHeat");
const refreshGapEl = document.getElementById("refreshGap");
let isLoading = false;
let autoTimer = null;
function escapeHtml(value) {
if (value === undefined || value === null) {
return "";
}
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function safeText(value, fallback = "--") {
if (value === undefined || value === null || value === "") {
return fallback;
}
return escapeHtml(value);
}
function formatNumber(value, fractionDigits = 2) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return "--";
}
return numeric.toFixed(fractionDigits);
}
function formatGap(seconds) {
const numeric = Number(seconds);
if (!Number.isFinite(numeric) || numeric <= 0) {
return "--";
}
if (numeric < 60) {
return `约每 ${Math.round(numeric)}`;
}
const minutes = Math.floor(numeric / 60);
const remainder = Math.round(numeric % 60);
if (remainder === 0) {
return `约每 ${minutes} 分钟`;
}
return `约每 ${minutes}${remainder}`;
}
function formatUpdateTime(data) {
if (data && typeof data.updated === "string" && data.updated.trim()) {
return data.updated.trim();
}
if (data && typeof data.updated_at === "number" && Number.isFinite(data.updated_at)) {
return new Date(data.updated_at).toLocaleString("zh-CN", { hour12: false });
}
return new Date().toLocaleString("zh-CN", { hour12: false });
}
function renderStats(list, gapSeconds) {
const total = Array.isArray(list) ? list.length : 0;
seriesCountEl.textContent = total ? total.toString() : "--";
if (total) {
let maxHeat = 0;
let sumHeat = 0;
list.forEach(item => {
const heat = Number(item?.curr_heat);
if (Number.isFinite(heat)) {
if (heat > maxHeat) {
maxHeat = heat;
}
sumHeat += heat;
}
});
topHeatEl.textContent = maxHeat ? maxHeat.toFixed(2) : "--";
const average = sumHeat && total ? (sumHeat / total) : 0;
avgHeatEl.textContent = average ? average.toFixed(2) : "--";
} else {
topHeatEl.textContent = "--";
avgHeatEl.textContent = "--";
}
refreshGapEl.textContent = formatGap(gapSeconds);
}
function createMetric(label, value) {
return `
<div class="metric">
<span class="metric-label">${label}</span>
<span class="metric-value">${safeText(value)}</span>
</div>
`;
}
function normalizeBarValue(list) {
let maxValue = 0;
if (Array.isArray(list)) {
list.forEach(item => {
const bar = Number(item?.bar_value ?? item?.curr_heat);
if (Number.isFinite(bar) && bar > maxValue) {
maxValue = bar;
}
});
}
return maxValue || 1;
}
function createSeriesItem(series, index, maxBar) {
const article = document.createElement("article");
article.className = "series-item";
const rankClass = index < 3 ? ` top-${index + 1}` : "";
const name = safeText(series?.series_name || "未命名剧集");
const releaseInfo = safeText(series?.release_info || "--");
const platform = safeText(series?.platform_desc || "--");
const heatDesc = safeText(series?.curr_heat_desc || formatNumber(series?.curr_heat));
const barValue = Number(series?.bar_value ?? series?.curr_heat);
const ratio = Number.isFinite(barValue) && maxBar > 0 ? Math.min(100, Math.max(0, (barValue / maxBar) * 100)) : 0;
article.innerHTML = `
<div class="rank-pill${rankClass}">${index + 1}</div>
<div class="series-body">
<div class="series-head">
<div class="series-name">${name}</div>
<div class="series-meta">${releaseInfo} · ${platform}</div>
</div>
<div class="metric-grid">
${createMetric("实时热度", heatDesc)}
${createMetric("上线信息", releaseInfo)}
${createMetric("播出平台", platform)}
${createMetric("剧集ID", safeText(series?.series_id))}
</div>
<div class="progress-wrap">
<div class="progress-row">
<div class="progress-label">
<span>热度走势</span>
<span>${heatDesc}</span>
</div>
<div class="progress-bar"><span style="width: ${ratio}%"></span></div>
</div>
</div>
</div>
`;
return article;
}
function renderSeriesList(list) {
seriesListEl.innerHTML = "";
if (!Array.isArray(list) || list.length === 0) {
const empty = document.createElement("div");
empty.className = "empty-message";
empty.textContent = "暂时没有可展示的剧集数据";
seriesListEl.appendChild(empty);
return;
}
const maxBar = normalizeBarValue(list);
list.slice(0, MAX_ITEMS).forEach((series, index) => {
seriesListEl.appendChild(createSeriesItem(series, index, maxBar));
});
}
async function requestJson(url) {
const response = await fetch(url, { cache: "no-store" });
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`);
}
return response.json();
}
async function retrieveData() {
for (const endpoint of API_ENDPOINTS) {
try {
const result = await requestJson(endpoint);
if (result?.code === 200 && result?.data) {
return result.data;
}
} catch (error) {
console.warn("主接口请求失败", error);
}
}
try {
const fallbackResult = await requestJson(FALLBACK_ENDPOINT);
if (fallbackResult?.data) {
return fallbackResult.data;
}
} catch (fallbackError) {
console.warn("本地示例数据读取失败", fallbackError);
}
return null;
}
async function loadData(isManual = false) {
if (isLoading) {
return;
}
isLoading = true;
if (isManual) {
refreshButton.disabled = true;
refreshButton.textContent = "刷新中...";
}
if (!seriesListEl.children.length) {
seriesListEl.innerHTML = '<div class="loading">正在载入网剧热度...</div>';
}
try {
const data = await retrieveData();
if (!data) {
throw new Error("无法获取数据");
}
const list = Array.isArray(data.list) ? data.list : [];
renderSeriesList(list);
renderStats(list, data.update_gap_second);
updateTimeEl.textContent = `最近更新 ${formatUpdateTime(data)}`;
} catch (error) {
console.error("加载数据失败", error);
seriesListEl.innerHTML = "";
const errBox = document.createElement("div");
errBox.className = "error-message";
errBox.textContent = "数据获取暂时不可用,系统稍后会自动重试";
seriesListEl.appendChild(errBox);
updateTimeEl.textContent = "最近更新 --";
renderStats([], 0);
} finally {
if (isManual) {
refreshButton.disabled = false;
refreshButton.textContent = "手动刷新";
}
isLoading = false;
}
}
function startAutoRefresh() {
if (autoTimer) {
clearInterval(autoTimer);
}
autoTimer = setInterval(() => {
loadData(false);
}, REFRESH_INTERVAL);
}
refreshButton.addEventListener("click", () => {
loadData(true);
});
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
if (autoTimer) {
clearInterval(autoTimer);
autoTimer = null;
}
} else {
startAutoRefresh();
loadData(false);
}
});
function init() {
loadData(false);
startAutoRefresh();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}

View File

@@ -0,0 +1,311 @@
{
"code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": {
"update_gap_second": 3,
"updated": "2025-09-26 16:36:56",
"updated_at": 1758875816062,
"list": [
{
"series_id": 1517707,
"series_name": "赴山海",
"release_info": "上线16天",
"platform_desc": "多平台播放",
"platform_txt": -1,
"curr_heat": 6290.29,
"curr_heat_desc": "6290.29",
"bar_value": 6290.29
},
{
"series_id": 1528168,
"series_name": "许我耀眼",
"release_info": "上线首日",
"platform_desc": "腾讯视频独播",
"platform_txt": -1,
"curr_heat": 6231.35,
"curr_heat_desc": "6231.35",
"bar_value": 5749.54721862606
},
{
"series_id": 1528151,
"series_name": "欢乐家长群2",
"release_info": "上线12天",
"platform_desc": "芒果TV独播",
"platform_txt": -1,
"curr_heat": 6012.95,
"curr_heat_desc": "6012.95",
"bar_value": 5119.06438712135
},
{
"series_id": 1492955,
"series_name": "吴邪私家笔记",
"release_info": "上线7天",
"platform_desc": "腾讯视频独播",
"platform_txt": -1,
"curr_heat": 5851.91,
"curr_heat_desc": "5851.91",
"bar_value": 4596.76326056356
},
{
"series_id": 1538034,
"series_name": "不眠日",
"release_info": "上线10天",
"platform_desc": "多平台播放",
"platform_txt": -1,
"curr_heat": 5804.09,
"curr_heat_desc": "5804.09",
"bar_value": 4206.68639815508
},
{
"series_id": 1501684,
"series_name": "灼灼韶华",
"release_info": "上线16天",
"platform_desc": "优酷独播",
"platform_txt": 0,
"curr_heat": 5799.01,
"curr_heat_desc": "5799.01",
"bar_value": 3878.03171596132
},
{
"series_id": 1474248,
"series_name": "围猎",
"release_info": "上线2天",
"platform_desc": "多平台播放",
"platform_txt": -1,
"curr_heat": 5752.05,
"curr_heat_desc": "5752.05",
"bar_value": 3549.20963005863
},
{
"series_id": 1501687,
"series_name": "守护者们",
"release_info": "上线4天",
"platform_desc": "多平台播放",
"platform_txt": -1,
"curr_heat": 5730.63,
"curr_heat_desc": "5730.63",
"bar_value": 3262.59275525736
},
{
"series_id": 1520710,
"series_name": "生万物",
"release_info": "上线45天",
"platform_desc": "爱奇艺独播",
"platform_txt": 1,
"curr_heat": 5475.27,
"curr_heat_desc": "5475.27",
"bar_value": 2876.18977832356
},
{
"series_id": 1520734,
"series_name": "芬芳喜事",
"release_info": "上线5天",
"platform_desc": "腾讯视频独播",
"platform_txt": -1,
"curr_heat": 5462.54,
"curr_heat_desc": "5462.54",
"bar_value": 2647.63508938203
},
{
"series_id": 1506349,
"series_name": "子夜归",
"release_info": "上线40天",
"platform_desc": "腾讯视频独播",
"platform_txt": -1,
"curr_heat": 5414.09,
"curr_heat_desc": "5414.09",
"bar_value": 2421.25465526037
},
{
"series_id": 1568466,
"series_name": "照镜辞",
"release_info": "上线8天",
"platform_desc": "哔哩哔哩独播",
"platform_txt": -1,
"curr_heat": 5405.94,
"curr_heat_desc": "5405.94",
"bar_value": 2230.68228745166
},
{
"series_id": 1501665,
"series_name": "足迹",
"release_info": "上线23天",
"platform_desc": "多平台播放",
"platform_txt": -1,
"curr_heat": 5345.55,
"curr_heat_desc": "5345.55",
"bar_value": 2035.21546242053
},
{
"series_id": 1481475,
"series_name": "与晋长安",
"release_info": "上线34天",
"platform_desc": "爱奇艺独播",
"platform_txt": 1,
"curr_heat": 5231.26,
"curr_heat_desc": "5231.26",
"bar_value": 1837.70502435479
},
{
"series_id": 1500426,
"series_name": "归队",
"release_info": "上线33天",
"platform_desc": "腾讯视频独播",
"platform_txt": -1,
"curr_heat": 5167.29,
"curr_heat_desc": "5167.29",
"bar_value": 1674.88052510491
},
{
"series_id": 1513970,
"series_name": "阵地",
"release_info": "上线11天",
"platform_desc": "多平台播放",
"platform_txt": -1,
"curr_heat": 5155.5,
"curr_heat_desc": "5155.50",
"bar_value": 1541.8541283172
},
{
"series_id": 1578416,
"series_name": "金式森林",
"release_info": "上线10天",
"platform_desc": "腾讯视频独播",
"platform_txt": -1,
"curr_heat": 5098.99,
"curr_heat_desc": "5098.99",
"bar_value": 1407.04554929882
},
{
"series_id": 1500365,
"series_name": "锦月如歌",
"release_info": "上线52天",
"platform_desc": "多平台播放",
"platform_txt": -1,
"curr_heat": 5082.9,
"curr_heat_desc": "5082.90",
"bar_value": 1294.15728646218
},
{
"series_id": 1481543,
"series_name": "凡人修仙传",
"release_info": "上线62天",
"platform_desc": "优酷独播",
"platform_txt": 0,
"curr_heat": 5064.74,
"curr_heat_desc": "5064.74",
"bar_value": 1189.82790916312
},
{
"series_id": 1521009,
"series_name": "十二封信",
"release_info": "上线29天",
"platform_desc": "腾讯视频独播",
"platform_txt": -1,
"curr_heat": 5029.58,
"curr_heat_desc": "5029.58",
"bar_value": 1090.21013799029
},
{
"series_id": 1492917,
"series_name": "献鱼",
"release_info": "上线42天",
"platform_desc": "优酷独播",
"platform_txt": 0,
"curr_heat": 5010.95,
"curr_heat_desc": "5010.95",
"bar_value": 1002.19
},
{
"series_id": 1444502,
"series_name": "利剑·玫瑰",
"release_info": "上线61天",
"platform_desc": "多平台播放",
"platform_txt": -1,
"curr_heat": 4972.54,
"curr_heat_desc": "4972.54",
"bar_value": 917.613471447017
},
{
"series_id": 1518217,
"series_name": "扫毒风暴",
"release_info": "上线77天",
"platform_desc": "腾讯视频独播",
"platform_txt": -1,
"curr_heat": 4896.37,
"curr_heat_desc": "4896.37",
"bar_value": 833.695051286619
},
{
"series_id": 1500364,
"series_name": "桃花映江山",
"release_info": "上线94天",
"platform_desc": "腾讯视频独播",
"platform_txt": -1,
"curr_heat": 4806.34,
"curr_heat_desc": "4806.34",
"bar_value": 755.090462080828
},
{
"series_id": 1505465,
"series_name": "定风波",
"release_info": "上线57天",
"platform_desc": "爱奇艺独播",
"platform_txt": 1,
"curr_heat": 4744.14,
"curr_heat_desc": "4744.14",
"bar_value": 687.69123872798
},
{
"series_id": 1531702,
"series_name": "书卷一梦",
"release_info": "上线93天",
"platform_desc": "爱奇艺独播",
"platform_txt": 1,
"curr_heat": 4733.39,
"curr_heat_desc": "4733.39",
"bar_value": 633.081734434469
},
{
"series_id": 1500328,
"series_name": "蓄意宠爱",
"release_info": "上线5天",
"platform_desc": "优酷独播",
"platform_txt": 0,
"curr_heat": 4730.18,
"curr_heat_desc": "4730.18",
"bar_value": 583.736247352187
},
{
"series_id": 1532221,
"series_name": "目之所及",
"release_info": "上线30天",
"platform_desc": "爱奇艺独播",
"platform_txt": 1,
"curr_heat": 4712.48,
"curr_heat_desc": "4712.48",
"bar_value": 536.586836256929
},
{
"series_id": 1524115,
"series_name": "白夜宸缘起三生",
"release_info": "上线13天",
"platform_desc": "腾讯视频独播",
"platform_txt": -1,
"curr_heat": 4653.77,
"curr_heat_desc": "4653.77",
"bar_value": 488.930252012005
},
{
"series_id": 1491942,
"series_name": "朝雪录",
"release_info": "上线76天",
"platform_desc": "爱奇艺独播",
"platform_txt": 1,
"curr_heat": 4623.3,
"curr_heat_desc": "4623.30",
"bar_value": 448.172875941959
}
]
}
}

View File

@@ -8,10 +8,10 @@
/* 主体样式 - iOS风格 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #8B4513 0%, #D2691E 50%, #F4A460 100%);
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc8 50%, #f1f8e9 100%);
min-height: 100vh;
padding: 20px;
color: #1D1D1F;
color: #2e7d32;
line-height: 1.47;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@@ -24,9 +24,9 @@ body {
background: rgba(255, 255, 255, 0.85);
border-radius: 24px;
padding: 32px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
box-shadow: 0 8px 32px rgba(76, 175, 80, 0.15), 0 2px 8px rgba(76, 175, 80, 0.1);
backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.2);
border: 1px solid rgba(76, 175, 80, 0.2);
}
/* 头部样式 - iOS风格 */
@@ -37,14 +37,14 @@ body {
.title {
font-size: 2.25rem;
color: #8B4513;
color: #1b5e20;
margin-bottom: 8px;
font-weight: 600;
letter-spacing: -0.02em;
}
.subtitle {
color: #D2691E;
color: #4caf50;
font-size: 1.0625rem;
margin-bottom: 24px;
font-weight: 400;
@@ -73,14 +73,14 @@ body {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #1D1D1F;
color: #2e7d32;
font-size: 1rem;
}
.form-input {
width: 100%;
padding: 16px;
border: 1px solid rgba(0, 0, 0, 0.1);
border: 1px solid rgba(76, 175, 80, 0.2);
border-radius: 12px;
font-size: 1rem;
transition: all 0.2s ease;
@@ -91,9 +91,9 @@ body {
.form-input:focus {
outline: none;
border-color: #D2691E;
border-color: #4caf50;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 0 0 4px rgba(210, 105, 30, 0.1);
box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.15);
}
.textarea {
@@ -117,7 +117,7 @@ body {
.btn {
width: 100%;
padding: 16px;
background: #D2691E;
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
color: white;
border: none;
border-radius: 12px;
@@ -126,18 +126,18 @@ body {
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(210, 105, 30, 0.25);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}
.btn:hover {
background: #B8860B;
background: linear-gradient(135deg, #388e3c 0%, #4caf50 100%);
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(210, 105, 30, 0.35);
box-shadow: 0 4px 16px rgba(76, 175, 80, 0.4);
}
.btn:active {
transform: translateY(0);
background: #A0522D;
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%);
}
.btn:disabled {
@@ -154,7 +154,7 @@ body {
.result-title {
font-size: 1.25rem;
color: #8B4513;
color: #1b5e20;
margin-bottom: 16px;
text-align: center;
font-weight: 600;
@@ -163,7 +163,7 @@ body {
.loading {
display: none;
text-align: center;
color: #D2691E;
color: #4caf50;
font-style: normal;
padding: 24px;
font-weight: 500;
@@ -192,8 +192,8 @@ body {
}
.conversion-info {
background: rgba(139, 69, 19, 0.1);
border: 1px solid rgba(139, 69, 19, 0.2);
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.2);
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
@@ -210,11 +210,11 @@ body {
.info-item .label {
font-weight: 600;
color: #8B4513;
color: #1b5e20;
}
.info-item .value {
color: #1D1D1F;
color: #2e7d32;
background: rgba(255, 255, 255, 0.8);
padding: 4px 12px;
border-radius: 8px;
@@ -233,8 +233,8 @@ body {
}
.classical-text {
background: rgba(244, 164, 96, 0.1);
border: 1px solid rgba(244, 164, 96, 0.3);
background: rgba(129, 199, 132, 0.1);
border: 1px solid rgba(129, 199, 132, 0.3);
}
.text-header {
@@ -246,13 +246,13 @@ body {
.text-header .label {
font-weight: 600;
color: #1D1D1F;
color: #2e7d32;
font-size: 1rem;
}
.text-content {
font-size: 1.125rem;
color: #1D1D1F;
color: #2e7d32;
line-height: 1.8;
font-weight: 400;
}
@@ -261,7 +261,7 @@ body {
font-family: 'STKaiti', 'KaiTi', '楷体', serif;
font-size: 1.25rem;
line-height: 2;
color: #8B4513;
color: #1b5e20;
font-weight: 500;
}
@@ -276,7 +276,7 @@ body {
.transformations-title {
font-weight: 600;
color: #1D1D1F;
color: #2e7d32;
margin-bottom: 16px;
font-size: 1rem;
}
@@ -286,7 +286,7 @@ body {
align-items: flex-start;
gap: 8px;
padding: 8px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
border-bottom: 1px solid rgba(76, 175, 80, 0.1);
}
.transformation-item:last-child {
@@ -294,20 +294,20 @@ body {
}
.transformation-item .number {
color: #D2691E;
color: #4caf50;
font-weight: 600;
min-width: 20px;
}
.transformation-item .text {
color: #1D1D1F;
color: #2e7d32;
line-height: 1.6;
}
/* 文言文特色分析 */
.classical-features {
background: rgba(210, 105, 30, 0.1);
border: 1px solid rgba(210, 105, 30, 0.2);
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.2);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
@@ -315,7 +315,7 @@ body {
.features-title {
font-weight: 600;
color: #D2691E;
color: #4caf50;
margin-bottom: 16px;
font-size: 1rem;
}
@@ -333,20 +333,20 @@ body {
.feature-label {
font-weight: 600;
color: #8B4513;
color: #1b5e20;
min-width: 80px;
font-size: 0.9375rem;
}
.feature-text {
color: #1D1D1F;
color: #2e7d32;
line-height: 1.6;
flex: 1;
}
.explanation {
background: rgba(139, 69, 19, 0.1);
border: 1px solid rgba(139, 69, 19, 0.2);
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.2);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
@@ -354,20 +354,20 @@ body {
.explanation .label {
font-weight: 600;
color: #8B4513;
color: #1b5e20;
margin-bottom: 12px;
display: block;
}
.explanation-text {
color: #1D1D1F;
color: #2e7d32;
line-height: 1.6;
font-size: 0.9375rem;
}
.pronunciation {
background: rgba(255, 149, 0, 0.1);
border: 1px solid rgba(255, 149, 0, 0.2);
background: rgba(129, 199, 132, 0.1);
border: 1px solid rgba(129, 199, 132, 0.2);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
@@ -376,18 +376,18 @@ body {
.pronunciation .label {
font-weight: 600;
color: #FF9500;
color: #4caf50;
margin-right: 8px;
}
.pronunciation .value {
color: #1D1D1F;
color: #2e7d32;
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;
}
/* 复制按钮样式 */
.copy-btn {
background: #D2691E;
background: #4caf50;
color: white;
border: none;
border-radius: 8px;
@@ -396,16 +396,16 @@ body {
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(210, 105, 30, 0.25);
box-shadow: 0 2px 4px rgba(76, 175, 80, 0.3);
}
.copy-btn:hover {
background: #B8860B;
background: #388e3c;
transform: translateY(-1px);
}
.copy-btn-small {
background: #D2691E;
background: #4caf50;
color: white;
border: none;
border-radius: 6px;
@@ -414,11 +414,11 @@ body {
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(210, 105, 30, 0.25);
box-shadow: 0 1px 2px rgba(76, 175, 80, 0.3);
}
.copy-btn-small:hover {
background: #B8860B;
background: #388e3c;
transform: translateY(-1px);
}
@@ -584,7 +584,7 @@ body {
top: 15px;
left: 15px;
font-size: 1.5rem;
color: #D2691E;
color: #4caf50;
opacity: 0.6;
}
@@ -594,7 +594,7 @@ body {
bottom: 15px;
right: 15px;
font-size: 1.5rem;
color: #D2691E;
color: #4caf50;
opacity: 0.6;
}

View File

@@ -8,10 +8,10 @@
/* 主体样式 - iOS风格 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc8 100%);
min-height: 100vh;
padding: 20px;
color: #1D1D1F;
color: #2e7d32;
line-height: 1.47;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@@ -24,9 +24,9 @@ body {
background: rgba(255, 255, 255, 0.85);
border-radius: 24px;
padding: 32px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
box-shadow: 0 8px 32px rgba(76, 175, 80, 0.15), 0 2px 8px rgba(76, 175, 80, 0.1);
backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.2);
border: 1px solid rgba(76, 175, 80, 0.2);
}
/* 头部样式 - iOS风格 */
@@ -37,14 +37,14 @@ body {
.title {
font-size: 2.25rem;
color: #1D1D1F;
color: #1b5e20;
margin-bottom: 8px;
font-weight: 600;
letter-spacing: -0.02em;
}
.subtitle {
color: #86868B;
color: #4caf50;
font-size: 1.0625rem;
margin-bottom: 24px;
font-weight: 400;
@@ -63,14 +63,14 @@ body {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #1D1D1F;
color: #2e7d32;
font-size: 1rem;
}
.form-input {
width: 100%;
padding: 16px;
border: 1px solid rgba(0, 0, 0, 0.1);
border: 1px solid rgba(76, 175, 80, 0.2);
border-radius: 12px;
font-size: 1rem;
transition: all 0.2s ease;
@@ -81,9 +81,9 @@ body {
.form-input:focus {
outline: none;
border-color: #667eea;
border-color: #4caf50;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.15);
}
.textarea {
@@ -105,7 +105,7 @@ body {
.btn {
width: 100%;
padding: 16px;
background: #667eea;
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
color: white;
border: none;
border-radius: 12px;
@@ -114,18 +114,18 @@ body {
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}
.btn:hover {
background: #5a67d8;
background: linear-gradient(135deg, #388e3c 0%, #4caf50 100%);
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.35);
box-shadow: 0 4px 16px rgba(76, 175, 80, 0.4);
}
.btn:active {
transform: translateY(0);
background: #4c51bf;
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%);
}
.btn:disabled {
@@ -142,7 +142,7 @@ body {
.result-title {
font-size: 1.25rem;
color: #1D1D1F;
color: #1b5e20;
margin-bottom: 16px;
text-align: center;
font-weight: 600;
@@ -151,7 +151,7 @@ body {
.loading {
display: none;
text-align: center;
color: #667eea;
color: #4caf50;
font-style: normal;
padding: 24px;
font-weight: 500;
@@ -181,10 +181,10 @@ body {
color: white;
margin: 20px 0 12px 0;
padding: 12px 16px;
background: #667eea;
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
border-radius: 12px;
text-align: center;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}
.section-title:first-child {
@@ -214,8 +214,8 @@ body {
}
.command-item:hover {
border-color: rgba(102, 126, 234, 0.3);
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.1);
border-color: rgba(76, 175, 80, 0.3);
box-shadow: 0 4px 16px rgba(76, 175, 80, 0.15);
background: rgba(255, 255, 255, 0.95);
}
@@ -312,7 +312,7 @@ body {
}
.copy-btn {
background: #667eea;
background: #4caf50;
color: white;
border: none;
border-radius: 8px;
@@ -321,14 +321,14 @@ body {
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(102, 126, 234, 0.25);
box-shadow: 0 2px 4px rgba(76, 175, 80, 0.3);
position: absolute;
top: 20px;
right: 20px;
}
.copy-btn:hover {
background: #5a67d8;
background: #388e3c;
transform: translateY(-1px);
}
@@ -562,15 +562,15 @@ code {
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
background: rgba(76, 175, 80, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: rgba(102, 126, 234, 0.3);
background: rgba(76, 175, 80, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(102, 126, 234, 0.5);
background: rgba(76, 175, 80, 0.5);
}

View File

@@ -8,10 +8,10 @@
/* 主体样式 - iOS风格 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #FFB6C1 0%, #FFE4E1 100%);
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc8 50%, #f1f8e9 100%);
min-height: 100vh;
padding: 20px;
color: #1D1D1F;
color: #2e7d32;
line-height: 1.47;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@@ -24,9 +24,9 @@ body {
background: rgba(255, 255, 255, 0.85);
border-radius: 24px;
padding: 32px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
box-shadow: 0 8px 32px rgba(76, 175, 80, 0.15), 0 2px 8px rgba(76, 175, 80, 0.1);
backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.2);
border: 1px solid rgba(76, 175, 80, 0.2);
}
/* 头部样式 - iOS风格 */
@@ -37,14 +37,14 @@ body {
.title {
font-size: 2.25rem;
color: #1D1D1F;
color: #1b5e20;
margin-bottom: 8px;
font-weight: 600;
letter-spacing: -0.02em;
}
.subtitle {
color: #86868B;
color: #4caf50;
font-size: 1.0625rem;
margin-bottom: 24px;
font-weight: 400;
@@ -63,14 +63,14 @@ body {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #1D1D1F;
color: #2e7d32;
font-size: 1rem;
}
.form-input {
width: 100%;
padding: 16px;
border: 1px solid rgba(0, 0, 0, 0.1);
border: 1px solid rgba(76, 175, 80, 0.2);
border-radius: 12px;
font-size: 1rem;
transition: all 0.2s ease;
@@ -81,9 +81,9 @@ body {
.form-input:focus {
outline: none;
border-color: #FF69B4;
border-color: #4caf50;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 0 0 4px rgba(255, 105, 180, 0.1);
box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.15);
}
.textarea {
@@ -105,7 +105,7 @@ body {
.btn {
width: 100%;
padding: 16px;
background: #FF69B4;
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
color: white;
border: none;
border-radius: 12px;
@@ -114,18 +114,18 @@ body {
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(255, 105, 180, 0.25);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}
.btn:hover {
background: #FF1493;
background: linear-gradient(135deg, #388e3c 0%, #4caf50 100%);
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(255, 105, 180, 0.35);
box-shadow: 0 4px 16px rgba(76, 175, 80, 0.4);
}
.btn:active {
transform: translateY(0);
background: #DC143C;
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%);
}
.btn:disabled {
@@ -142,7 +142,7 @@ body {
.result-title {
font-size: 1.25rem;
color: #1D1D1F;
color: #1b5e20;
margin-bottom: 16px;
text-align: center;
font-weight: 600;
@@ -151,7 +151,7 @@ body {
.loading {
display: none;
text-align: center;
color: #FF69B4;
color: #4caf50;
font-style: normal;
padding: 24px;
font-weight: 500;
@@ -181,10 +181,10 @@ body {
color: white;
margin: 20px 0 12px 0;
padding: 12px 16px;
background: #FF69B4;
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
border-radius: 12px;
text-align: center;
box-shadow: 0 2px 8px rgba(255, 105, 180, 0.25);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}
.expression-group-title:first-child {
@@ -208,8 +208,8 @@ body {
}
.expression-item:hover {
border-color: rgba(255, 105, 180, 0.3);
box-shadow: 0 4px 16px rgba(255, 105, 180, 0.1);
border-color: rgba(76, 175, 80, 0.3);
box-shadow: 0 4px 16px rgba(76, 175, 80, 0.15);
background: rgba(255, 255, 255, 0.95);
}
@@ -248,7 +248,7 @@ body {
}
.copy-btn {
background: #FF69B4;
background: #4caf50;
color: white;
border: none;
border-radius: 8px;
@@ -257,13 +257,13 @@ body {
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(255, 105, 180, 0.25);
box-shadow: 0 2px 4px rgba(76, 175, 80, 0.3);
margin-top: 8px;
align-self: center;
}
.copy-btn:hover {
background: #FF1493;
background: #388e3c;
transform: translateY(-1px);
}

View File

@@ -5,28 +5,28 @@
box-sizing: border-box;
}
/* 主体样式 - iOS风格 */
/* 主体样式 - 清新绿色风格 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc8 50%, #f1f8e9 100%);
min-height: 100vh;
padding: 20px;
color: #1D1D1F;
color: #2e7d32;
line-height: 1.47;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 容器样式 - iOS毛玻璃效果 */
/* 容器样式 - 清新绿色毛玻璃效果 */
.container {
max-width: 800px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.85);
background: rgba(255, 255, 255, 0.9);
border-radius: 24px;
padding: 32px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
box-shadow: 0 8px 32px rgba(46, 125, 50, 0.15), 0 2px 8px rgba(46, 125, 50, 0.08);
backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.2);
border: 1px solid rgba(168, 230, 207, 0.3);
}
/* 头部样式 - iOS风格 */
@@ -37,14 +37,14 @@ body {
.title {
font-size: 2.25rem;
color: #1D1D1F;
color: #1b5e20;
margin-bottom: 8px;
font-weight: 600;
letter-spacing: -0.02em;
}
.subtitle {
color: #86868B;
color: #4caf50;
font-size: 1.0625rem;
margin-bottom: 24px;
font-weight: 400;
@@ -63,7 +63,7 @@ body {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #1D1D1F;
color: #2e7d32;
font-size: 1rem;
}
@@ -81,9 +81,9 @@ body {
.form-input:focus {
outline: none;
border-color: #007AFF;
border-color: #4caf50;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.1);
box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.15);
}
.textarea {
@@ -101,11 +101,11 @@ body {
padding-right: 40px;
}
/* 按钮样式 - iOS风格 */
/* 按钮样式 - 清新绿色风格 */
.btn {
width: 100%;
padding: 16px;
background: #007AFF;
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
color: white;
border: none;
border-radius: 12px;
@@ -114,18 +114,18 @@ body {
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.25);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}
.btn:hover {
background: #0056CC;
background: linear-gradient(135deg, #388e3c 0%, #4caf50 100%);
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.35);
box-shadow: 0 4px 16px rgba(76, 175, 80, 0.4);
}
.btn:active {
transform: translateY(0);
background: #004499;
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%);
}
.btn:disabled {
@@ -142,7 +142,7 @@ body {
.result-title {
font-size: 1.25rem;
color: #1D1D1F;
color: #2e7d32;
margin-bottom: 16px;
text-align: center;
font-weight: 600;
@@ -151,7 +151,7 @@ body {
.loading {
display: none;
text-align: center;
color: #007AFF;
color: #4caf50;
font-style: normal;
padding: 24px;
font-weight: 500;
@@ -195,12 +195,12 @@ body {
}
.detected-language .value {
color: #1D1D1F;
color: #2e7d32;
}
.main-translation {
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.06);
border: 1px solid rgba(76, 175, 80, 0.1);
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
@@ -216,13 +216,13 @@ body {
.translation-header .label {
font-weight: 600;
color: #1D1D1F;
color: #2e7d32;
font-size: 1rem;
}
.translation-text {
font-size: 1.125rem;
color: #1D1D1F;
color: #2e7d32;
line-height: 1.6;
font-weight: 500;
}
@@ -243,13 +243,13 @@ body {
}
.pronunciation .value {
color: #1D1D1F;
color: #2e7d32;
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;
}
.alternatives {
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(0, 0, 0, 0.06);
border: 1px solid rgba(76, 175, 80, 0.1);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
@@ -257,7 +257,7 @@ body {
.alternatives-title {
font-weight: 600;
color: #1D1D1F;
color: #2e7d32;
margin-bottom: 12px;
font-size: 1rem;
}
@@ -282,33 +282,33 @@ body {
}
.alternative-text {
color: #1D1D1F;
color: #2e7d32;
font-size: 1rem;
}
.explanation {
background: rgba(0, 122, 255, 0.1);
border: 1px solid rgba(0, 122, 255, 0.2);
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.2);
border-radius: 12px;
padding: 16px;
}
.explanation .label {
font-weight: 600;
color: #007AFF;
color: #4caf50;
margin-bottom: 8px;
display: block;
}
.explanation-text {
color: #1D1D1F;
color: #2e7d32;
line-height: 1.6;
font-size: 0.9375rem;
}
/* 复制按钮样式 */
.copy-btn {
background: #007AFF;
background: #4caf50;
color: white;
border: none;
border-radius: 8px;
@@ -317,16 +317,16 @@ body {
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 122, 255, 0.25);
box-shadow: 0 2px 4px rgba(76, 175, 80, 0.3);
}
.copy-btn:hover {
background: #0056CC;
background: #388e3c;
transform: translateY(-1px);
}
.copy-btn-small {
background: #007AFF;
background: #4caf50;
color: white;
border: none;
border-radius: 6px;
@@ -335,11 +335,11 @@ body {
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 122, 255, 0.25);
box-shadow: 0 1px 2px rgba(76, 175, 80, 0.3);
}
.copy-btn-small:hover {
background: #0056CC;
background: #388e3c;
transform: translateY(-1px);
}

View File

@@ -49,53 +49,238 @@
z-index: 9999;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
overflow: hidden;
}
/* 背景粒子动画 */
#loading::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 80%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(255, 255, 255, 0.05) 0%, transparent 50%);
animation: backgroundFloat 8s ease-in-out infinite;
}
/* 流动渐变背景 */
#loading::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
45deg,
transparent,
rgba(255, 255, 255, 0.03),
transparent,
rgba(255, 255, 255, 0.03),
transparent
);
animation: gradientFlow 6s linear infinite;
}
/* Logo容器 */
.loading-logo-container {
position: relative;
margin-bottom: 24px;
}
/* Logo增强动画 */
#loading .loading-logo {
width: 90px;
height: 90px;
margin-bottom: 24px;
animation: pulse 2s infinite;
border-radius: 8px;
border-radius: 12px;
position: relative;
z-index: 2;
animation: logoEnhanced 3s ease-in-out infinite;
filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.3));
}
#loading .loading-text {
font-size: 34px;
font-weight: bold;
/* Logo光晕效果 */
.loading-logo-container::before {
content: '';
position: absolute;
top: -10px;
left: -10px;
right: -10px;
bottom: -10px;
background: linear-gradient(45deg, rgba(144, 238, 144, 0.3), rgba(240, 230, 140, 0.3), rgba(144, 238, 144, 0.3));
border-radius: 20px;
z-index: 1;
animation: logoGlow 3s ease-in-out infinite;
opacity: 0.4;
}
/* 文字容器 */
.loading-text-container {
position: relative;
margin-bottom: 16px;
}
#loading .loading-desc {
font-size: 20px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 32px;
/* 标题文字增强动画 */
#loading .loading-text {
font-size: 34px;
font-weight: bold;
background: linear-gradient(45deg, #ffffff, #f0f8ff, #ffffff, #e6f3ff);
background-size: 300% 300%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: textGradient 3s ease-in-out infinite, textFloat 4s ease-in-out infinite;
text-shadow: 0 0 30px rgba(255, 255, 255, 0.5);
}
/* 描述文字动画 */
#loading .loading-desc {
font-size: 20px;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 32px;
animation: descFloat 5s ease-in-out infinite;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
/* 加载器容器 */
.loading-spinner-container {
position: relative;
width: 80px;
height: 80px;
}
/* 主加载器 */
#loading .loading-spinner {
width: 56px;
height: 56px;
border: 6px solid rgba(255, 255, 255, 0.3);
border-top: 6px solid #ffffff;
border: 4px solid transparent;
border-top: 4px solid #ffffff;
border-right: 4px solid rgba(255, 255, 255, 0.8);
border-bottom: 4px solid rgba(255, 255, 255, 0.6);
border-left: 4px solid rgba(255, 255, 255, 0.4);
border-radius: 50%;
animation: spin 1s linear infinite;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: spinEnhanced 1.2s linear infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
/* 外层加载器 */
.loading-spinner-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 80px;
height: 80px;
border: 3px solid transparent;
border-top: 3px solid rgba(255, 255, 255, 0.4);
border-radius: 50%;
animation: spinSlow 2s linear infinite reverse;
}
@keyframes spin {
/* 内层加载器 */
.loading-spinner-container::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 30px;
height: 30px;
border: 2px solid transparent;
border-top: 2px solid rgba(255, 255, 255, 0.6);
border-radius: 50%;
transform: translate(-50%, -50%);
animation: spinFast 0.8s linear infinite;
}
/* 动画定义 */
@keyframes logoEnhanced {
0%, 100% {
transform: scale(1) rotate(0deg);
filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.3));
}
25% {
transform: scale(1.05) rotate(2deg);
filter: drop-shadow(0 0 25px rgba(255, 255, 255, 0.4));
}
50% {
transform: scale(1.1) rotate(0deg);
filter: drop-shadow(0 0 30px rgba(255, 255, 255, 0.5));
}
75% {
transform: scale(1.05) rotate(-2deg);
filter: drop-shadow(0 0 25px rgba(255, 255, 255, 0.4));
}
}
@keyframes logoGlow {
0% { transform: scale(1); opacity: 0.2; }
50% { transform: scale(1.02); opacity: 0.4; }
100% { transform: scale(1); opacity: 0.2; }
}
@keyframes textGradient {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
@keyframes textFloat {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
}
@keyframes descFloat {
0%, 100% { transform: translateY(0px); opacity: 0.9; }
50% { transform: translateY(-3px); opacity: 1; }
}
@keyframes spinEnhanced {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
@keyframes spinSlow {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes spinFast {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
@keyframes backgroundFloat {
0%, 100% { transform: translateY(0px) translateX(0px); }
25% { transform: translateY(-10px) translateX(5px); }
50% { transform: translateY(-5px) translateX(-5px); }
75% { transform: translateY(-15px) translateX(3px); }
}
@keyframes gradientFlow {
0% { transform: translateX(-100%) translateY(-100%) rotate(0deg); }
100% { transform: translateX(100%) translateY(100%) rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 768px) {
#loading .loading-logo {
width: 67px;
height: 67px;
}
.loading-logo-container::before {
top: -8px;
left: -8px;
right: -8px;
bottom: -8px;
}
#loading .loading-text {
font-size: 28px;
}
@@ -104,11 +289,27 @@
font-size: 17px;
}
.loading-spinner-container {
width: 60px;
height: 60px;
}
#loading .loading-spinner {
width: 42px;
height: 42px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid #ffffff;
border-width: 3px;
}
.loading-spinner-container::before {
width: 60px;
height: 60px;
border-width: 2px;
}
.loading-spinner-container::after {
width: 24px;
height: 24px;
border-width: 2px;
}
}
</style>
@@ -118,10 +319,16 @@
<!-- 加载动画 -->
<div id="loading">
<img class="loading-logo" src="%PUBLIC_URL%/assets/logo.png" alt="万象口袋" />
<div class="loading-text">万象口袋</div>
<div class="loading-logo-container">
<img class="loading-logo" src="%PUBLIC_URL%/assets/logo.png" alt="万象口袋" />
</div>
<div class="loading-text-container">
<div class="loading-text">万象口袋</div>
</div>
<div class="loading-desc">🎨 一个跨平台的多功能聚合应用(´。• ω •。`) 💬</div>
<div class="loading-spinner"></div>
<div class="loading-spinner-container">
<div class="loading-spinner"></div>
</div>
</div>
<div id="root"></div>

View File

@@ -26,7 +26,15 @@ class GameControls {
}
initKeyboardControls() {
document.addEventListener('keydown', (e) => {
// 确保iframe能够获得焦点并接收键盘事件
const gameContainer = document.querySelector('.container');
if (gameContainer) {
gameContainer.setAttribute('tabindex', '0');
gameContainer.focus();
}
// 为document和window都添加键盘事件监听器确保在iframe中也能工作
const handleKeyDown = (e) => {
if (!this.isGameActive || !window.game2048) {
console.log('Game not ready:', { isGameActive: this.isGameActive, game2048: !!window.game2048 });
return;
@@ -69,7 +77,18 @@ class GameControls {
this.togglePause();
break;
}
});
};
// 同时监听document和window的键盘事件
document.addEventListener('keydown', handleKeyDown);
window.addEventListener('keydown', handleKeyDown);
// 确保游戏容器在点击时获得焦点
if (gameContainer) {
gameContainer.addEventListener('click', () => {
gameContainer.focus();
});
}
}
initTouchControls() {
@@ -389,90 +408,6 @@ class GameControls {
enable() {
this.isGameActive = true;
}
// 显示控制提示
showControlHints() {
const hints = document.createElement('div');
hints.className = 'control-hints';
hints.innerHTML = `
<div class="hint-content">
<h3>操作说明</h3>
<div class="hint-section">
<h4>📱 手机操作</h4>
<p>在游戏区域滑动手指移动方块</p>
<div class="gesture-demo">
<span>👆 上滑</span>
<span>👇 下滑</span>
<span>👈 左滑</span>
<span>👉 右滑</span>
</div>
</div>
<div class="hint-section">
<h4>⌨️ 键盘操作</h4>
<div class="key-demo">
<div class="key-row">
<span class="key">↑</span>
<span class="key">W</span>
<span>上移</span>
</div>
<div class="key-row">
<span class="key">↓</span>
<span class="key">S</span>
<span>下移</span>
</div>
<div class="key-row">
<span class="key">←</span>
<span class="key">A</span>
<span>左移</span>
</div>
<div class="key-row">
<span class="key">→</span>
<span class="key">D</span>
<span>右移</span>
</div>
<div class="key-row">
<span class="key">R</span>
<span>重新开始</span>
</div>
<div class="key-row">
<span class="key">ESC</span>
<span>暂停/继续</span>
</div>
</div>
</div>
<button class="close-hints">知道了</button>
</div>
`;
// 添加样式
hints.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
backdrop-filter: blur(5px);
`;
document.body.appendChild(hints);
// 关闭按钮事件
hints.querySelector('.close-hints').addEventListener('click', () => {
hints.remove();
});
// 点击背景关闭
hints.addEventListener('click', (e) => {
if (e.target === hints) {
hints.remove();
}
});
}
}
// 创建全局控制实例
@@ -486,8 +421,7 @@ document.addEventListener('DOMContentLoaded', () => {
gameControls = new GameControls();
console.log('Game controls initialized successfully');
// 创建帮助按钮
createHelpButton();
} else {
console.log('Waiting for game2048 to initialize...');
setTimeout(initControls, 100);
@@ -497,41 +431,7 @@ document.addEventListener('DOMContentLoaded', () => {
initControls();
});
// 创建帮助按钮函数
function createHelpButton() {
const helpBtn = document.createElement('button');
helpBtn.textContent = '❓';
helpBtn.title = '操作说明';
helpBtn.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
border: none;
font-size: 20px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
z-index: 1000;
transition: all 0.3s ease;
`;
helpBtn.addEventListener('click', () => {
gameControls.showControlHints();
});
helpBtn.addEventListener('mouseenter', () => {
helpBtn.style.transform = 'scale(1.1)';
});
helpBtn.addEventListener('mouseleave', () => {
helpBtn.style.transform = 'scale(1)';
});
document.body.appendChild(helpBtn);
}
// 导出控制实例
window.gameControls = gameControls;

View File

@@ -4,7 +4,7 @@ class Game2048 {
this.size = 4;
this.grid = [];
this.score = 0;
this.bestScore = parseInt(localStorage.getItem('2048-best-score')) || 0;
this.gameWon = false;
this.gameOver = false;
this.moved = false;
@@ -98,7 +98,6 @@ class Game2048 {
// 更新分数
document.getElementById('score').textContent = this.score;
document.getElementById('best-score').textContent = this.bestScore;
// 更新统计数据显示
if (window.gameStats) {
@@ -359,9 +358,7 @@ class Game2048 {
this.startTimer();
}
keepPlaying() {
document.getElementById('game-message').style.display = 'none';
}
startTimer() {
this.stats.startTime = Date.now();
@@ -381,28 +378,13 @@ class Game2048 {
}
bindEvents() {
// 重新开始按钮
document.getElementById('restart-btn').addEventListener('click', () => {
this.restart();
});
// 继续游戏按钮
document.getElementById('keep-playing').addEventListener('click', () => {
this.keepPlaying();
});
// 重试按钮
document.getElementById('retry-btn').addEventListener('click', () => {
this.restart();
});
}
updateBestScore() {
if (this.score > this.bestScore) {
this.bestScore = this.score;
localStorage.setItem('2048-best-score', this.bestScore.toString());
}
}
}
// 游戏实例
@@ -412,12 +394,7 @@ let game;
document.addEventListener('DOMContentLoaded', () => {
game = new Game2048();
// 监听分数变化以更新最高分
const originalUpdateDisplay = game.updateDisplay.bind(game);
game.updateDisplay = function() {
originalUpdateDisplay();
this.updateBestScore();
};
// 导出游戏实例供其他模块使用
window.game2048 = game;

View File

@@ -0,0 +1,20 @@
const playerdata = [
{
"名称":"树萌芽",
"账号":"3205788256@qq.com",
"分数":1232,
"时间":"2025-09-08"
},
{
"名称":"柚大青",
"账号":"2143323382@qq.com",
"分数":132,
"时间":"2025-09-21"
},
{
"名称":"牛马",
"账号":"2973419538@qq.com",
"分数":876,
"时间":"2025-09-25"
}
]

View File

@@ -7,7 +7,7 @@
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<div class="container" tabindex="0">
<header class="header">
<h1 class="title">2048</h1>
<div class="score-container">
@@ -15,25 +15,14 @@
<div class="score-label">分数</div>
<div class="score" id="score">0</div>
</div>
<div class="score-box">
<div class="score-label">最高分</div>
<div class="score" id="best-score">0</div>
</div>
</div>
</header>
<div class="game-intro">
<p class="game-explanation">
合并相同数字,达到<strong>2048</strong>
</p>
<div class="restart-button" id="restart-btn">新游戏</div>
</div>
<div class="game-container">
<div class="game-message" id="game-message">
<p></p>
<div class="lower">
<a class="keep-playing-button" id="keep-playing">继续游戏</a>
<a class="retry-button" id="retry-btn">重新开始</a>
</div>
</div>
@@ -70,96 +59,12 @@
</div>
</div>
<div class="game-stats" id="game-stats">
<h3>游戏统计</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">移动次数:</span>
<span class="stat-value" id="moves-count">0</span>
</div>
<div class="stat-item">
<span class="stat-label">游戏时间:</span>
<span class="stat-value" id="game-time">00:00</span>
</div>
<div class="stat-item">
<span class="stat-label">最大数字:</span>
<span class="stat-value" id="max-tile">2</span>
</div>
<div class="stat-item">
<span class="stat-label">合并次数:</span>
<span class="stat-value" id="merge-count">0</span>
</div>
</div>
</div>
<div class="controls-hint">
<p><strong>操作说明:</strong></p>
<p>手机: 滑动屏幕移动方块</p>
<p>电脑: 使用方向键 ↑↓←→ 或 WASD 键</p>
</div>
</div>
<!-- 游戏结束统计弹窗 -->
<div class="modal" id="stats-modal">
<div class="modal-content">
<div class="modal-header">
<h2>游戏结束</h2>
<span class="close" id="close-modal">&times;</span>
</div>
<div class="modal-body">
<div class="final-score">
<h3>最终得分: <span id="final-score">0</span></h3>
</div>
<div class="achievement-section">
<h4>成就统计</h4>
<div class="achievement-grid">
<div class="achievement-item">
<span class="achievement-icon">🎯</span>
<div class="achievement-info">
<div class="achievement-title">总移动次数</div>
<div class="achievement-value" id="final-moves">0</div>
</div>
</div>
<div class="achievement-item">
<span class="achievement-icon">⏱️</span>
<div class="achievement-info">
<div class="achievement-title">游戏时长</div>
<div class="achievement-value" id="final-time">00:00</div>
</div>
</div>
<div class="achievement-item">
<span class="achievement-icon">🏆</span>
<div class="achievement-info">
<div class="achievement-title">最大数字</div>
<div class="achievement-value" id="final-max-tile">2</div>
</div>
</div>
<div class="achievement-item">
<span class="achievement-icon">🔥</span>
<div class="achievement-info">
<div class="achievement-title">合并次数</div>
<div class="achievement-value" id="final-merges">0</div>
</div>
</div>
<div class="achievement-item">
<span class="achievement-icon">📊</span>
<div class="achievement-info">
<div class="achievement-title">平均每步得分</div>
<div class="achievement-value" id="avg-score">0</div>
</div>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn btn-primary" id="new-game-btn">开始新游戏</button>
<button class="btn btn-secondary" id="share-btn">分享成绩</button>
</div>
</div>
</div>
</div>
<script src="game-logic.js"></script>
<script src="controls.js"></script>
<script src="statistics.js"></script>
</body>
</html>

View File

@@ -1,381 +0,0 @@
// 游戏统计模块
class GameStatistics {
constructor() {
this.achievements = {
firstWin: false,
speedRunner: false, // 5分钟内达到2048
efficient: false, // 少于500步达到2048
persistent: false, // 游戏时间超过30分钟
merger: false, // 单局合并超过100次
highScorer: false // 分数超过50000
};
this.loadAchievements();
this.initializeModal();
}
updateDisplay() {
if (!window.game2048) return;
const game = window.game2048;
// 更新实时统计显示
document.getElementById('moves-count').textContent = game.stats.moves;
document.getElementById('game-time').textContent = this.formatTime(game.stats.gameTime);
document.getElementById('max-tile').textContent = game.stats.maxTile;
document.getElementById('merge-count').textContent = game.stats.mergeCount;
}
formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}
showFinalStats() {
if (!window.game2048) return;
const game = window.game2048;
const modal = document.getElementById('stats-modal');
// 更新最终统计数据
document.getElementById('final-score').textContent = game.score;
document.getElementById('final-moves').textContent = game.stats.moves;
document.getElementById('final-time').textContent = this.formatTime(game.stats.gameTime);
document.getElementById('final-max-tile').textContent = game.stats.maxTile;
document.getElementById('final-merges').textContent = game.stats.mergeCount;
// 计算平均每步得分
const avgScore = game.stats.moves > 0 ? Math.round(game.score / game.stats.moves) : 0;
document.getElementById('avg-score').textContent = avgScore;
// 检查成就
this.checkAchievements(game);
// 显示模态框
modal.style.display = 'block';
// 添加动画效果
setTimeout(() => {
modal.querySelector('.modal-content').style.transform = 'scale(1)';
}, 10);
}
checkAchievements(game) {
let newAchievements = [];
// 首次胜利
if (game.gameWon && !this.achievements.firstWin) {
this.achievements.firstWin = true;
newAchievements.push('🏆 首次胜利达到了2048');
}
// 速度跑者 - 5分钟内达到2048
if (game.gameWon && game.stats.gameTime <= 300 && !this.achievements.speedRunner) {
this.achievements.speedRunner = true;
newAchievements.push('⚡ 速度跑者5分钟内达到2048');
}
// 高效玩家 - 少于500步达到2048
if (game.gameWon && game.stats.moves < 500 && !this.achievements.efficient) {
this.achievements.efficient = true;
newAchievements.push('🎯 高效玩家少于500步达到2048');
}
// 坚持不懈 - 游戏时间超过30分钟
if (game.stats.gameTime >= 1800 && !this.achievements.persistent) {
this.achievements.persistent = true;
newAchievements.push('⏰ 坚持不懈游戏时间超过30分钟');
}
// 合并大师 - 单局合并超过100次
if (game.stats.mergeCount >= 100 && !this.achievements.merger) {
this.achievements.merger = true;
newAchievements.push('🔥 合并大师单局合并超过100次');
}
// 高分玩家 - 分数超过50000
if (game.score >= 50000 && !this.achievements.highScorer) {
this.achievements.highScorer = true;
newAchievements.push('💎 高分玩家分数超过50000');
}
// 保存成就
if (newAchievements.length > 0) {
this.saveAchievements();
this.showAchievementNotifications(newAchievements);
}
}
showAchievementNotifications(achievements) {
// 在成就区域显示新获得的成就
const achievementSection = document.querySelector('.achievement-section');
achievements.forEach((achievement, index) => {
setTimeout(() => {
const notification = document.createElement('div');
notification.className = 'achievement-notification';
notification.innerHTML = `
<div class="achievement-popup">
<span class="achievement-text">${achievement}</span>
</div>
`;
achievementSection.appendChild(notification);
// 添加样式
const popup = notification.querySelector('.achievement-popup');
popup.style.cssText = `
background: linear-gradient(45deg, #ff6b6b, #feca57);
color: white;
padding: 10px 15px;
border-radius: 20px;
margin: 5px 0;
font-weight: bold;
text-align: center;
animation: achievementSlide 0.5s ease-out;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
`;
// 添加动画样式
if (!document.getElementById('achievement-styles')) {
const style = document.createElement('style');
style.id = 'achievement-styles';
style.textContent = `
@keyframes achievementSlide {
from {
opacity: 0;
transform: translateX(-100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
`;
document.head.appendChild(style);
}
// 3秒后移除通知
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 3000);
}, index * 500);
});
}
saveAchievements() {
localStorage.setItem('2048-achievements', JSON.stringify(this.achievements));
}
loadAchievements() {
const saved = localStorage.getItem('2048-achievements');
if (saved) {
this.achievements = { ...this.achievements, ...JSON.parse(saved) };
}
}
initializeModal() {
const modal = document.getElementById('stats-modal');
const closeBtn = document.getElementById('close-modal');
const newGameBtn = document.getElementById('new-game-btn');
const shareBtn = document.getElementById('share-btn');
// 关闭模态框
closeBtn.addEventListener('click', () => {
modal.style.display = 'none';
});
// 点击模态框外部关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.style.display = 'none';
}
});
// 新游戏按钮
newGameBtn.addEventListener('click', () => {
modal.style.display = 'none';
if (window.game2048) {
window.game2048.restart();
}
});
// 分享按钮
shareBtn.addEventListener('click', () => {
this.shareScore();
});
// ESC键关闭模态框
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.style.display === 'block') {
modal.style.display = 'none';
}
});
}
shareScore() {
if (!window.game2048) return;
const game = window.game2048;
const shareText = `我在2048游戏中获得了${game.score}分!\n` +
`最大数字: ${game.stats.maxTile}\n` +
`移动次数: ${game.stats.moves}\n` +
`游戏时间: ${this.formatTime(game.stats.gameTime)}\n` +
`来挑战一下吧!`;
// 尝试使用Web Share API
if (navigator.share) {
navigator.share({
title: '2048游戏成绩',
text: shareText,
url: window.location.href
}).catch(err => {
console.log('分享失败:', err);
this.fallbackShare(shareText);
});
} else {
this.fallbackShare(shareText);
}
}
fallbackShare(text) {
// 复制到剪贴板
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
this.showToast('成绩已复制到剪贴板!');
}).catch(() => {
this.showShareModal(text);
});
} else {
this.showShareModal(text);
}
}
showShareModal(text) {
// 创建分享文本显示框
const shareModal = document.createElement('div');
shareModal.innerHTML = `
<div style="
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
z-index: 10000;
max-width: 90%;
text-align: center;
">
<h3>分享你的成绩</h3>
<textarea readonly style="
width: 100%;
height: 120px;
margin: 10px 0;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
resize: none;
">${text}</textarea>
<div>
<button onclick="this.parentElement.parentElement.parentElement.remove()" style="
background: #4ecdc4;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 0 5px;
">关闭</button>
<button onclick="
this.parentElement.previousElementSibling.select();
document.execCommand('copy');
alert('已复制到剪贴板!');
" style="
background: #ff6b6b;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 0 5px;
">复制</button>
</div>
</div>
`;
document.body.appendChild(shareModal);
}
showToast(message) {
const toast = document.createElement('div');
toast.textContent = message;
toast.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 24px;
border-radius: 25px;
z-index: 10000;
font-weight: bold;
animation: toastSlide 0.3s ease-out;
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
// 获取游戏统计摘要
getStatsSummary() {
if (!window.game2048) return null;
const game = window.game2048;
return {
score: game.score,
bestScore: game.bestScore,
moves: game.stats.moves,
gameTime: game.stats.gameTime,
maxTile: game.stats.maxTile,
mergeCount: game.stats.mergeCount,
achievements: this.achievements
};
}
// 重置所有统计数据
resetAllStats() {
this.achievements = {
firstWin: false,
speedRunner: false,
efficient: false,
persistent: false,
merger: false,
highScorer: false
};
localStorage.removeItem('2048-achievements');
localStorage.removeItem('2048-best-score');
this.showToast('所有统计数据已重置!');
}
}
// 创建全局统计实例
window.gameStats = new GameStatistics();
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
// 确保统计模块正确初始化
if (!window.gameStats) {
window.gameStats = new GameStatistics();
}
});

View File

@@ -7,8 +7,8 @@
body {
font-family: 'Arial', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #776e65;
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc8 50%, #f1f8e9 100%);
color: #2e7d32;
font-size: 18px;
margin: 0;
padding: 0;
@@ -21,6 +21,12 @@ body {
margin: 0 auto;
padding: 20px;
position: relative;
outline: none; /* 移除默认的焦点轮廓 */
}
.container:focus {
/* 当容器获得焦点时的样式用于iframe环境 */
outline: none;
}
/* 头部样式 */
@@ -35,8 +41,8 @@ body {
.title {
font-size: 48px;
font-weight: bold;
color: #ffffff;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
color: #1b5e20;
text-shadow: 2px 2px 4px rgba(255,255,255,0.5);
margin: 0;
}
@@ -46,17 +52,18 @@ body {
}
.score-box {
background: rgba(255, 255, 255, 0.9);
background: linear-gradient(135deg, #c8e6c9 0%, #e8f5e8 100%);
padding: 10px 15px;
border-radius: 8px;
text-align: center;
min-width: 80px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
box-shadow: 0 2px 8px rgba(46,125,50,0.2);
border: 1px solid rgba(129,199,132,0.3);
}
.score-label {
font-size: 12px;
color: #776e65;
color: #2e7d32;
text-transform: uppercase;
font-weight: bold;
}
@@ -64,54 +71,22 @@ body {
.score {
font-size: 20px;
font-weight: bold;
color: #ffffff;
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
color: #1b5e20;
background: linear-gradient(45deg, #4caf50, #66bb6a);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* 游戏介绍区域 */
.game-intro {
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 12px;
margin-bottom: 20px;
text-align: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.game-explanation {
margin-bottom: 15px;
font-size: 16px;
line-height: 1.5;
}
.restart-button {
background: linear-gradient(45deg, #4ecdc4, #44a08d);
color: white;
padding: 12px 24px;
border-radius: 25px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
display: inline-block;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.restart-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.3);
}
/* 游戏容器 */
.game-container {
position: relative;
background: rgba(255, 255, 255, 0.9);
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%);
border-radius: 12px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
box-shadow: 0 4px 12px rgba(46,125,50,0.15);
border: 1px solid rgba(129,199,132,0.2);
}
/* 网格样式 */
@@ -132,7 +107,7 @@ body {
.grid-cell {
width: calc(25% - 6px);
height: 80px;
background: rgba(238, 228, 218, 0.35);
background: rgba(200, 230, 201, 0.4);
border-radius: 8px;
margin-right: 8px;
position: relative;
@@ -168,17 +143,17 @@ body {
}
/* 不同数字的颜色 */
.tile-2 { background: #eee4da; color: #776e65; }
.tile-4 { background: #ede0c8; color: #776e65; }
.tile-8 { background: #f2b179; color: #f9f6f2; }
.tile-16 { background: #f59563; color: #f9f6f2; }
.tile-32 { background: #f67c5f; color: #f9f6f2; }
.tile-64 { background: #f65e3b; color: #f9f6f2; }
.tile-128 { background: #edcf72; color: #f9f6f2; font-size: 28px; }
.tile-256 { background: #edcc61; color: #f9f6f2; font-size: 28px; }
.tile-512 { background: #edc850; color: #f9f6f2; font-size: 28px; }
.tile-1024 { background: #edc53f; color: #f9f6f2; font-size: 24px; }
.tile-2048 { background: #edc22e; color: #f9f6f2; font-size: 24px; box-shadow: 0 0 20px rgba(237, 194, 46, 0.5); }
.tile-2 { background: #e8f5e8; color: #2e7d32; }
.tile-4 { background: #c8e6c9; color: #1b5e20; }
.tile-8 { background: #a5d6a7; color: #ffffff; }
.tile-16 { background: #81c784; color: #ffffff; }
.tile-32 { background: #66bb6a; color: #ffffff; }
.tile-64 { background: #4caf50; color: #ffffff; }
.tile-128 { background: #43a047; color: #ffffff; font-size: 28px; }
.tile-256 { background: #388e3c; color: #ffffff; font-size: 28px; }
.tile-512 { background: #2e7d32; color: #ffffff; font-size: 28px; }
.tile-1024 { background: #1b5e20; color: #ffffff; font-size: 24px; }
.tile-2048 { background: #0d4e14; color: #ffffff; font-size: 24px; box-shadow: 0 0 20px rgba(76, 175, 80, 0.6); }
.tile-super { background: #3c3a32; color: #f9f6f2; font-size: 20px; }
/* 动画效果 */
@@ -251,7 +226,6 @@ body {
gap: 15px;
}
.keep-playing-button,
.retry-button {
background: #8f7a66;
color: #f9f6f2;
@@ -263,237 +237,16 @@ body {
transition: all 0.3s ease;
}
.keep-playing-button:hover,
.retry-button:hover {
background: #9f8a76;
transform: translateY(-2px);
}
/* 游戏统计 */
.game-stats {
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.game-stats h3 {
text-align: center;
margin-bottom: 15px;
color: #776e65;
font-size: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: rgba(238, 228, 218, 0.3);
border-radius: 6px;
}
.stat-label {
font-size: 14px;
color: #776e65;
}
.stat-value {
font-weight: bold;
color: #f67c5f;
font-size: 16px;
}
/* 操作提示 */
.controls-hint {
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 12px;
text-align: center;
font-size: 14px;
line-height: 1.6;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.controls-hint p {
margin-bottom: 5px;
}
.controls-hint p:last-child {
margin-bottom: 0;
}
/* 模态框样式 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
border-radius: 15px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 15px 15px 0 0;
}
.modal-header h2 {
margin: 0;
font-size: 24px;
}
.close {
color: white;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.3s ease;
}
.close:hover {
color: #ddd;
}
.modal-body {
padding: 20px;
}
.final-score {
text-align: center;
margin-bottom: 25px;
padding: 20px;
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
border-radius: 10px;
color: white;
}
.final-score h3 {
margin: 0;
font-size: 28px;
text-shadow: 1px 1px 2px rgba(0,0,0,0.2);
}
.achievement-section h4 {
text-align: center;
margin-bottom: 20px;
color: #776e65;
font-size: 20px;
}
.achievement-grid {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
margin-bottom: 25px;
}
.achievement-item {
display: flex;
align-items: center;
padding: 15px;
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
}
.achievement-item:hover {
transform: translateY(-2px);
}
.achievement-icon {
font-size: 32px;
margin-right: 15px;
}
.achievement-info {
flex: 1;
}
.achievement-title {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.achievement-value {
font-size: 20px;
font-weight: bold;
color: #333;
}
.modal-actions {
display: flex;
gap: 15px;
justify-content: center;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 25px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background: linear-gradient(45deg, #4ecdc4, #44a08d);
color: white;
}
.btn-secondary {
background: linear-gradient(45deg, #ff9a9e, #fecfef);
color: white;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.2);
}
/* 手机端优化 */
@media (max-width: 480px) {
@@ -540,18 +293,7 @@ body {
font-size: 28px;
}
.modal-content {
margin: 10% auto;
width: 95%;
}
.achievement-grid {
grid-template-columns: 1fr;
}
.modal-actions {
flex-direction: column;
}
}
/* 超小屏幕优化 */
@@ -590,21 +332,12 @@ body {
font-size: 36px;
}
.achievement-grid {
grid-template-columns: 1fr 1fr;
}
.modal-actions {
flex-direction: row;
}
}
/* 触摸优化 */
@media (hover: none) and (pointer: coarse) {
.restart-button,
.keep-playing-button,
.retry-button,
.btn {
.retry-button {
min-height: 44px;
min-width: 44px;
}

View File

@@ -35,24 +35,32 @@ class GameControls {
switch(key) {
case 'ArrowLeft':
case 'a':
case 'A':
e.preventDefault();
this.game.moveLeft();
this.startKeyRepeat('ArrowLeft', () => this.game.moveLeft());
this.startKeyRepeat(key, () => this.game.moveLeft());
break;
case 'ArrowRight':
case 'd':
case 'D':
e.preventDefault();
this.game.moveRight();
this.startKeyRepeat('ArrowRight', () => this.game.moveRight());
this.startKeyRepeat(key, () => this.game.moveRight());
break;
case 'ArrowDown':
case 's':
case 'S':
e.preventDefault();
this.game.moveDown();
this.startKeyRepeat('ArrowDown', () => this.game.moveDown());
this.startKeyRepeat(key, () => this.game.moveDown());
break;
case 'ArrowUp':
case 'w':
case 'W':
e.preventDefault();
this.game.rotatePiece();
break;

View File

@@ -0,0 +1,20 @@
const playerdata = [
{
"名称":"树萌芽",
"账号":"3205788256@qq.com",
"分数":1232,
"时间":"2025-09-08"
},
{
"名称":"柚大青",
"账号":"2143323382@qq.com",
"分数":132,
"时间":"2025-09-21"
},
{
"名称":"牛马",
"账号":"2973419538@qq.com",
"分数":876,
"时间":"2025-09-25"
}
]

View File

@@ -40,30 +40,6 @@
</div>
<div class="game-sidebar">
<div class="next-piece">
<h3>下一个</h3>
<canvas id="nextCanvas" width="120" height="120"></canvas>
</div>
<div class="controls-info">
<h3>操作说明</h3>
<div class="control-item">
<span class="key">←→</span>
<span class="desc">移动</span>
</div>
<div class="control-item">
<span class="key"></span>
<span class="desc">快速下降</span>
</div>
<div class="control-item">
<span class="key"></span>
<span class="desc">旋转</span>
</div>
<div class="control-item">
<span class="key">空格</span>
<span class="desc">暂停/继续</span>
</div>
</div>
</div>
</div>

View File

@@ -8,8 +8,8 @@
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #1e3c72, #2a5298);
color: white;
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 30%, #f9fbe7 70%, #f0f4c3 100%);
color: #2e7d32;
height: 100vh;
overflow: hidden;
user-select: none;
@@ -23,6 +23,10 @@ body {
max-width: 800px;
margin: 0 auto;
padding: 10px;
background: linear-gradient(135deg, rgba(232, 245, 232, 0.4) 0%, rgba(241, 248, 233, 0.4) 50%, rgba(249, 251, 231, 0.4) 100%);
border-radius: 20px;
box-shadow: 0 10px 30px rgba(139, 195, 74, 0.2);
backdrop-filter: blur(10px);
}
/* 游戏头部 */
@@ -34,7 +38,12 @@ body {
.game-header h1 {
font-size: 2rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
background: linear-gradient(135deg, #4caf50 0%, #8bc34a 50%, #cddc39 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 2px 2px 4px rgba(255, 255, 255, 0.3);
font-weight: bold;
}
.score-board {
@@ -45,16 +54,24 @@ body {
}
.score-item {
background: rgba(255,255,255,0.1);
background: linear-gradient(135deg, #66bb6a 0%, #8bc34a 50%, #aed581 100%);
color: white;
padding: 8px 15px;
border-radius: 20px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.2);
border: 1px solid rgba(139, 195, 74, 0.3);
box-shadow: 0 4px 12px rgba(139, 195, 74, 0.3);
transition: all 0.3s ease;
}
.score-item:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(139, 195, 74, 0.4);
}
.score-item .label {
font-size: 0.9rem;
opacity: 0.8;
opacity: 0.9;
}
.score-item span:last-child {
@@ -73,15 +90,30 @@ body {
.game-board {
position: relative;
border-radius: 10px;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
box-shadow: 0 15px 35px rgba(139, 195, 74, 0.3);
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 50%, #f9fbe7 100%);
border: 3px solid transparent;
background-clip: padding-box;
}
.game-board::before {
content: '';
position: absolute;
top: -3px;
left: -3px;
right: -3px;
bottom: -3px;
background: linear-gradient(135deg, #66bb6a 0%, #8bc34a 50%, #aed581 100%);
border-radius: 15px;
z-index: -1;
}
#gameCanvas {
display: block;
background: #1a1a1a;
border: 2px solid #333;
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 50%, #4caf50 100%);
border-radius: 12px;
}
/* 游戏覆盖层 */
@@ -91,34 +123,44 @@ body {
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
background: rgba(139, 195, 74, 0.8);
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(5px);
backdrop-filter: blur(8px);
border-radius: 12px;
}
.overlay-content {
text-align: center;
background: rgba(255,255,255,0.1);
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 50%, #f9fbe7 100%);
color: #2e7d32;
padding: 30px;
border-radius: 15px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 20px;
border: 2px solid rgba(139, 195, 74, 0.4);
box-shadow: 0 15px 30px rgba(139, 195, 74, 0.3);
backdrop-filter: blur(10px);
}
.overlay-content h2 {
margin-bottom: 15px;
font-size: 1.8rem;
background: linear-gradient(135deg, #4caf50 0%, #8bc34a 50%, #cddc39 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: bold;
}
.overlay-content p {
margin-bottom: 20px;
opacity: 0.8;
color: #388e3c;
}
/* 游戏按钮 */
.game-btn {
background: linear-gradient(45deg, #667eea, #764ba2);
background: linear-gradient(45deg, #66bb6a 0%, #8bc34a 50%, #aed581 100%);
border: none;
color: white;
padding: 12px 24px;
@@ -127,12 +169,14 @@ body {
font-size: 1rem;
margin: 5px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
box-shadow: 0 4px 15px rgba(139, 195, 74, 0.3);
font-weight: 600;
}
.game-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
box-shadow: 0 6px 20px rgba(139, 195, 74, 0.4);
background: linear-gradient(45deg, #8bc34a 0%, #aed581 50%, #c5e1a5 100%);
}
.game-btn:active {
@@ -147,58 +191,7 @@ body {
min-width: 150px;
}
.next-piece {
background: rgba(255,255,255,0.1);
padding: 15px;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.2);
text-align: center;
}
.next-piece h3 {
margin-bottom: 10px;
font-size: 1.1rem;
}
#nextCanvas {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 5px;
}
.controls-info {
background: rgba(255,255,255,0.1);
padding: 15px;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.2);
}
.controls-info h3 {
margin-bottom: 15px;
font-size: 1.1rem;
text-align: center;
}
.control-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding: 5px 0;
}
.control-item .key {
background: rgba(255,255,255,0.2);
padding: 4px 8px;
border-radius: 4px;
font-family: monospace;
font-size: 0.9rem;
}
.control-item .desc {
font-size: 0.9rem;
opacity: 0.8;
}
/* 手机端控制 */
.mobile-controls {
@@ -234,15 +227,15 @@ body {
height: 55px;
border: none;
border-radius: 50%;
background: linear-gradient(45deg, #667eea, #764ba2);
background: linear-gradient(45deg, #66bb6a 0%, #8bc34a 50%, #aed581 100%);
color: white;
font-size: 1.4rem;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
box-shadow: 0 4px 15px rgba(139, 195, 74, 0.3);
user-select: none;
backdrop-filter: blur(10px);
border: 2px solid rgba(255,255,255,0.2);
border: 2px solid rgba(255, 255, 255, 0.2);
}
.control-btn:active {
@@ -257,7 +250,7 @@ body {
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
background: rgba(46, 125, 50, 0.9);
display: none;
align-items: center;
justify-content: center;
@@ -266,13 +259,15 @@ body {
}
.stats-content {
background: linear-gradient(135deg, #667eea, #764ba2);
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%);
color: #1b5e20;
padding: 30px;
border-radius: 20px;
text-align: center;
max-width: 90%;
width: 400px;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
box-shadow: 0 20px 40px rgba(46, 125, 50, 0.4);
border: 1px solid rgba(46, 125, 50, 0.3);
}
.stats-content h2 {
@@ -289,16 +284,18 @@ body {
}
.stat-item {
background: rgba(255,255,255,0.1);
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%);
color: white;
padding: 15px;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.2);
border: 1px solid rgba(46, 125, 50, 0.3);
box-shadow: 0 4px 8px rgba(46, 125, 50, 0.2);
}
.stat-label {
display: block;
font-size: 0.9rem;
opacity: 0.8;
opacity: 0.9;
margin-bottom: 5px;
}
@@ -309,7 +306,8 @@ body {
}
.achievement {
background: linear-gradient(45deg, #f093fb, #f5576c);
background: linear-gradient(45deg, #81c784, #66bb6a);
color: white;
padding: 15px;
border-radius: 10px;
margin-bottom: 25px;
@@ -318,6 +316,7 @@ body {
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(46, 125, 50, 0.3);
}
/* 响应式设计 */
@@ -359,11 +358,7 @@ body {
width: 100%;
}
.next-piece,
.controls-info {
flex: 1;
max-width: 200px;
}
.mobile-controls {
display: block;
@@ -428,10 +423,7 @@ body {
gap: 10px;
}
.next-piece,
.controls-info {
max-width: unset;
}
}
/* 隐藏类 */

View File

@@ -3,8 +3,6 @@ class TetrisGame {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.nextCanvas = document.getElementById('nextCanvas');
this.nextCtx = this.nextCanvas.getContext('2d');
// 游戏配置
this.BOARD_WIDTH = 10;
@@ -140,7 +138,6 @@ class TetrisGame {
return false;
}
this.drawNextPiece();
return true;
}
@@ -348,39 +345,7 @@ class TetrisGame {
}
}
drawNextPiece() {
this.nextCtx.clearRect(0, 0, this.nextCanvas.width, this.nextCanvas.height);
if (this.nextPiece) {
const size = 20;
const matrix = this.nextPiece.matrix;
const offsetX = (this.nextCanvas.width - matrix[0].length * size) / 2;
const offsetY = (this.nextCanvas.height - matrix.length * size) / 2;
this.nextCtx.fillStyle = this.nextPiece.color;
for (let row = 0; row < matrix.length; row++) {
for (let col = 0; col < matrix[row].length; col++) {
if (matrix[row][col] !== 0) {
this.nextCtx.fillRect(
offsetX + col * size,
offsetY + row * size,
size,
size
);
this.nextCtx.strokeStyle = '#333';
this.nextCtx.lineWidth = 1;
this.nextCtx.strokeRect(
offsetX + col * size,
offsetY + row * size,
size,
size
);
}
}
}
}
}
updateDisplay() {
document.getElementById('score').textContent = this.score;

View File

@@ -0,0 +1,20 @@
const playerdata = [
{
"名称":"树萌芽",
"账号":"3205788256@qq.com",
"分数":1232,
"时间":"2025-09-08"
},
{
"名称":"柚大青",
"账号":"2143323382@qq.com",
"分数":132,
"时间":"2025-09-21"
},
{
"名称":"牛马",
"账号":"2973419538@qq.com",
"分数":876,
"时间":"2025-09-25"
}
]

View File

@@ -11,7 +11,7 @@
}
body {
background: #f5f5f5;
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc8 50%, #f1f8e9 100%);
font-family: 'Arial', 'Microsoft YaHei', sans-serif;
display: flex;
justify-content: center;
@@ -25,41 +25,45 @@
display: flex;
flex-direction: column;
align-items: center;
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%);
border-radius: 15px;
box-shadow: 0 10px 30px rgba(46,125,50,0.3);
padding: 20px;
position: relative;
border: 1px solid rgba(129,199,132,0.2);
}
.game-title {
font-size: 24px;
font-weight: bold;
color: #333;
color: #1b5e20;
margin-bottom: 10px;
text-align: center;
text-shadow: 1px 1px 2px rgba(255,255,255,0.5);
}
.score-display {
position: relative;
width: 300px;
height: 60px;
background: #333;
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%);
border-radius: 8px 8px 0 0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(46,125,50,0.3);
}
.game-container {
position: relative;
width: 300px;
height: 600px;
border: 3px solid #333;
border: 3px solid #2e7d32;
border-top: none;
border-radius: 0 0 8px 8px;
overflow: hidden;
background: white;
box-shadow: 0 4px 12px rgba(46,125,50,0.2);
}
.control-panel {
@@ -82,28 +86,32 @@
}
.start-btn {
background: #4CAF50;
background: linear-gradient(45deg, #66bb6a, #4caf50);
color: white;
box-shadow: 0 4px 12px rgba(76,175,80,0.3);
}
.start-btn:hover {
background: #45a049;
background: linear-gradient(45deg, #4caf50, #388e3c);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(76,175,80,0.4);
}
.pause-btn {
background: #ff9800;
background: linear-gradient(45deg, #81c784, #66bb6a);
color: white;
box-shadow: 0 4px 12px rgba(129,199,132,0.3);
}
.pause-btn:hover {
background: #e68900;
background: linear-gradient(45deg, #66bb6a, #4caf50);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(129,199,132,0.4);
}
.instructions {
text-align: center;
color: #666;
color: #2e7d32;
font-size: 14px;
margin-top: 10px;
line-height: 1.4;
@@ -124,30 +132,32 @@
}
.modal-content {
background: white;
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%);
padding: 30px;
border-radius: 15px;
text-align: center;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
box-shadow: 0 20px 40px rgba(46,125,50,0.3);
max-width: 300px;
width: 90%;
border: 1px solid rgba(129,199,132,0.3);
}
.modal-title {
font-size: 24px;
font-weight: bold;
color: #e74c3c;
color: #c62828;
margin-bottom: 15px;
text-shadow: 1px 1px 2px rgba(255,255,255,0.5);
}
.final-score, .final-speed {
font-size: 18px;
margin: 15px 0;
color: #333;
color: #1b5e20;
}
.final-speed {
color: #666;
color: #2e7d32;
font-size: 16px;
}
@@ -163,12 +173,14 @@
}
.restart-btn {
background: #4CAF50;
background: linear-gradient(45deg, #66bb6a, #4caf50);
color: white;
box-shadow: 0 4px 12px rgba(76,175,80,0.3);
}
.restart-btn:hover {
background: #45a049;
background: linear-gradient(45deg, #4caf50, #388e3c);
box-shadow: 0 6px 16px rgba(76,175,80,0.4);
}
/* 移动端适配 */

View File

@@ -1,62 +1,62 @@
/* 经典扫雷 - 手机竖屏优先 + 电脑端适配 */
:root{
--bg:#0f172a;
--panel:#111827;
--accent:#22d3ee;
--accent-2:#60a5fa;
--text:#e5e7eb;
--muted:#94a3b8;
--danger:#ef4444;
--bg:#e8f5e8;
--panel:#c8e6c9;
--accent:#2e7d32;
--accent-2:#388e3c;
--text:#1b5e20;
--muted:#4caf50;
--danger:#d32f2f;
--success:#22c55e;
--warn:#f59e0b;
--cell:#1f2937;
--cell-hover:#273244;
--flag:#fb7185;
--warn:#f57c00;
--cell:#f1f8e9;
--cell-hover:#dcedc8;
--flag:#4caf50;
}
*{box-sizing:border-box}
html,body{height:100%;}
body{margin:0;background:linear-gradient(180deg,#0b1220,#0f172a);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif;-webkit-tap-highlight-color:transparent}
body{margin:0;background:linear-gradient(135deg, #a8e6cf 0%, #dcedc8 50%, #f1f8e9 100%);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif;-webkit-tap-highlight-color:transparent}
.app{min-height:100dvh;display:flex;flex-direction:column;gap:12px;padding:12px;}
.header{display:flex;flex-direction:column;gap:10px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.06);border-radius:14px;padding:12px 12px 10px;backdrop-filter:blur(6px)}
.title{margin:0;font-size:20px;letter-spacing:1px}
.header{display:flex;flex-direction:column;gap:10px;background:linear-gradient(135deg, #c8e6c9 0%, #e8f5e8 100%);border:1px solid rgba(46, 125, 50, 0.2);border-radius:14px;padding:12px 12px 10px;backdrop-filter:blur(6px);box-shadow:0 4px 8px rgba(46, 125, 50, 0.1)}
.title{margin:0;font-size:20px;letter-spacing:1px;color:#1b5e20;text-shadow:1px 1px 2px rgba(255,255,255,0.5)}
.hud{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;align-items:center}
.hud-item{display:flex;flex-direction:column;align-items:center;justify-content:center;background:var(--panel);border:1px solid rgba(255,255,255,0.06);border-radius:10px;padding:8px 6px}
.hud-item .label{font-size:12px;color:var(--muted)}
.hud-item .value{font-size:18px;font-weight:700;color:#fff}
.btn{appearance:none;border:none;background:#1e293b;color:#fff;padding:10px 12px;border-radius:10px;cursor:pointer;outline:none;transition:.15s transform,.15s background;display:inline-flex;align-items:center;justify-content:center}
.hud-item{display:flex;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(135deg, #a5d6a7 0%, #c8e6c9 100%);border:1px solid rgba(46, 125, 50, 0.2);border-radius:10px;padding:8px 6px;box-shadow:0 2px 4px rgba(46, 125, 50, 0.1)}
.hud-item .label{font-size:12px;color:#2e7d32}
.hud-item .value{font-size:18px;font-weight:700;color:#1b5e20}
.btn{appearance:none;border:none;background:linear-gradient(135deg, #81c784 0%, #a5d6a7 100%);color:#fff;padding:10px 12px;border-radius:10px;cursor:pointer;outline:none;transition:.15s transform,.15s background;display:inline-flex;align-items:center;justify-content:center;box-shadow:0 2px 4px rgba(46, 125, 50, 0.2)}
.btn:active{transform:scale(.98)}
.btn.primary{background:linear-gradient(90deg,var(--accent),var(--accent-2))}
.btn.primary{background:linear-gradient(135deg,var(--accent),var(--accent-2));color:#fff}
.btn.primary:active{filter:brightness(.95)}
.main{display:flex;flex-direction:column;gap:12px}
.board-wrapper{display:flex;justify-content:center;align-items:center}
.board{display:grid;gap:4px;touch-action:manipulation;user-select:none;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:6px;width:100%;max-width:92vw}
.cell{display:grid;place-items:center;background:var(--cell);border-radius:8px;border:1px solid rgba(255,255,255,0.06);font-weight:700;color:#9ca3af;box-shadow:inset 0 -1px 0 rgba(255,255,255,0.04);aspect-ratio:1/1;font-size:clamp(12px, 2.2vw, 18px)}
.cell.revealed{background:#0b1220;color:#e5e7eb}
.board{display:grid;gap:4px;touch-action:manipulation;user-select:none;background:linear-gradient(135deg, #c8e6c9 0%, #e8f5e8 100%);border:1px solid rgba(46, 125, 50, 0.2);border-radius:12px;padding:6px;width:100%;max-width:92vw;box-shadow:0 4px 8px rgba(46, 125, 50, 0.1)}
.cell{display:grid;place-items:center;background:var(--cell);border-radius:8px;border:1px solid rgba(46, 125, 50, 0.15);font-weight:700;color:#4caf50;box-shadow:inset 0 -1px 0 rgba(255,255,255,0.3);aspect-ratio:1/1;font-size:clamp(12px, 2.2vw, 18px)}
.cell.revealed{background:#e8f5e8;color:#2e7d32}
.cell:hover{background:var(--cell-hover)}
.cell.flag::after{content:"🚩"}
.cell.mine.revealed{background:#3b0d0d;color:#fff}
.cell.mine.revealed{background:#ffcdd2;color:#d32f2f}
.cell.mine.revealed::after{content:"💣"}
.cell[data-n="1"].revealed{color:#60a5fa}
.cell[data-n="2"].revealed{color:#34d399}
.cell[data-n="3"].revealed{color:#f87171}
.cell[data-n="4"].revealed{color:#a78bfa}
.cell[data-n="5"].revealed{color:#fbbf24}
.cell[data-n="6"].revealed{color:#22d3ee}
.cell[data-n="7"].revealed{color:#e879f9}
.cell[data-n="8"].revealed{color:#cbd5e1}
.cell[data-n="1"].revealed{color:#1976d2}
.cell[data-n="2"].revealed{color:#388e3c}
.cell[data-n="3"].revealed{color:#d32f2f}
.cell[data-n="4"].revealed{color:#7b1fa2}
.cell[data-n="5"].revealed{color:#f57c00}
.cell[data-n="6"].revealed{color:#00796b}
.cell[data-n="7"].revealed{color:#c2185b}
.cell[data-n="8"].revealed{color:#455a64}
.tips{font-size:12px;color:var(--muted);text-align:center}
.toast{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);background:rgba(17,24,39,.95);border:1px solid rgba(255,255,255,.08);padding:10px 14px;border-radius:10px}
.toast{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);background:linear-gradient(135deg, #c8e6c9 0%, #e8f5e8 100%);border:1px solid rgba(46, 125, 50, 0.2);padding:10px 14px;border-radius:10px;color:#1b5e20;box-shadow:0 4px 8px rgba(46, 125, 50, 0.2)}
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);display:grid;place-items:center;padding:14px}
.modal{width:min(520px,92vw);background:linear-gradient(180deg,#0f172a,#0b1320);border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:16px 14px}
.modal h2{margin:4px 0 8px;font-size:20px}
.modal-overlay{position:fixed;inset:0;background:rgba(46, 125, 50, 0.3);display:grid;place-items:center;padding:14px}
.modal{width:min(520px,92vw);background:linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%);border:1px solid rgba(46, 125, 50, 0.2);border-radius:14px;padding:16px 14px;box-shadow:0 8px 16px rgba(46, 125, 50, 0.2)}
.modal h2{margin:4px 0 8px;font-size:20px;color:#1b5e20}
.stats{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:8px 0 14px}
.stats .card{background:var(--panel);border:1px solid rgba(255,255,255,0.06);border-radius:10px;padding:10px}
.stats .card .k{font-size:12px;color:var(--muted)}
.stats .card .v{font-size:18px;font-weight:700}
.stats .card{background:linear-gradient(135deg, #c8e6c9 0%, #e8f5e8 100%);border:1px solid rgba(46, 125, 50, 0.2);border-radius:10px;padding:10px;box-shadow:0 2px 4px rgba(46, 125, 50, 0.1)}
.stats .card .k{font-size:12px;color:#2e7d32}
.stats .card .v{font-size:18px;font-weight:700;color:#1b5e20}
.modal-actions{display:flex;gap:10px;justify-content:flex-end}
/* 响应式:手机竖屏优先 */

View File

@@ -0,0 +1,20 @@
const playerdata = [
{
"名称":"树萌芽",
"账号":"3205788256@qq.com",
"分数":1232,
"时间":"2025-09-08"
},
{
"名称":"柚大青",
"账号":"2143323382@qq.com",
"分数":132,
"时间":"2025-09-21"
},
{
"名称":"牛马",
"账号":"2973419538@qq.com",
"分数":876,
"时间":"2025-09-25"
}
]

View File

@@ -6,28 +6,6 @@ class GameControls {
}
initControls() {
// 方向按钮控制
document.getElementById('upBtn').addEventListener('click', () => {
this.game.changeDirection(0, -1);
});
document.getElementById('downBtn').addEventListener('click', () => {
this.game.changeDirection(0, 1);
});
document.getElementById('leftBtn').addEventListener('click', () => {
this.game.changeDirection(-1, 0);
});
document.getElementById('rightBtn').addEventListener('click', () => {
this.game.changeDirection(1, 0);
});
// 暂停/继续按钮
document.getElementById('pauseBtn').addEventListener('click', () => {
this.game.togglePause();
});
// 重新开始按钮
document.getElementById('restartBtn').addEventListener('click', () => {
this.game.restart();
@@ -46,62 +24,82 @@ class GameControls {
this.game.restart();
}
break;
case 'p':
case 'P':
this.game.togglePause();
break;
case 'Escape':
if (this.game.gameOver) {
document.getElementById('gameOverModal').style.display = 'none';
}
break;
}
});
}
initTouchControls() {
const canvas = document.getElementById('gameCanvas');
let touchStartX = 0;
let touchStartY = 0;
let isDragging = false;
let lastTouchX = 0;
let lastTouchY = 0;
let lastDirectionChange = 0;
const directionChangeDelay = 200; // 防止方向变化过快
// 触摸开始
canvas.addEventListener('touchstart', (e) => {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
isDragging = true;
lastTouchX = e.touches[0].clientX;
lastTouchY = e.touches[0].clientY;
e.preventDefault();
}, { passive: false });
// 拖动过程中实时检测方向
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
}, { passive: false });
canvas.addEventListener('touchend', (e) => {
const touchEndX = e.changedTouches[0].clientX;
const touchEndY = e.changedTouches[0].clientY;
if (!isDragging) return;
const deltaX = touchEndX - touchStartX;
const deltaY = touchEndY - touchStartY;
const minSwipeDistance = 30;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 水平滑动
if (Math.abs(deltaX) > minSwipeDistance) {
if (deltaX > 0) {
this.game.changeDirection(1, 0); // 右滑
} else {
this.game.changeDirection(-1, 0); // 左滑
}
}
} else {
// 垂直滑动
if (Math.abs(deltaY) > minSwipeDistance) {
if (deltaY > 0) {
this.game.changeDirection(0, 1); // 下滑
} else {
this.game.changeDirection(0, -1); // 上滑
}
}
const currentTime = Date.now();
if (currentTime - lastDirectionChange < directionChangeDelay) {
e.preventDefault();
return;
}
const currentTouchX = e.touches[0].clientX;
const currentTouchY = e.touches[0].clientY;
const deltaX = currentTouchX - lastTouchX;
const deltaY = currentTouchY - lastTouchY;
const minDragDistance = 20; // 最小拖动距离
// 检查是否达到最小拖动距离
if (Math.abs(deltaX) > minDragDistance || Math.abs(deltaY) > minDragDistance) {
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 水平拖动
if (deltaX > 0) {
this.game.changeDirection(1, 0); // 向右拖动
} else {
this.game.changeDirection(-1, 0); // 向左拖动
}
} else {
// 垂直拖动
if (deltaY > 0) {
this.game.changeDirection(0, 1); // 向下拖动
} else {
this.game.changeDirection(0, -1); // 向上拖动
}
}
// 更新最后触摸位置和方向变化时间
lastTouchX = currentTouchX;
lastTouchY = currentTouchY;
lastDirectionChange = currentTime;
// 添加触觉反馈
this.vibrate(30);
}
e.preventDefault();
}, { passive: false });
// 触摸结束
canvas.addEventListener('touchend', (e) => {
isDragging = false;
e.preventDefault();
}, { passive: false });
// 触摸取消
canvas.addEventListener('touchcancel', (e) => {
isDragging = false;
e.preventDefault();
}, { passive: false });
@@ -111,8 +109,71 @@ class GameControls {
e.preventDefault();
}
}, { passive: false });
// 添加鼠标拖动支持(用于桌面测试)
this.initMouseDragControls(canvas);
}
// 鼠标拖动控制(用于桌面测试)
initMouseDragControls(canvas) {
let isDragging = false;
let lastMouseX = 0;
let lastMouseY = 0;
let lastDirectionChange = 0;
const directionChangeDelay = 200;
canvas.addEventListener('mousedown', (e) => {
isDragging = true;
lastMouseX = e.clientX;
lastMouseY = e.clientY;
e.preventDefault();
});
canvas.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const currentTime = Date.now();
if (currentTime - lastDirectionChange < directionChangeDelay) {
return;
}
const currentMouseX = e.clientX;
const currentMouseY = e.clientY;
const deltaX = currentMouseX - lastMouseX;
const deltaY = currentMouseY - lastMouseY;
const minDragDistance = 20;
if (Math.abs(deltaX) > minDragDistance || Math.abs(deltaY) > minDragDistance) {
if (Math.abs(deltaX) > Math.abs(deltaY)) {
if (deltaX > 0) {
this.game.changeDirection(1, 0); // 向右拖动
} else {
this.game.changeDirection(-1, 0); // 向左拖动
}
} else {
if (deltaY > 0) {
this.game.changeDirection(0, 1); // 向下拖动
} else {
this.game.changeDirection(0, -1); // 向上拖动
}
}
lastMouseX = currentMouseX;
lastMouseY = currentMouseY;
lastDirectionChange = currentTime;
}
});
canvas.addEventListener('mouseup', () => {
isDragging = false;
});
canvas.addEventListener('mouseleave', () => {
isDragging = false;
});
}
// 震动反馈(移动端)
vibrate(duration = 50) {
if ('vibrate' in navigator) {

View File

@@ -23,8 +23,7 @@ class SnakeGame {
this.dy = 0;
this.score = 0;
this.level = 1;
this.gameSpeed = 10; // 初始速度
this.isPaused = false;
this.gameSpeed = 6.5; // 初始速度 (10 * 0.65)
this.gameOver = false;
this.startTime = Date.now();
this.foodEaten = 0;
@@ -43,7 +42,7 @@ class SnakeGame {
// 监听键盘事件
document.addEventListener('keydown', (e) => {
if (this.isPaused || this.gameOver) return;
if (this.gameOver) return;
switch(e.key) {
case 'ArrowUp':
@@ -201,7 +200,7 @@ class SnakeGame {
const newLevel = Math.floor(this.foodEaten / 5) + 1;
if (newLevel > this.level) {
this.level = newLevel;
this.gameSpeed = Math.min(20, 10 + this.level); // 速度上限20
this.gameSpeed = Math.min(13, 6.5 + this.level * 0.65); // 速度上限13 (20 * 0.65)
}
}
@@ -297,17 +296,10 @@ class SnakeGame {
}
}
togglePause() {
this.isPaused = !this.isPaused;
document.getElementById('pauseBtn').textContent = this.isPaused ? '继续' : '暂停';
if (!this.isPaused && !this.gameOver) {
this.gameLoop();
}
}
changeDirection(dx, dy) {
if (this.isPaused || this.gameOver) return;
if (this.gameOver) return;
// 防止180度转弯
if ((this.dx !== 0 && dx !== 0) || (this.dy !== 0 && dy !== 0)) {
@@ -319,18 +311,13 @@ class SnakeGame {
}
showGameOver() {
const modal = document.getElementById('gameOverModal');
// 游戏结束时只记录最终状态,不显示弹窗
const gameTime = Math.floor((Date.now() - this.startTime) / 1000);
document.getElementById('finalScore').textContent = this.score;
document.getElementById('finalLength').textContent = this.snake.length;
document.getElementById('finalLevel').textContent = this.level;
document.getElementById('gameTime').textContent = gameTime;
document.getElementById('foodEaten').textContent = this.foodEaten;
modal.style.display = 'flex';
console.log(`游戏结束! 分数: ${this.score}, 长度: ${this.snake.length}, 等级: ${this.level}, 时间: ${gameTime}`);
}
restart() {
this.snake = [
{x: 10, y: 10},
@@ -341,8 +328,7 @@ class SnakeGame {
this.dy = 0;
this.score = 0;
this.level = 1;
this.gameSpeed = 10;
this.isPaused = false;
this.gameSpeed = 6.5;
this.gameOver = false;
this.startTime = Date.now();
this.foodEaten = 0;
@@ -351,9 +337,6 @@ class SnakeGame {
this.generateFood();
this.updateUI();
document.getElementById('gameOverModal').style.display = 'none';
document.getElementById('pauseBtn').textContent = '暂停';
this.gameLoop();
}
}

View File

@@ -0,0 +1,62 @@
const playerdata = [
{
"名称":"树萌芽",
"账号":"3205788256@qq.com",
"分数":1568,
"时间":"2025-09-08"
},
{
"名称":"柚大青",
"账号":"2143323382@qq.com",
"分数":245,
"时间":"2025-09-21"
},
{
"名称":"牛马",
"账号":"2973419538@qq.com",
"分数":1123,
"时间":"2025-09-25"
},
{
"名称":"风行者",
"账号":"4456723190@qq.com",
"分数":1987,
"时间":"2025-09-30"
},
{
"名称":"月光骑士",
"账号":"5832197462@qq.com",
"分数":876,
"时间":"2025-10-02"
},
{
"名称":"星河",
"账号":"6724981532@qq.com",
"分数":1345,
"时间":"2025-10-05"
},
{
"名称":"雷霆",
"账号":"7891234567@qq.com",
"分数":2105,
"时间":"2025-10-08"
},
{
"名称":"火焰猫",
"账号":"8912345678@qq.com",
"分数":654,
"时间":"2025-10-10"
},
{
"名称":"冰雪女王",
"账号":"9123456789@qq.com",
"分数":1789,
"时间":"2025-10-12"
},
{
"名称":"😊",
"账号":"1125234890@qq.com",
"分数":1432,
"时间":"2025-10-15"
}
]

View File

@@ -23,37 +23,18 @@
<div class="game-controls">
<div class="control-row">
<button id="upBtn" class="control-btn"></button>
</div>
<div class="control-row">
<button id="leftBtn" class="control-btn"></button>
<button id="pauseBtn" class="control-btn">暂停</button>
<button id="rightBtn" class="control-btn"></button>
</div>
<div class="control-row">
<button id="downBtn" class="control-btn"></button>
<button id="restartBtn" class="control-btn">重新开始</button>
</div>
</div>
<div class="game-instructions">
<p>使用方向键或触摸按钮控制蛇的方向</p>
<p>使用方向键或拖动手势控制蛇的方向</p>
</div>
</div>
<div id="gameOverModal" class="modal">
<div class="modal-content">
<h2>游戏结束!</h2>
<div class="stats">
<p>最终分数: <span id="finalScore">0</span></p>
<p>蛇的长度: <span id="finalLength">0</span></p>
<p>达到等级: <span id="finalLevel">0</span></p>
<p>游戏时间: <span id="gameTime">0</span></p>
<p>吃掉食物: <span id="foodEaten">0</span></p>
</div>
<button id="restartBtn" class="restart-btn">重新开始</button>
</div>
</div>
<script src="gamedata.js"></script>
<script src="game-core.js"></script>
<script src="game-controls.js"></script>
<script src="game-stats.js"></script>

View File

@@ -6,7 +6,7 @@
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc8 50%, #f1f8e9 100%);
min-height: 100vh;
display: flex;
justify-content: center;
@@ -16,13 +16,14 @@ body {
}
.game-container {
background: rgba(255, 255, 255, 0.95);
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%);
border-radius: 20px;
padding: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
box-shadow: 0 20px 40px rgba(46, 125, 50, 0.3);
max-width: 400px;
width: 95%;
margin: 20px auto;
border: 1px solid rgba(46, 125, 50, 0.2);
}
.game-header {
@@ -31,21 +32,22 @@ body {
}
.game-header h1 {
color: #333;
color: #1b5e20;
font-size: 2.5rem;
margin-bottom: 15px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
text-shadow: 2px 2px 4px rgba(255, 255, 255, 0.5);
}
.score-board {
display: flex;
justify-content: space-around;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%);
padding: 12px;
border-radius: 15px;
color: white;
font-weight: bold;
font-size: 1.1rem;
box-shadow: 0 4px 8px rgba(46, 125, 50, 0.2);
}
.score-board span {
@@ -61,10 +63,10 @@ body {
}
#gameCanvas {
border: 3px solid #333;
border: 3px solid #2e7d32;
border-radius: 10px;
background: #222;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
background: #1b5e20;
box-shadow: 0 8px 16px rgba(46, 125, 50, 0.3);
}
.game-controls {
@@ -83,33 +85,32 @@ body {
margin: 0 10px;
border: none;
border-radius: 50%;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
color: white;
font-size: 1.5rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px rgba(46, 125, 50, 0.3);
}
.control-btn:hover {
transform: scale(1.1);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
box-shadow: 0 6px 12px rgba(46, 125, 50, 0.4);
}
.control-btn:active {
transform: scale(0.95);
box-shadow: 0 2px 4px rgba(46, 125, 50, 0.2);
}
#pauseBtn {
background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%);
color: #333;
background: linear-gradient(135deg, #81c784 0%, #a5d6a7 100%);
color: #1b5e20;
font-size: 1.2rem;
}
.game-instructions {
text-align: center;
color: #666;
color: #2e7d32;
font-size: 0.9rem;
margin-top: 15px;
}
@@ -121,41 +122,43 @@ body {
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
background: rgba(46, 125, 50, 0.6);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%);
padding: 30px;
border-radius: 20px;
text-align: center;
max-width: 400px;
width: 90%;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
box-shadow: 0 20px 40px rgba(46, 125, 50, 0.4);
border: 1px solid rgba(46, 125, 50, 0.2);
}
.modal-content h2 {
color: #e74c3c;
color: #1b5e20;
margin-bottom: 20px;
font-size: 2rem;
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.5);
}
.stats p {
margin: 10px 0;
font-size: 1.1rem;
color: #333;
color: #2e7d32;
}
.stats span {
font-weight: bold;
color: #e74c3c;
color: #1b5e20;
}
.restart-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%);
color: white;
border: none;
padding: 15px 30px;
@@ -165,11 +168,70 @@ body {
cursor: pointer;
margin-top: 20px;
transition: all 0.3s ease;
box-shadow: 0 4px 8px rgba(46, 125, 50, 0.2);
}
.restart-btn:hover {
transform: scale(1.05);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
box-shadow: 0 8px 16px rgba(46, 125, 50, 0.3);
}
/* 排行榜样式 */
.leaderboard {
margin: 20px 0;
padding: 15px;
background: rgba(255, 255, 255, 0.3);
border-radius: 15px;
border: 1px solid rgba(46, 125, 50, 0.2);
}
.leaderboard h3 {
color: #1b5e20;
margin-bottom: 15px;
font-size: 1.3rem;
text-align: center;
}
.leaderboard-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.leaderboard-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.5);
border-radius: 10px;
font-size: 0.9rem;
transition: all 0.2s ease;
}
.leaderboard-item.current-player {
background: linear-gradient(135deg, #ffeb3b 0%, #fff176 100%);
font-weight: bold;
border: 2px solid #f57f17;
}
.leaderboard-item .rank {
font-weight: bold;
min-width: 30px;
text-align: left;
}
.leaderboard-item .player-name {
flex: 1;
text-align: left;
margin-left: 10px;
color: #2e7d32;
}
.leaderboard-item .player-score {
font-weight: bold;
color: #1b5e20;
min-width: 50px;
text-align: right;
}
/* 手机端优化 */
@@ -246,13 +308,4 @@ body {
-ms-user-select: none;
}
/* 动画效果 */
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.score-board {
animation: pulse 2s infinite;
}
/* 动画效果已删除 */

View File

@@ -10,11 +10,14 @@ import Api60sPage from './pages/Api60sPage';
import SmallGamePage from './pages/SmallGamePage';
import AiModelPage from './pages/AiModelPage';
import UserProfilePage from './pages/UserProfilePage';
import AboutPage from './pages/AboutPage';
// 公共组件
import Header from './components/Header';
import Navigation from './components/Navigation';
import Footer from './components/Footer';
import ParticleEffect from './components/ParticleEffect';
import ScrollToTop from './components/ScrollToTop';
// 上下文
import { UserProvider } from './contexts/UserContext';
@@ -39,6 +42,7 @@ function App() {
return (
<UserProvider>
<Router>
<ScrollToTop />
<AppContainer>
<Header />
<MainContent>
@@ -49,6 +53,7 @@ function App() {
<Route path="/60sapi" element={<Api60sPage />} />
<Route path="/smallgame" element={<SmallGamePage />} />
<Route path="/aimodel" element={<AiModelPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/profile" element={<UserProfilePage />} />
{/* 通配符路由 - 所有未匹配的路径都重定向到首页 */}
<Route path="*" element={<Navigate to="/" replace />} />
@@ -83,6 +88,9 @@ function App() {
}
}}
/>
{/* 全局粒子效果 */}
<ParticleEffect />
</AppContainer>
</Router>
</UserProvider>

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import styled from 'styled-components';
import { FiUser, FiMenu, FiX, FiLogOut } from 'react-icons/fi';
import { FiUser, FiMenu, FiX, FiLogOut, FiInfo } from 'react-icons/fi';
import { useUser } from '../contexts/UserContext';
const HeaderContainer = styled.header`
@@ -59,16 +59,48 @@ const Nav = styled.nav`
}
`;
const NavLink = styled(Link)`
color: rgba(255, 255, 255, 0.9);
const NavLink = styled(Link).withConfig({
shouldForwardProp: (prop) => prop !== 'isActive'
})`
color: ${props => props.isActive ? 'white' : 'rgba(255, 255, 255, 0.9)'};
text-decoration: none;
padding: 8px 16px;
border-radius: 6px;
transition: all 0.2s ease;
padding: 10px 18px;
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
font-weight: ${props => props.isActive ? '600' : '500'};
background: ${props => props.isActive ? 'rgba(255, 255, 255, 0.2)' : 'transparent'};
box-shadow: ${props => props.isActive ? '0 4px 12px rgba(0, 0, 0, 0.15)' : 'none'};
transform: ${props => props.isActive ? 'translateY(-1px)' : 'translateY(0)'};
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.1) 100%);
border-radius: 12px;
opacity: 0;
transition: opacity 0.3s ease;
z-index: -1;
}
&:hover {
background: rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.15);
color: white;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
&::before {
opacity: 1;
}
}
&:active {
transform: translateY(0);
transition: transform 0.1s ease;
}
`;
@@ -194,20 +226,67 @@ const CloseButton = styled.button`
cursor: pointer;
`;
const MobileNavLink = styled(Link)`
const MobileNavLink = styled(Link).withConfig({
shouldForwardProp: (prop) => prop !== 'isActive'
})`
display: block;
color: #374151;
color: ${props => props.isActive ? '#4ade80' : '#374151'};
text-decoration: none;
padding: 12px 0;
border-bottom: 1px solid #f3f4f6;
transition: color 0.2s ease;
padding: 16px 20px;
margin: 4px 0;
border-radius: 12px;
border-bottom: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
font-weight: ${props => props.isActive ? '600' : '500'};
background: ${props => props.isActive ? 'rgba(74, 222, 128, 0.1)' : 'transparent'};
transform: ${props => props.isActive ? 'translateX(8px)' : 'translateX(0)'};
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: ${props => props.isActive ? '4px' : '0'};
height: 60%;
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
border-radius: 0 2px 2px 0;
transition: width 0.3s ease;
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(74, 222, 128, 0.05) 0%, rgba(34, 197, 94, 0.05) 100%);
border-radius: 12px;
opacity: 0;
transition: opacity 0.3s ease;
z-index: -1;
}
&:hover {
color: #4ade80;
background: rgba(74, 222, 128, 0.08);
transform: translateX(12px);
box-shadow: 0 4px 12px rgba(74, 222, 128, 0.15);
&::after {
opacity: 1;
}
&::before {
width: 4px;
}
}
&:last-child {
border-bottom: none;
&:active {
transform: translateX(6px);
transition: transform 0.1s ease;
}
`;
@@ -215,6 +294,11 @@ const Header = () => {
const { user, isLoggedIn, logout, getQQAvatar } = useUser();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const isActive = (path) => {
return location.pathname.startsWith(path);
};
const handleLogout = async () => {
await logout();
@@ -240,10 +324,10 @@ const Header = () => {
</Logo>
<Nav>
<NavLink to="/60sapi">聚合应用</NavLink>
<NavLink to="/smallgame">休闲游戏</NavLink>
<NavLink to="/aimodel">AI工具</NavLink>
<NavLink to="/profile">个人中心</NavLink>
<NavLink to="/60sapi" isActive={isActive('/60sapi')}>聚合应用</NavLink>
<NavLink to="/smallgame" isActive={isActive('/smallgame')}>休闲游戏</NavLink>
<NavLink to="/aimodel" isActive={isActive('/aimodel')}>AI工具</NavLink>
<NavLink to="/profile" isActive={isActive('/profile')}>个人中心</NavLink>
</Nav>
<UserSection>
@@ -291,19 +375,22 @@ const Header = () => {
</CloseButton>
</MobileMenuHeader>
<MobileNavLink to="/" onClick={handleMenuClose}>
<MobileNavLink to="/" onClick={handleMenuClose} isActive={location.pathname === '/'}>
首页
</MobileNavLink>
<MobileNavLink to="/60sapi" onClick={handleMenuClose}>
<MobileNavLink to="/60sapi" onClick={handleMenuClose} isActive={isActive('/60sapi')}>
聚合应用
</MobileNavLink>
<MobileNavLink to="/smallgame" onClick={handleMenuClose}>
<MobileNavLink to="/smallgame" onClick={handleMenuClose} isActive={isActive('/smallgame')}>
休闲游戏
</MobileNavLink>
<MobileNavLink to="/aimodel" onClick={handleMenuClose}>
<MobileNavLink to="/aimodel" onClick={handleMenuClose} isActive={isActive('/aimodel')}>
AI工具
</MobileNavLink>
<MobileNavLink to="/profile" onClick={handleMenuClose}>
<MobileNavLink to="/about" onClick={handleMenuClose} isActive={isActive('/about')}>
关于
</MobileNavLink>
<MobileNavLink to="/profile" onClick={handleMenuClose} isActive={isActive('/profile')}>
个人中心
</MobileNavLink>

View File

@@ -37,33 +37,110 @@ const NavItem = styled(Link).withConfig({
align-items: center;
text-decoration: none;
color: ${props => props.isActive ? '#66bb6a' : '#6b7280'};
transition: all 0.2s ease;
padding: 8px 12px;
border-radius: 12px;
min-width: 60px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
padding: 10px 14px;
border-radius: 16px;
min-width: 64px;
position: relative;
overflow: hidden;
/* 基础状态 */
background: ${props => props.isActive
? 'linear-gradient(135deg, rgba(102, 187, 106, 0.15), rgba(129, 199, 132, 0.1))'
: 'transparent'};
box-shadow: ${props => props.isActive
? '0 2px 8px rgba(102, 187, 106, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.3)'
: '0 0 0 rgba(0, 0, 0, 0)'};
transform: ${props => props.isActive ? 'translateY(-2px)' : 'translateY(0)'};
/* 伪元素用于悬停效果 */
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(102, 187, 106, 0.1), rgba(129, 199, 132, 0.05));
opacity: 0;
transition: opacity 0.3s ease;
border-radius: 16px;
}
&:hover {
color: #66bb6a;
background: rgba(129, 199, 132, 0.1);
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(102, 187, 106, 0.25),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
&::before {
opacity: 1;
}
}
&:active {
transform: translateY(-1px);
transition: all 0.1s ease;
}
.nav-icon {
font-size: 20px;
margin-bottom: 4px;
transition: transform 0.2s ease;
font-size: 22px;
margin-bottom: 6px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
z-index: 1;
filter: ${props => props.isActive ? 'drop-shadow(0 1px 2px rgba(102, 187, 106, 0.3))' : 'none'};
}
.nav-text {
font-size: 11px;
font-weight: 500;
font-weight: ${props => props.isActive ? '600' : '500'};
line-height: 1;
position: relative;
z-index: 1;
text-shadow: ${props => props.isActive ? '0 1px 2px rgba(102, 187, 106, 0.2)' : 'none'};
}
${props => props.isActive && `
.nav-icon {
transform: scale(1.1);
transform: scale(1.15) rotate(5deg);
animation: bounce 0.6s ease;
}
.nav-text {
animation: fadeInUp 0.4s ease 0.1s both;
}
`}
&:hover .nav-icon {
transform: scale(1.2) rotate(-2deg);
}
@keyframes bounce {
0%, 20%, 53%, 80%, 100% {
transform: scale(1.15) rotate(5deg) translateY(0);
}
40%, 43% {
transform: scale(1.15) rotate(5deg) translateY(-4px);
}
70% {
transform: scale(1.15) rotate(5deg) translateY(-2px);
}
90% {
transform: scale(1.15) rotate(5deg) translateY(-1px);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`;
const Navigation = () => {

View File

@@ -0,0 +1,194 @@
import React, { useEffect, useRef, useCallback, useMemo } from 'react';
import styled from 'styled-components';
const ParticleContainer = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9999;
overflow: hidden;
`;
const ParticleEffect = () => {
const containerRef = useRef(null);
const particleId = useRef(0);
const colors = useMemo(() => [
'#4caf50', '#81c784', '#a5d6a7', '#c8e6c9',
'#66bb6a', '#8bc34a', '#cddc39', '#ffeb3b',
'#ffc107', '#ff9800', '#ff5722', '#e91e63',
'#9c27b0', '#673ab7', '#3f51b5', '#2196f3',
'#03a9f4', '#00bcd4', '#009688', '#4caf50'
], []);
const createParticle = useCallback((x, y) => {
if (!containerRef.current) return;
const particleCount = Math.random() * 8 + 6; // 6-14个粒子
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement('div');
particle.className = 'click-particle';
particle.id = `particle-${particleId.current++}`;
// 随机颜色
const color = colors[Math.floor(Math.random() * colors.length)];
// 随机大小
const size = Math.random() * 8 + 4; // 4-12px
// 随机方向和距离
const angle = (Math.PI * 2 * i) / particleCount + Math.random() * 0.5;
const distance = Math.random() * 100 + 50; // 50-150px
const dx = Math.cos(angle) * distance;
const dy = Math.sin(angle) * distance - Math.random() * 50; // 向上偏移
// 设置样式
particle.style.cssText = `
position: absolute;
left: ${x - size/2}px;
top: ${y - size/2}px;
width: ${size}px;
height: ${size}px;
background: ${color};
border-radius: 50%;
pointer-events: none;
box-shadow: 0 0 6px ${color}40;
--dx: ${dx}px;
--dy: ${dy}px;
animation: particleAnimation 1.2s ease-out forwards;
z-index: 9999;
`;
containerRef.current.appendChild(particle);
// 动画结束后移除粒子
setTimeout(() => {
if (particle && particle.parentNode) {
particle.parentNode.removeChild(particle);
}
}, 1200);
}
}, [colors]);
const createRipple = useCallback((x, y) => {
if (!containerRef.current) return;
const ripple = document.createElement('div');
ripple.className = 'click-ripple';
ripple.id = `ripple-${particleId.current++}`;
const color = colors[Math.floor(Math.random() * colors.length)];
ripple.style.cssText = `
position: absolute;
left: ${x}px;
top: ${y}px;
width: 0;
height: 0;
border-radius: 50%;
background: radial-gradient(circle, ${color}20 0%, transparent 70%);
border: 2px solid ${color}40;
pointer-events: none;
animation: rippleAnimation 0.8s ease-out forwards;
z-index: 9998;
transform: translate(-50%, -50%);
`;
// 添加涟漪动画
const style = document.createElement('style');
style.textContent = `
@keyframes rippleAnimation {
0% {
width: 0;
height: 0;
opacity: 1;
}
100% {
width: 100px;
height: 100px;
opacity: 0;
}
}
`;
if (!document.querySelector('#ripple-animation-style')) {
style.id = 'ripple-animation-style';
document.head.appendChild(style);
}
containerRef.current.appendChild(ripple);
setTimeout(() => {
if (ripple && ripple.parentNode) {
ripple.parentNode.removeChild(ripple);
}
}, 800);
}, [colors]);
const handleClick = useCallback((event) => {
const x = event.clientX;
const y = event.clientY;
// 创建粒子效果
createParticle(x, y);
// 创建涟漪效果
createRipple(x, y);
}, [createParticle, createRipple]);
useEffect(() => {
// 添加全局点击监听器
document.addEventListener('click', handleClick);
// 添加粒子动画样式
const style = document.createElement('style');
style.id = 'particle-animation-style';
style.textContent = `
@keyframes particleAnimation {
0% {
transform: translate(0, 0) scale(1);
opacity: 1;
}
100% {
transform: translate(var(--dx), var(--dy)) scale(0);
opacity: 0;
}
}
.click-particle {
animation: particleAnimation 1.2s ease-out forwards;
}
.click-ripple {
animation: rippleAnimation 0.8s ease-out forwards;
}
`;
if (!document.querySelector('#particle-animation-style')) {
document.head.appendChild(style);
}
return () => {
document.removeEventListener('click', handleClick);
// 清理样式
const existingStyle = document.querySelector('#particle-animation-style');
if (existingStyle) {
existingStyle.remove();
}
const rippleStyle = document.querySelector('#ripple-animation-style');
if (rippleStyle) {
rippleStyle.remove();
}
};
}, [handleClick]);
return <ParticleContainer ref={containerRef} />;
};
export default ParticleEffect;

View File

@@ -0,0 +1,23 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
/**
* ScrollToTop 组件
* 监听路由变化,在页面切换时自动滚动到顶部
*/
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
// 页面切换时滚动到顶部
window.scrollTo({
top: 0,
left: 0,
behavior: 'instant' // 立即滚动,不使用平滑动画
});
}, [pathname]); // 依赖于路径变化
return null; // 这个组件不渲染任何内容
}
export default ScrollToTop;

View File

@@ -114,17 +114,21 @@ export const API_60S_CATEGORIES = [
apis: [
{ title: '哔哩哔哩热搜榜', link: '/60sapi/热搜榜单/哔哩哔哩热搜榜/index.html', icon: '📺', IsShow: true },
{ title: '抖音热搜榜', link: '/60sapi/热搜榜单/抖音热搜榜/index.html', icon: '🎵', IsShow: true },
{ title: '小红书热搜榜', link: '/60sapi/热搜榜单/小红书热点/index.html', icon: '📖', IsShow: true },
{ title: '猫眼票房排行榜', link: '/60sapi/热搜榜单/猫眼票房排行榜/index.html', icon: '🎬', IsShow: true },
{ title: '头条热搜榜', link: '/60sapi/热搜榜单/头条热搜榜/index.html', icon: '📰', IsShow: true },
{ title: '猫眼电视收视率排行榜', link: '/60sapi/热搜榜单/猫眼电视收视排行/index.html', icon: '📺', IsShow: true },
{ title: '猫眼电影实时票房', link: '/60sapi/热搜榜单/猫眼电影实时票房/index.html', icon: '🎬', IsShow: true },
{ title: '猫眼网剧实时热搜榜', link: '/60sapi/热搜榜单/猫眼网剧实时热度/index.html', icon: '💻', IsShow: true },
{ title: '今日头条热搜榜', link: '/60sapi/热搜榜单/头条热搜榜/index.html', icon: '📰', IsShow: true },
{ title: '网易云榜单', link: '/60sapi/热搜榜单/网易云榜单/index.html', icon: '🎶', IsShow: true },
{ title: '微博热搜榜', link: '/60sapi/热搜榜单/微博热搜榜/index.html', icon: '📱', IsShow: true },
{ title: '知乎热门话题', link: '/60sapi/热搜榜单/知乎热门话题/index.html', icon: '💡', IsShow: true },
{ title: 'Hacker News 榜单', link: '/60sapi/热搜榜单/Hacker News 榜单/index.html', icon: '💻', IsShow: true },
{ title: '小红书热点', link: '/60sapi/热搜榜单/小红书热点/index.html', icon: '📖', IsShow: true },
{ title: '百度实时热搜', link: '/60sapi/热搜榜单/百度实时热搜/index.html', icon: '🔍', IsShow: true },
{ title: '百度电视剧榜', link: '/60sapi/热搜榜单/百度电视剧榜/index.html', icon: '📺', IsShow: true },
{ title: 'HackerNews HotRanks', link: '/60sapi/热搜榜单/Hacker News 榜单/index.html', icon: '💻', IsShow: true },
{ title: '百度实时热搜', link: '/60sapi/热搜榜单/百度实时热搜/index.html', icon: '🔍', IsShow: true },
{ title: '百度电视剧热搜榜', link: '/60sapi/热搜榜单/百度电视剧榜/index.html', icon: '📺', IsShow: true },
{ title: '百度贴吧话题榜', link: '/60sapi/热搜榜单/百度贴吧话题榜/index.html', icon: '💬', IsShow: true },
{ title: '懂车帝热搜', link: '/60sapi/热搜榜单/懂车帝热搜/index.html', icon: '🚗', IsShow: true },
{ title: '懂车帝热搜', link: '/60sapi/热搜榜单/懂车帝热搜/index.html', icon: '🚗', IsShow: true },
]
},
{

View File

@@ -3,7 +3,8 @@
// 统一环境配置
const config = {
API_URL: 'https://infogenie.api.shumengya.top',
//API_URL: 'https://infogenie.api.shumengya.top',
API_URL: 'http://127.0.0.1:5002', // 确保本地开发环境正常工作
DEBUG: true,
LOG_LEVEL: 'debug'
};

View File

@@ -1,118 +0,0 @@
# 前端邮件功能测试指南
## 问题修复说明
### 修复的问题
- **响应拦截器问题**:修复了 `api.js` 中响应拦截器直接返回 `response.data` 导致前端无法正确访问 `response.data.success` 的问题
- **API响应格式不匹配**:现在前端代码可以正确处理后端返回的响应格式
### 修复内容
`src/utils/api.js` 文件中:
```javascript
// 修复前
api.interceptors.response.use(
(response) => {
return response.data; // 这里直接返回了data导致前端无法访问response.data.success
},
// ...
);
// 修复后
api.interceptors.response.use(
(response) => {
return response; // 现在返回完整的response对象
},
// ...
);
```
## 测试步骤
### 1. 启动服务
确保以下服务正在运行:
- **后端服务**`http://localhost:5000`
- **前端服务**`http://localhost:3001`
### 2. 测试注册功能
1. 打开浏览器访问 `http://localhost:3001`
2. 点击登录按钮或直接访问 `/login` 页面
3. 切换到「注册」标签
4. 填写以下信息:
- **邮箱**输入有效的QQ邮箱your_qq@qq.com
- **用户名**:输入用户名
- **密码**输入密码至少6位
- **确认密码**:再次输入相同密码
5. 点击「发送验证码」按钮
6. 检查是否显示成功提示:"验证码已发送到您的邮箱"
7. 检查邮箱是否收到验证码邮件
8. 输入收到的验证码
9. 点击「注册」按钮完成注册
### 3. 测试登录功能(验证码登录)
1. 在登录页面选择「验证码登录」
2. 输入已注册的QQ邮箱
3. 点击「发送验证码」按钮
4. 检查是否显示成功提示
5. 检查邮箱是否收到登录验证码
6. 输入验证码并点击「登录」
### 4. 测试登录功能(密码登录)
1. 在登录页面选择「密码登录」
2. 输入邮箱和密码
3. 点击「登录」按钮
## 预期结果
### 成功的表现
- ✅ 点击「发送验证码」后显示绿色成功提示
- ✅ 倒计时正常显示60秒
- ✅ 邮箱收到格式正确的验证码邮件
- ✅ 后端日志显示:"验证码邮件发送成功: your_email@qq.com"
- ✅ 验证码验证成功,注册/登录流程完整
### 失败的表现
- ❌ 显示红色错误提示
- ❌ 邮箱未收到验证码
- ❌ 后端日志显示SMTP错误
## 技术细节
### API调用流程
1. 前端调用 `authAPI.sendVerification(data)`
2. 请求发送到 `/api/auth/send-verification`
3. 后端处理邮件发送
4. 返回响应格式:`{ success: true/false, message: "...", data: {...} }`
5. 前端通过 `response.data.success` 判断是否成功
### 环境变量要求
确保后端设置了正确的环境变量:
```bash
MAIL_USERNAME=your_qq_email@qq.com
MAIL_PASSWORD=your_qq_auth_code
```
## 故障排除
### 如果仍然无法发送邮件
1. 检查后端环境变量是否正确设置
2. 确认QQ邮箱已开启SMTP服务并获取授权码
3. 检查网络连接是否正常
4. 查看浏览器开发者工具的Network标签确认API请求状态
5. 查看后端控制台日志,确认具体错误信息
### 常见错误
- **535 Authentication failed**QQ邮箱授权码错误
- **Network Error**:前后端连接问题
- **Timeout**网络超时或SMTP服务器响应慢
## 注意事项
- 仅支持QQ邮箱系列qq.com、vip.qq.com、foxmail.com
- 验证码有效期为10分钟
- 同一邮箱60秒内只能发送一次验证码
- 验证码最多尝试5次
---
**修复完成时间**2025年9月2日
**修复内容**API响应拦截器格式问题
**测试状态**:✅ 后端功能正常前端API调用已修复

View File

@@ -0,0 +1,294 @@
import React from 'react';
import styled from 'styled-components';
import { FiInfo, FiGlobe, FiDownload, FiStar, FiHeart } from 'react-icons/fi';
const AboutContainer = styled.div`
min-height: calc(100vh - 140px);
padding: 20px 0;
opacity: 0;
transform: translateY(20px);
animation: pageEnter 0.8s ease-out forwards;
@keyframes pageEnter {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
`;
const Container = styled.div`
max-width: 800px;
margin: 0 auto;
padding: 0 16px;
`;
const PageHeader = styled.div`
text-align: center;
margin-bottom: 40px;
`;
const PageTitle = styled.h1`
color: white;
font-size: 44.8px;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
@media (max-width: 768px) {
font-size: 33.6px;
}
`;
const PageDescription = styled.p`
color: rgba(255, 255, 255, 0.8);
font-size: 18px;
max-width: 600px;
margin: 0 auto;
`;
const AboutCard = styled.div`
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 40px;
box-shadow: 0 8px 32px rgba(168, 230, 207, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(168, 230, 207, 0.2);
margin-bottom: 24px;
`;
const AppIcon = styled.div`
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24px;
img {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 8px;
}
`;
const AppName = styled.h2`
font-size: 32px;
font-weight: bold;
color: #2e7d32;
text-align: center;
margin-bottom: 8px;
`;
const AppVersion = styled.div`
text-align: center;
margin-bottom: 24px;
`;
const VersionBadge = styled.span`
background: linear-gradient(135deg, #81c784 0%, #a5d6a7 100%);
color: white;
padding: 6px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
box-shadow: 0 2px 8px rgba(129, 199, 132, 0.3);
`;
const AppDescription = styled.p`
color: #4a4a4a;
font-size: 16px;
line-height: 1.6;
text-align: center;
margin-bottom: 32px;
`;
const LinksSection = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin-bottom: 32px;
`;
const LinkCard = styled.a`
background: rgba(129, 199, 132, 0.1);
border: 1px solid rgba(129, 199, 132, 0.3);
border-radius: 16px;
padding: 20px;
text-decoration: none;
color: inherit;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 12px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(129, 199, 132, 0.2);
background: rgba(129, 199, 132, 0.15);
border-color: #81c784;
}
`;
const LinkIcon = styled.div`
width: 40px;
height: 40px;
background: linear-gradient(135deg, #81c784 0%, #a5d6a7 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
flex-shrink: 0;
`;
const LinkContent = styled.div`
flex: 1;
`;
const LinkTitle = styled.div`
font-weight: 600;
color: #2e7d32;
margin-bottom: 4px;
`;
const LinkUrl = styled.div`
font-size: 14px;
color: #666;
word-break: break-all;
`;
const FeatureSection = styled.div`
margin-top: 32px;
`;
const FeatureTitle = styled.h3`
font-size: 20px;
font-weight: bold;
color: #2e7d32;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
`;
const FeatureList = styled.ul`
list-style: none;
padding: 0;
margin: 0;
`;
const FeatureItem = styled.li`
color: #4a4a4a;
font-size: 14px;
margin-bottom: 8px;
padding-left: 20px;
position: relative;
&:before {
content: '✓';
position: absolute;
left: 0;
color: #10b981;
font-weight: bold;
}
&:last-child {
margin-bottom: 0;
}
`;
const FooterText = styled.div`
text-align: center;
color: #666;
font-size: 14px;
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid rgba(129, 199, 132, 0.2);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
`;
const AboutPage = () => {
return (
<AboutContainer>
<Container>
<PageHeader>
<PageTitle>关于我们</PageTitle>
<PageDescription>
<strong>了解万象口袋的更多信息和功能特色(,,ω,,)</strong>
</PageDescription>
</PageHeader>
<AboutCard>
<AppIcon>
<img src="/assets/logo.png" alt="InfoGenie Logo" />
</AppIcon>
<AppName>万象口袋</AppName>
<AppVersion>
<VersionBadge>v2.2.3</VersionBadge>
</AppVersion>
<AppDescription>
一款跨平台式聚合应用集成了多种实用工具和娱乐功能
为用户提供便捷的一站式服务体验
</AppDescription>
<LinksSection>
<LinkCard href="https://infogenie.shumengya.top" target="_blank" rel="noopener noreferrer">
<LinkIcon>
<FiGlobe />
</LinkIcon>
<LinkContent>
<LinkTitle>Web端在线体验</LinkTitle>
<LinkUrl>https://infogenie.shumengya.top</LinkUrl>
</LinkContent>
</LinkCard>
<LinkCard href="https://work.shumengya.top/#/work/InfoGenie" target="_blank" rel="noopener noreferrer">
<LinkIcon>
<FiDownload />
</LinkIcon>
<LinkContent>
<LinkTitle>最新版下载地址</LinkTitle>
<LinkUrl>https://work.shumengya.top/#/work/InfoGenie</LinkUrl>
</LinkContent>
</LinkCard>
</LinksSection>
<FeatureSection>
<FeatureTitle>
<FiStar />
主要功能
</FeatureTitle>
<FeatureList>
<FeatureItem>聚合应用 - 提供天气预报平台热搜百度百科等实用工具</FeatureItem>
<FeatureItem>休闲游戏 - 迷你解压小游戏即点即玩</FeatureItem>
<FeatureItem>AI工具 - AI翻译AI写诗文章转换功能体验</FeatureItem>
<FeatureItem>用户系统 - 个人中心签到奖励等</FeatureItem>
<FeatureItem>跨平台 - 支持WebWindowsAndroid平台使用</FeatureItem>
<FeatureItem>响应式设计 - 完美适配各种设备屏幕</FeatureItem>
</FeatureList>
</FeatureSection>
<FooterText>
<FiHeart style={{ color: '#ef4444' }} />
感谢您使用万象口袋我们将持续为您提供更好的服务
</FooterText>
</AboutCard>
</Container>
</AboutContainer>
);
};
export default AboutPage;

View File

@@ -10,6 +10,20 @@ import api from '../utils/api';
const AiContainer = styled.div`
min-height: calc(100vh - 140px);
padding: 20px 0;
opacity: 0;
transform: translateY(20px);
animation: pageEnter 0.8s ease-out forwards;
@keyframes pageEnter {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
`;
const Container = styled.div`
@@ -25,7 +39,7 @@ const PageHeader = styled.div`
const PageTitle = styled.h1`
color: white;
font-size: 32px;
font-size: 44.8px;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
@@ -35,7 +49,7 @@ const PageTitle = styled.h1`
}
@media (max-width: 768px) {
font-size: 24px;
font-size: 33.6px;
}
`;
@@ -437,7 +451,7 @@ const AiModelPage = () => {
每次使用AI功能将消耗<b>100萌芽币</b>使AI
</p>
<p style={{ lineHeight: '1.6', color: '#374151' }}>
您可以通过<b>每日签到</b>300使AI
您可以通过<b>每日签到</b>300
</p>
</div>
)}

View File

@@ -8,6 +8,20 @@ import { API_60S_CATEGORIES } from '../config/StaticPageConfig';
const Api60sContainer = styled.div`
min-height: calc(100vh - 140px);
padding: 20px 0;
opacity: 0;
transform: translateY(20px);
animation: pageEnter 0.8s ease-out forwards;
@keyframes pageEnter {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
`;
const Container = styled.div`
@@ -38,23 +52,89 @@ const Subtitle = styled.p`
const CategorySection = styled.div`
margin-bottom: 50px;
position: relative;
&::before {
content: '';
position: absolute;
top: -10px;
left: -20px;
right: -20px;
bottom: -20px;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 50%,
rgba(0, 0, 0, 0.02) 100%);
border-radius: 20px;
pointer-events: none;
z-index: -1;
}
`;
const CategoryTitle = styled.h2`
color: rgba(255, 255, 255, 0.95);
font-size: 24px;
font-weight: 600;
margin-bottom: 20px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
text-shadow:
0 2px 8px rgba(0, 0, 0, 0.2),
0 1px 3px rgba(0, 0, 0, 0.3);
position: relative;
padding: 8px 0;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 60px;
height: 2px;
background: linear-gradient(90deg,
rgba(255, 255, 255, 0.6) 0%,
rgba(255, 255, 255, 0.2) 100%);
border-radius: 1px;
}
`;
const CategoryDescription = styled.p`
color: rgba(255, 255, 255, 0.9);
font-size: 17px;
margin: 0 0 20px 0;
line-height: 1.4;
font-weight: 400;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
`;
const CategoryGrid = styled.div`
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
position: relative;
padding: 8px;
border-radius: 12px;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.02) 0%,
rgba(255, 255, 255, 0.01) 50%,
rgba(0, 0, 0, 0.01) 100%);
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 12px;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.05) 0%,
transparent 50%,
rgba(0, 0, 0, 0.02) 100%);
pointer-events: none;
z-index: -1;
}
@media (max-width: 1200px) {
grid-template-columns: repeat(4, 1fr);
@@ -69,24 +149,29 @@ const CategoryGrid = styled.div`
@media (max-width: 600px) {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
padding: 6px;
}
`;
const ApiCard = styled.div`
background: rgba(255, 255, 255, 0.98);
background: linear-gradient(145deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.95));
border-radius: 16px;
padding: 20px 16px;
text-decoration: none;
color: inherit;
transition: all 0.2s ease;
border: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.1),
0 1px 3px rgba(0, 0, 0, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.6);
cursor: pointer;
min-height: 90px;
display: flex;
align-items: center;
backdrop-filter: blur(10px);
@media (max-width: 768px) {
padding: 16px 12px;
@@ -101,16 +186,42 @@ const ApiCard = styled.div`
right: 0;
height: 3px;
background: ${props => props.color || 'linear-gradient(135deg, #81c784 0%, #a5d6a7 100%)'};
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.05) 50%,
rgba(0, 0, 0, 0.02) 100%);
pointer-events: none;
border-radius: 16px;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-3px) scale(1.02);
box-shadow:
0 8px 25px rgba(0, 0, 0, 0.15),
0 4px 12px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.8),
0 0 0 1px rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.4);
background: linear-gradient(145deg, rgba(255, 255, 255, 1), rgba(250, 252, 255, 0.98));
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
transform: translateY(-1px) scale(1.01);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.12),
0 2px 6px rgba(0, 0, 0, 0.08),
inset 0 2px 4px rgba(0, 0, 0, 0.06);
background: linear-gradient(145deg, rgba(245, 248, 250, 0.98), rgba(240, 245, 248, 0.95));
}
`;
@@ -126,6 +237,23 @@ const CardIcon = styled.div`
color: ${props => props.color || '#66bb6a'};
margin-right: 10px;
flex-shrink: 0;
position: relative;
padding: 4px;
border-radius: 6px;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.05) 100%);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
transition: all 0.2s ease;
${ApiCard}:hover & {
transform: scale(1.1);
box-shadow:
0 2px 6px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
`;
const CardTitle = styled.h3`
@@ -135,10 +263,17 @@ const CardTitle = styled.h3`
margin: 0;
flex: 1;
line-height: 1.3;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
transition: all 0.2s ease;
@media (max-width: 768px) {
font-size: 14px;
}
${ApiCard}:hover & {
color: #1b5e20;
text-shadow: 0 1px 3px rgba(255, 255, 255, 0.9);
}
`;
const ExternalIcon = styled.div`
@@ -146,6 +281,16 @@ const ExternalIcon = styled.div`
color: #81c784;
opacity: 0.7;
flex-shrink: 0;
transition: all 0.2s ease;
padding: 2px;
border-radius: 4px;
${ApiCard}:hover & {
opacity: 1;
color: #4caf50;
background: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
`;
@@ -222,6 +367,17 @@ const Api60sPage = () => {
const [loading, setLoading] = useState(true);
const [embeddedApi, setEmbeddedApi] = useState(null);
// 获取分类描述文字
const getCategoryDescription = (categoryTitle) => {
const descriptions = {
'热搜榜单': '实时追踪各大平台热门话题,掌握最新网络动态和流行趋势',
'日更资讯': '每日精选优质内容,提供最新资讯和实用信息',
'实用功能': '集成多种便民工具,让生活和工作更加便捷高效',
'娱乐消遣': '轻松有趣的娱乐内容,为您的闲暇时光增添乐趣'
};
return descriptions[categoryTitle] || '';
};
// 从配置文件获取60s API数据
const scanApiModules = async () => {
try {
@@ -299,6 +455,9 @@ const Api60sPage = () => {
{category.icon}
{category.title}
</CategoryTitle>
<CategoryDescription>
{getCategoryDescription(category.title)}
</CategoryDescription>
<CategoryGrid>
{category.apis.map((api, apiIndex) => (
<ApiCard

View File

@@ -7,6 +7,20 @@ import { FiActivity, FiGrid, FiCpu, FiTrendingUp } from 'react-icons/fi';
const HomeContainer = styled.div`
min-height: calc(100vh - 140px);
padding: 20px 0;
opacity: 0;
transform: translateY(20px);
animation: pageEnter 0.8s ease-out forwards;
@keyframes pageEnter {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
`;
const HeroSection = styled.section`
@@ -27,9 +41,34 @@ const HeroTitle = styled.h1`
font-weight: bold;
color: #1f2937;
margin-bottom: 16px;
text-shadow:
0 2px 4px rgba(0, 0, 0, 0.1),
0 4px 8px rgba(129, 199, 132, 0.2);
position: relative;
.title-emoji {
margin: 0 8px;
display: inline-block;
animation: float 3s ease-in-out infinite;
filter: drop-shadow(0 2px 4px rgba(129, 199, 132, 0.3));
}
&::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 3px;
background: linear-gradient(90deg, #81c784, #a5d6a7, #c8e6c9);
border-radius: 2px;
box-shadow: 0 2px 4px rgba(129, 199, 132, 0.3);
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-3px); }
}
@media (max-width: 768px) {
@@ -60,15 +99,71 @@ const HeroButton = styled(Link)`
text-decoration: none;
font-weight: 600;
font-size: 16px;
transition: all 0.3s ease;
box-shadow: 0 8px 32px rgba(129, 199, 132, 0.4);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 8px 32px rgba(129, 199, 132, 0.4),
0 4px 16px rgba(129, 199, 132, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.3),
inset 0 -1px 0 rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.6s ease;
}
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: all 0.3s ease;
pointer-events: none;
}
&:hover {
transform: translateY(-3px);
box-shadow: 0 12px 40px rgba(129, 199, 132, 0.5);
transform: translateY(-4px) scale(1.05);
box-shadow:
0 16px 48px rgba(129, 199, 132, 0.5),
0 8px 24px rgba(129, 199, 132, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.4),
inset 0 -1px 0 rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(255, 255, 255, 0.1);
background: linear-gradient(135deg, #66bb6a 0%, #81c784 100%);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
&::before {
left: 100%;
}
&::after {
width: 120px;
height: 120px;
}
}
&:active {
transform: translateY(-2px) scale(1.02);
transition: all 0.1s ease;
box-shadow:
0 8px 24px rgba(129, 199, 132, 0.4),
0 4px 12px rgba(129, 199, 132, 0.2),
inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
`;
@@ -84,9 +179,42 @@ const SectionTitle = styled.h2`
color: #1f2937;
margin-bottom: 24px;
text-align: center;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
.section-emoji {
margin-right: 12px;
display: inline-block;
filter: drop-shadow(0 2px 4px rgba(129, 199, 132, 0.3));
animation: pulse 2s ease-in-out infinite;
}
&::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(129, 199, 132, 0.3), transparent);
z-index: -1;
}
&::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 2px;
background: linear-gradient(90deg, #81c784, #a5d6a7);
border-radius: 1px;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
`;
@@ -108,16 +236,64 @@ const ModuleCard = styled(Link)`
padding: 32px 24px;
text-decoration: none;
color: inherit;
box-shadow: 0 8px 32px rgba(168, 230, 207, 0.3);
transition: all 0.3s ease;
box-shadow:
0 8px 32px rgba(168, 230, 207, 0.3),
0 2px 8px rgba(129, 199, 132, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid rgba(168, 230, 207, 0.2);
backdrop-filter: blur(10px);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, #81c784, #a5d6a7, #c8e6c9);
opacity: 0;
transition: opacity 0.3s ease;
}
&::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(129, 199, 132, 0.05) 0%, transparent 70%);
opacity: 0;
transition: all 0.4s ease;
pointer-events: none;
}
&:hover {
transform: translateY(-6px);
box-shadow: 0 12px 40px rgba(168, 230, 207, 0.4);
transform: translateY(-8px) scale(1.02);
box-shadow:
0 20px 60px rgba(168, 230, 207, 0.4),
0 8px 24px rgba(129, 199, 132, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.9),
0 0 0 1px rgba(129, 199, 132, 0.3);
border-color: #81c784;
background: rgba(255, 255, 255, 0.98);
&::before {
opacity: 1;
}
&::after {
opacity: 1;
transform: scale(1.1);
}
}
&:active {
transform: translateY(-4px) scale(1.01);
transition: all 0.1s ease;
}
`;
@@ -132,20 +308,83 @@ const ModuleIcon = styled.div`
color: white;
font-size: 24px;
margin-bottom: 20px;
box-shadow: 0 4px 16px rgba(129, 199, 132, 0.3);
box-shadow:
0 4px 16px rgba(129, 199, 132, 0.3),
0 2px 8px rgba(129, 199, 132, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
position: relative;
transition: all 0.3s ease;
&::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(135deg, #a5d6a7, #c8e6c9);
border-radius: 22px;
z-index: -1;
opacity: 0;
transition: opacity 0.3s ease;
}
${ModuleCard}:hover & {
transform: translateY(-2px) scale(1.05);
box-shadow:
0 8px 24px rgba(129, 199, 132, 0.4),
0 4px 12px rgba(129, 199, 132, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
&::before {
opacity: 1;
}
}
`;
const ModuleTitle = styled.h3`
font-size: 20px;
font-weight: bold;
font-size: 20px !important;
font-weight: 700 !important;
color: #2e7d32;
margin-bottom: 12px;
line-height: 1.2 !important;
letter-spacing: normal !important;
text-shadow: 0 1px 2px rgba(46, 125, 50, 0.1);
position: relative;
transition: all 0.3s ease;
&::before {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: linear-gradient(90deg, #81c784, #a5d6a7);
border-radius: 1px;
transition: width 0.3s ease;
}
${ModuleCard}:hover & {
color: #1b5e20;
text-shadow: 0 2px 4px rgba(46, 125, 50, 0.2);
&::before {
width: 100%;
}
}
`;
const ModuleDescription = styled.p`
color: #4a4a4a;
line-height: 1.6;
margin-bottom: 16px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
transition: color 0.3s ease;
${ModuleCard}:hover & {
color: #2d3748;
}
`;
const ModuleFeatures = styled.ul`
@@ -160,6 +399,8 @@ const ModuleFeature = styled.li`
margin-bottom: 8px;
padding-left: 20px;
position: relative;
transition: all 0.3s ease;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&:before {
content: '✓';
@@ -167,11 +408,23 @@ const ModuleFeature = styled.li`
left: 0;
color: #10b981;
font-weight: bold;
text-shadow: 0 1px 2px rgba(16, 185, 129, 0.3);
transition: all 0.3s ease;
}
&:last-child {
margin-bottom: 0;
}
${ModuleCard}:hover & {
color: #1f2937;
transform: translateX(2px);
&:before {
color: #059669;
transform: scale(1.1);
}
}
`;
//================css样式================

View File

@@ -12,6 +12,20 @@ const LoginContainer = styled.div`
align-items: center;
justify-content: center;
padding: 20px;
opacity: 0;
transform: translateY(20px);
animation: pageEnter 0.8s ease-out forwards;
@keyframes pageEnter {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
`;
const LoginCard = styled.div`

View File

@@ -6,6 +6,20 @@ import { SMALL_GAMES } from '../config/StaticPageConfig';
const GameContainer = styled.div`
min-height: calc(100vh - 140px);
padding: 20px 0;
opacity: 0;
transform: translateY(20px);
animation: pageEnter 0.8s ease-out forwards;
@keyframes pageEnter {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
`;
const Container = styled.div`
@@ -21,7 +35,7 @@ const PageHeader = styled.div`
const PageTitle = styled.h1`
color: white;
font-size: 32px;
font-size: 44.8px;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
@@ -31,7 +45,7 @@ const PageTitle = styled.h1`
}
@media (max-width: 768px) {
font-size: 24px;
font-size: 33.6px;
}
`;
@@ -350,6 +364,12 @@ const SmallGamePage = () => {
<EmbeddedFrame
src={embeddedGame.link}
title={embeddedGame.title}
allow="keyboard-map *"
tabIndex="0"
onLoad={(e) => {
// 确保iframe获得焦点以接收键盘事件
e.target.focus();
}}
/>
</EmbeddedContent>
</EmbeddedContainer>

View File

@@ -9,6 +9,20 @@ const ProfileContainer = styled.div`
min-height: calc(100vh - 140px);
padding: 20px 0;
background: linear-gradient(135deg, rgba(74, 222, 128, 0.05) 0%, rgba(34, 197, 94, 0.05) 100%);
opacity: 0;
transform: translateY(20px);
animation: pageEnter 0.8s ease-out forwards;
@keyframes pageEnter {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
`;
const Container = styled.div`
@@ -24,13 +38,13 @@ const PageHeader = styled.div`
const PageTitle = styled.h1`
color: white;
font-size: 32px;
font-size: 44.8px;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
@media (max-width: 768px) {
font-size: 24px;
font-size: 33.6px;
}
`;

View File

@@ -317,16 +317,36 @@
.mb-5 { margin-bottom: 20px; }
.mb-6 { margin-bottom: 24px; }
.pt-1 { padding-top: 4px; }
.pt-2 { padding-top: 8px; }
.pt-3 { padding-top: 12px; }
.pt-4 { padding-top: 16px; }
.pt-5 { padding-top: 20px; }
.pt-6 { padding-top: 24px; }
/* 粒子效果样式优化 */
.click-particle {
will-change: transform, opacity;
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
filter: blur(0.5px);
}
.pb-1 { padding-bottom: 4px; }
.pb-2 { padding-bottom: 8px; }
.pb-3 { padding-bottom: 12px; }
.pb-4 { padding-bottom: 16px; }
.pb-5 { padding-bottom: 20px; }
.pb-6 { padding-bottom: 24px; }
.click-ripple {
will-change: transform, opacity;
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* 为可点击元素添加更好的视觉反馈 */
button, .btn, a, [role="button"] {
position: relative;
overflow: hidden;
}
/* 优化粒子效果在不同背景下的显示 */
@media (prefers-reduced-motion: reduce) {
.click-particle,
.click-ripple {
animation-duration: 0.3s;
}
}
/* 高性能动画优化 */
.click-particle,
.click-ripple {
transform: translateZ(0);
backface-visibility: hidden;
perspective: 1000px;
}

View File

@@ -1,27 +0,0 @@
import requests
url = "https://api.coolapk.com/v6/page/dataList?url=%23%2Ftopic%2FtagList%3FfilterId%3D9%26keywords%3D%E6%89%8B%E6%9C%BA%E5%A4%84%E7%90%86%E5%99%A8%26ratingUI%3D1&title=SoC%E6%A6%9C&page=1&firstItem=132485"
headers = {
'User-Agent': "Dalvik/2.1.0 (Linux; U; Android 13; 23049RAD8C Build/TKQ1.221114.001) (#Build; Redmi; 23049RAD8C; TKQ1.221114.001 test-keys; 13) +CoolMarket/14.0.0-2401171-universal",
'Connection': "Keep-Alive",
'Accept-Encoding': "gzip",
'X-Requested-With': "XMLHttpRequest",
'X-Sdk-Int': "33",
'X-Sdk-Locale': "zh-CN",
'X-App-Id': "com.coolapk.market",
'X-App-Token': "v3JDJ5JDEwJE5qaGpOemMyTUdRdllUWm1PVEZqWU8wemJjdWhNRHFEYm5URi84TG5yLnRnYkVjTVlWd2JD",
'X-App-Version': "14.0.0",
'X-App-Code': "2401171",
'X-Api-Version': "14",
'X-App-Device': "0UzMjlTZ1MWZyYDNlVTM3AyOzlXZr1CdzVGdgEDMw4CNxETMyIjLxE1SUByODhDRBJVO0AzMyAyOp1GZlJFI7kWbvFWaYByOgsDI7AyOhV2TqNXYVdWR3cXQ6hjZYNTWORkY5IXajZzbOl0bfpkaIVFR",
'X-Dark-Mode': "1",
'X-App-Channel': "yyb",
'X-App-Mode': "universal",
'X-App-Supported': "2401171",
'Cookie': "ddid=763cdbda-df79-485a-b4ac-92df073f1df5"
}
response = requests.get(url, headers=headers)
print(response.text)

View File

@@ -1,27 +0,0 @@
import requests
url = "https://api.coolapk.com/v6/page/dataList?url=%23%2Fproduct%2FproductList%3Ftype%3Dcategory%26id%3D1000%26sortField%3Dv4_score_item_6_owner_average_score%26limitField%3Dv4_score_item_6_owner_total_count-200%3Brank_status-0-0%26ratingUI%3D1%26withConfigCard%3D1%26configCardExtraData%3D%257B%2522withRanking%2522%253A1%257D%26rightStyle%3Dtext%26rightBottomText%3D%E6%80%A7%E4%BB%B7%E6%AF%94%E5%88%86%26rightTopField%3Dv4_score_item_6_owner_average_score&title=%E6%80%A7%E4%BB%B7%E6%AF%94%E6%A6%9C&page=1&firstItem=4068"
headers = {
'User-Agent': "Dalvik/2.1.0 (Linux; U; Android 13; 23049RAD8C Build/TKQ1.221114.001) (#Build; Redmi; 23049RAD8C; TKQ1.221114.001 test-keys; 13) +CoolMarket/14.0.0-2401171-universal",
'Connection': "Keep-Alive",
'Accept-Encoding': "gzip",
'X-Requested-With': "XMLHttpRequest",
'X-Sdk-Int': "33",
'X-Sdk-Locale': "zh-CN",
'X-App-Id': "com.coolapk.market",
'X-App-Token': "v3JDJ5JDEwJE5qaGpOemMxWldVdk9USmtaR1kzTXU3U3lLanVDVnJIR3RoMUdoNzhVT1dKNFlpdmxGL3l5",
'X-App-Version': "14.0.0",
'X-App-Code': "2401171",
'X-Api-Version': "14",
'X-App-Device': "0UzMjlTZ1MWZyYDNlVTM3AyOzlXZr1CdzVGdgEDMw4CNxETMyIjLxE1SUByODhDRBJVO0AzMyAyOp1GZlJFI7kWbvFWaYByOgsDI7AyOhV2TqNXYVdWR3cXQ6hjZYNTWORkY5IXajZzbOl0bfpkaIVFR",
'X-Dark-Mode': "1",
'X-App-Channel': "yyb",
'X-App-Mode': "universal",
'X-App-Supported': "2401171",
'Cookie': "ddid=763cdbda-df79-485a-b4ac-92df073f1df5"
}
response = requests.get(url, headers=headers)
print(response.text)

View File

@@ -1,27 +0,0 @@
import requests
url = "https://api.coolapk.com/v6/page/dataList?url=%23%2Fproduct%2FhotProductList%3FhotType%3Dday%26withConfigCard%3D1%26configCardExtraData%3D%257B%2522withRanking%2522%253A1%257D&title=%E7%83%AD%E5%BA%A6%E6%A6%9C&page=1&firstItem=4283"
headers = {
'User-Agent': "Dalvik/2.1.0 (Linux; U; Android 13; 23049RAD8C Build/TKQ1.221114.001) (#Build; Redmi; 23049RAD8C; TKQ1.221114.001 test-keys; 13) +CoolMarket/14.0.0-2401171-universal",
'Connection': "Keep-Alive",
'Accept-Encoding': "gzip",
'X-Requested-With': "XMLHttpRequest",
'X-Sdk-Int': "33",
'X-Sdk-Locale': "zh-CN",
'X-App-Id': "com.coolapk.market",
'X-App-Token': "v3JDJ5JDEwJE5qaGpObVJpTkRZdk1EVTVORGxpTi5XNDFxTUdXQjVsRzB1NWdEY1pXdmJjQUMyWEFnRE1X",
'X-App-Version': "14.0.0",
'X-App-Code': "2401171",
'X-Api-Version': "14",
'X-App-Device': "0UzMjlTZ1MWZyYDNlVTM3AyOzlXZr1CdzVGdgEDMw4CNxETMyIjLxE1SUByODhDRBJVO0AzMyAyOp1GZlJFI7kWbvFWaYByOgsDI7AyOhV2TqNXYVdWR3cXQ6hjZYNTWORkY5IXajZzbOl0bfpkaIVFR",
'X-Dark-Mode': "1",
'X-App-Channel': "yyb",
'X-App-Mode': "universal",
'X-App-Supported': "2401171",
'Cookie': "ddid=f8600b13-4ac2-4207-8541-9cec299202ac"
}
response = requests.get(url, headers=headers)
print(response.text)

View File

@@ -1,27 +0,0 @@
import requests
url = "https://api.coolapk.com/v6/page/dataList?url=%23%2Fproduct%2FhotProductList%3FhotType%3Dday%26withConfigCard%3D1%26configCardExtraData%3D%257B%2522withRanking%2522%253A1%257D&title=%E7%83%AD%E5%BA%A6%E6%A6%9C&page=1&firstItem=4283"
headers = {
'User-Agent': "Dalvik/2.1.0 (Linux; U; Android 13; 23049RAD8C Build/TKQ1.221114.001) (#Build; Redmi; 23049RAD8C; TKQ1.221114.001 test-keys; 13) +CoolMarket/14.0.0-2401171-universal",
'Connection': "Keep-Alive",
'Accept-Encoding': "gzip",
'X-Requested-With': "XMLHttpRequest",
'X-Sdk-Int': "33",
'X-Sdk-Locale': "zh-CN",
'X-App-Id': "com.coolapk.market",
'X-App-Token': "v3JDJ5JDEwJE5qaGpOemMxTkRJdk1ERmpNemszT094SE5UZEpybXZXbjQ5bU03NlVBS1pTRi9nRzRxb0NL",
'X-App-Version': "14.0.0",
'X-App-Code': "2401171",
'X-Api-Version': "14",
'X-App-Device': "0UzMjlTZ1MWZyYDNlVTM3AyOzlXZr1CdzVGdgEDMw4CNxETMyIjLxE1SUByODhDRBJVO0AzMyAyOp1GZlJFI7kWbvFWaYByOgsDI7AyOhV2TqNXYVdWR3cXQ6hjZYNTWORkY5IXajZzbOl0bfpkaIVFR",
'X-Dark-Mode': "1",
'X-App-Channel': "yyb",
'X-App-Mode': "universal",
'X-App-Supported': "2401171",
'Cookie': "ddid=763cdbda-df79-485a-b4ac-92df073f1df5"
}
response = requests.get(url, headers=headers)
print(response.text)

View File

@@ -1,27 +0,0 @@
import requests
url = "https://api.coolapk.com/v6/page/dataList?url=%23%2Ftopic%2FtagList%3FfilterId%3D3%26keywords%3DROM%26ratingUI%3D1&title=%E7%B3%BB%E7%BB%9F%E6%A6%9C&page=1&firstItem=128126"
headers = {
'User-Agent': "Dalvik/2.1.0 (Linux; U; Android 13; 23049RAD8C Build/TKQ1.221114.001) (#Build; Redmi; 23049RAD8C; TKQ1.221114.001 test-keys; 13) +CoolMarket/14.0.0-2401171-universal",
'Connection': "Keep-Alive",
'Accept-Encoding': "gzip",
'X-Requested-With': "XMLHttpRequest",
'X-Sdk-Int': "33",
'X-Sdk-Locale': "zh-CN",
'X-App-Id': "com.coolapk.market",
'X-App-Token': "v3JDJ5JDEwJE5qaGpOemMxWXpndk9EUmtOekpoTnVlQUczTHV1dUh3QTN2ZVdWUlVWcmNiaE02aVRyS1pH",
'X-App-Version': "14.0.0",
'X-App-Code': "2401171",
'X-Api-Version': "14",
'X-App-Device': "0UzMjlTZ1MWZyYDNlVTM3AyOzlXZr1CdzVGdgEDMw4CNxETMyIjLxE1SUByODhDRBJVO0AzMyAyOp1GZlJFI7kWbvFWaYByOgsDI7AyOhV2TqNXYVdWR3cXQ6hjZYNTWORkY5IXajZzbOl0bfpkaIVFR",
'X-Dark-Mode': "1",
'X-App-Channel': "yyb",
'X-App-Mode': "universal",
'X-App-Supported': "2401171",
'Cookie': "ddid=763cdbda-df79-485a-b4ac-92df073f1df5"
}
response = requests.get(url, headers=headers)
print(response.text)