Compare commits
6 Commits
7786e5f507
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 04a4cb962a | |||
| d48c2647f9 | |||
| bef33251cb | |||
| 1221d6faf1 | |||
| 9d9924dd79 | |||
| c147502b4d |
36
.dockerignore
Normal file
36
.dockerignore
Normal file
@@ -0,0 +1,36 @@
|
||||
# Node modules
|
||||
InfoGenie-frontend/node_modules
|
||||
InfoGenie-frontend/build
|
||||
|
||||
# Python cache
|
||||
InfoGenie-backend/__pycache__
|
||||
InfoGenie-backend/**/__pycache__
|
||||
InfoGenie-backend/*.pyc
|
||||
InfoGenie-backend/**/*.pyc
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test files
|
||||
InfoGenie-backend/test
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
|
||||
# Backup files
|
||||
*.backup
|
||||
*.bak
|
||||
57
Dockerfile
Normal file
57
Dockerfile
Normal file
@@ -0,0 +1,57 @@
|
||||
# InfoGenie 统一 Docker 镜像
|
||||
# 多阶段构建:前端构建 + 后端 + Nginx
|
||||
|
||||
# 阶段1: 前端构建
|
||||
FROM node:18-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
COPY InfoGenie-frontend/package*.json ./
|
||||
RUN npm install --legacy-peer-deps
|
||||
COPY InfoGenie-frontend/ ./
|
||||
RUN npm run build
|
||||
|
||||
# 阶段2: 最终镜像
|
||||
FROM python:3.10-slim
|
||||
|
||||
# 安装 Nginx 和必要的工具
|
||||
RUN apt-get update && apt-get install -y \
|
||||
nginx \
|
||||
supervisor \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制后端代码
|
||||
COPY InfoGenie-backend/ ./backend/
|
||||
|
||||
# 安装 Python 依赖
|
||||
RUN pip install --no-cache-dir -r ./backend/requirements.txt gunicorn
|
||||
|
||||
# 复制前端构建产物到 Nginx 目录
|
||||
COPY --from=frontend-builder /frontend/build /usr/share/nginx/html
|
||||
|
||||
# 创建持久化数据目录
|
||||
RUN mkdir -p /app/data/logs
|
||||
|
||||
# 复制 Nginx 配置
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY docker/default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 复制 Supervisor 配置
|
||||
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# 复制启动脚本
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 2323
|
||||
|
||||
# 设置环境变量
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_ENV=production
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# 使用 supervisor 管理多进程
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -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/
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"deepseek": {
|
||||
"api_key": "sk-832f8e5250464de08a31523c7fd71295",
|
||||
"api_key": "sk-832f8e5250464de08a31523c7fd712",
|
||||
"api_base": "https://api.deepseek.com",
|
||||
"model": ["deepseek-chat","deepseek-reasoner"]
|
||||
},
|
||||
|
||||
"kimi": {
|
||||
"api_key": "sk-zdg9NBpTlhOcDDpoWfaBKu0KNDdGv18SipORnL2utawja0bE",
|
||||
"api_key": "sk-zdg9NBpTlhOcDDpoWfaBKu0KNDdGv18SipORnL2utawja",
|
||||
"api_base": "https://api.moonshot.cn",
|
||||
"model": ["kimi-k2-0905-preview","kimi-k2-0711-preview"]
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
InfoGenie-backend/modules/__pycache__/game_stats.cpython-313.pyc
Normal file
BIN
InfoGenie-backend/modules/__pycache__/game_stats.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
@@ -855,6 +855,56 @@ def linux_command_generator():
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Linux命令生成失败: {str(e)}'}), 500
|
||||
|
||||
#AI文章排版(Markdown格式化)接口
|
||||
@aimodelapp_bp.route('/markdown_formatting', methods=['POST'])
|
||||
@verify_user_coins
|
||||
def markdown_formatting():
|
||||
"""AI文章排版(Markdown格式化)接口"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
article_text = data.get('article_text', '').strip()
|
||||
emoji_style = data.get('emoji_style', 'balanced').strip()
|
||||
markdown_option = data.get('markdown_option', 'standard').strip()
|
||||
|
||||
if not article_text:
|
||||
return jsonify({'error': '文章内容不能为空'}), 400
|
||||
|
||||
# 构建Markdown排版的提示词
|
||||
prompt = f"""你是一位专业的文档排版助手。请将用户提供的全文按“标准Markdown格式”进行排版,并在不改变任何原文内容的前提下进行结构化呈现。严格遵守以下规则:
|
||||
|
||||
1) 保留所有原始内容,严禁改写、删减或添加新内容。
|
||||
2) 使用合理的Markdown结构(标题、分节、段落、列表、引用、表格如有必要、代码块仅当原文包含)。
|
||||
3) 智能添加适量Emoji以增强可读性({emoji_style}),在标题、关键句、列表项等处点缀;避免过度使用,保持专业。
|
||||
4) 保持语言与语气不变,只优化排版和表现形式。
|
||||
5) 输出“纯Markdown文本”,不要包含任何JSON、HTML、XML、解释文字、或代码块围栏标记(例如不要在最外层使用```)。
|
||||
|
||||
如果原文本较长,可在开头自动生成简洁的“目录”以便阅读。
|
||||
|
||||
原文如下:
|
||||
{article_text}
|
||||
"""
|
||||
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
# 使用DeepSeek进行排版生成
|
||||
content, error = call_deepseek_api(messages)
|
||||
|
||||
if error:
|
||||
return jsonify({'error': error}), 500
|
||||
|
||||
# 返回AI生成的Markdown文本
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'formatted_markdown': content,
|
||||
'source_text': article_text,
|
||||
'emoji_style': emoji_style,
|
||||
'markdown_option': markdown_option,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'文章排版失败: {str(e)}'}), 500
|
||||
|
||||
#获取用户萌芽币余额
|
||||
@aimodelapp_bp.route('/coins', methods=['GET'])
|
||||
def get_user_coins():
|
||||
@@ -951,4 +1001,87 @@ def get_available_models():
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'获取模型列表失败: {str(e)}'}), 500
|
||||
return jsonify({'error': f'获取模型列表失败: {str(e)}'}), 500
|
||||
|
||||
#中国亲戚称呼计算器接口(普通话版 + 方言)
|
||||
@aimodelapp_bp.route('/kinship-calculator', methods=['POST'])
|
||||
@verify_user_coins
|
||||
def kinship_calculator():
|
||||
"""中国亲戚称呼计算器接口"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
relation_chain = (data.get('relation_chain') or '').strip()
|
||||
dialects = data.get('dialects') # 可选,指定方言列表
|
||||
|
||||
if not relation_chain:
|
||||
return jsonify({'error': '亲属关系链不能为空'}), 400
|
||||
|
||||
# 组装提示词:要求严格JSON输出
|
||||
requested_dialects = dialects if isinstance(dialects, list) and dialects else [
|
||||
'粤语', '闽南语', '上海话', '四川话', '东北话', '客家话'
|
||||
]
|
||||
|
||||
prompt = f"""你是一位中国亲属称呼专家。请解析下面的亲属关系链,给出最终的亲属称呼。
|
||||
输入的关系链会用“的”连接,如“妈妈的爸爸”“爸爸的姐姐的儿子”。
|
||||
|
||||
请遵循:
|
||||
1) 以中国大陆通行的标准普通话称呼为准,给出最常用、规范的最终称呼。
|
||||
2) 同时给出若干方言的对应称呼:{', '.join(requested_dialects)}。
|
||||
3) 如存在地区差异或性别歧义,请在notes中说明,但最终给出一个最常用称呼。
|
||||
4) 不要展示推理过程;只输出JSON。
|
||||
|
||||
严格按以下JSON结构输出:
|
||||
{{
|
||||
"mandarin_title": "标准普通话称呼",
|
||||
"dialect_titles": {{
|
||||
"粤语": {{"title": "称呼", "romanization": "粤拼或发音", "notes": "可选说明"}},
|
||||
"闽南语": {{"title": "称呼", "romanization": "白话字或发音", "notes": "可选说明"}},
|
||||
"上海话": {{"title": "称呼", "romanization": "拟音或IPA", "notes": "可选说明"}},
|
||||
"四川话": {{"title": "称呼", "romanization": "拟音或IPA", "notes": "可选说明"}},
|
||||
"东北话": {{"title": "称呼", "romanization": "拟音或IPA", "notes": "可选说明"}},
|
||||
"客家话": {{"title": "称呼", "romanization": "客家话拟音", "notes": "可选说明"}}
|
||||
}},
|
||||
"notes": "总体说明(如地区差异、辈分方向、父系/母系等提示)"
|
||||
}}
|
||||
|
||||
关系链:
|
||||
{relation_chain}
|
||||
"""
|
||||
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
content, error = call_deepseek_api(messages)
|
||||
|
||||
if error:
|
||||
return jsonify({'error': error}), 500
|
||||
|
||||
# 解析AI返回的JSON
|
||||
try:
|
||||
result = json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
import re
|
||||
m = re.search(r'\{[\s\S]*\}', content)
|
||||
if not m:
|
||||
return jsonify({'error': 'AI返回的数据中未找到有效JSON'}), 500
|
||||
try:
|
||||
result = json.loads(m.group())
|
||||
except Exception:
|
||||
return jsonify({'error': 'AI返回的JSON格式无法解析'}), 500
|
||||
|
||||
mandarin_title = result.get('mandarin_title')
|
||||
dialect_titles = result.get('dialect_titles', {})
|
||||
notes = result.get('notes', '')
|
||||
|
||||
if not mandarin_title:
|
||||
return jsonify({'error': '未获得标准普通话称呼'}), 500
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'relation_chain': relation_chain,
|
||||
'mandarin_title': mandarin_title,
|
||||
'dialect_titles': dialect_titles,
|
||||
'notes': notes,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'亲戚称呼计算失败: {str(e)}'}), 500
|
||||
@@ -70,6 +70,8 @@ def validate_password(password):
|
||||
"""验证密码格式(6-20位)"""
|
||||
return 6 <= len(password) <= 20
|
||||
|
||||
|
||||
#==========================对外暴露的HTTP接口==========================
|
||||
#发送验证码邮件
|
||||
@auth_bp.route('/send-verification', methods=['POST'])
|
||||
def send_verification():
|
||||
@@ -450,3 +452,4 @@ def check_login():
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
#==========================对外暴露的HTTP接口==========================
|
||||
@@ -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;">
|
||||
|
||||
@@ -51,14 +51,29 @@ def login_required(f):
|
||||
return decorated_function
|
||||
return decorated_function
|
||||
|
||||
|
||||
#==========================对外暴露的HTTP接口==========================
|
||||
# 获取用户资料
|
||||
@user_bp.route('/profile', methods=['GET'])
|
||||
@login_required
|
||||
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 +83,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,
|
||||
@@ -84,6 +104,127 @@ def get_profile():
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 为指定账号增加萌芽币
|
||||
@user_bp.route('/add-coins', methods=['POST'])
|
||||
@login_required
|
||||
def add_coins():
|
||||
"""为指定账号增加萌芽币(支持email或username指定账号,amount为正整数)"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
email = (data.get('email') or '').strip()
|
||||
username = (data.get('username') or '').strip()
|
||||
amount = data.get('amount')
|
||||
|
||||
# 参数校验
|
||||
if not email and not username:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请提供email或username其中之一'
|
||||
}), 400
|
||||
|
||||
if amount is None:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'amount不能为空'
|
||||
}), 400
|
||||
|
||||
try:
|
||||
amount_int = int(amount)
|
||||
except Exception:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'amount必须为整数'
|
||||
}), 400
|
||||
|
||||
if amount_int <= 0:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'amount必须为正整数'
|
||||
}), 400
|
||||
|
||||
users_collection = current_app.mongo.db.userdata
|
||||
query = {'邮箱': email} if email else {'用户名': username}
|
||||
user = users_collection.find_one(query)
|
||||
if not user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '用户不存在'
|
||||
}), 404
|
||||
|
||||
before_coins = user.get('萌芽币', 0)
|
||||
update_result = users_collection.update_one(query, {'$inc': {'萌芽币': amount_int}})
|
||||
|
||||
if update_result.modified_count == 0:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '更新失败,请稍后重试'
|
||||
}), 500
|
||||
|
||||
updated = users_collection.find_one({'_id': user['_id']})
|
||||
new_coins = updated.get('萌芽币', before_coins)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f"已为账户{email or username}增加{amount_int}萌芽币",
|
||||
'data': {
|
||||
'before_coins': before_coins,
|
||||
'added': amount_int,
|
||||
'new_coins': new_coins,
|
||||
'user': {
|
||||
'id': str(updated.get('_id')),
|
||||
'email': updated.get('邮箱'),
|
||||
'username': updated.get('用户名'),
|
||||
'avatar': updated.get('头像'),
|
||||
'register_time': updated.get('注册时间'),
|
||||
'last_login': updated.get('最后登录'),
|
||||
'login_count': updated.get('登录次数', 0),
|
||||
'status': updated.get('用户状态', 'active'),
|
||||
'level': updated.get('等级', 0),
|
||||
'experience': updated.get('经验', 0),
|
||||
'coins': new_coins
|
||||
}
|
||||
}
|
||||
}), 200
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 列出所有用户
|
||||
@user_bp.route('/list', methods=['GET'])
|
||||
@login_required
|
||||
def list_users():
|
||||
"""列出所有用户(不返回密码)"""
|
||||
try:
|
||||
users_collection = current_app.mongo.db.userdata
|
||||
cursor = users_collection.find({}, {'密码': 0})
|
||||
users = []
|
||||
for u in cursor:
|
||||
users.append({
|
||||
'id': str(u.get('_id')),
|
||||
'email': u.get('邮箱'),
|
||||
'username': u.get('用户名'),
|
||||
'avatar': u.get('头像'),
|
||||
'register_time': u.get('注册时间'),
|
||||
'last_login': u.get('最后登录'),
|
||||
'login_count': u.get('登录次数', 0),
|
||||
'status': u.get('用户状态', 'active'),
|
||||
'level': u.get('等级', 0),
|
||||
'experience': u.get('经验', 0),
|
||||
'coins': u.get('萌芽币', 0)
|
||||
})
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'count': len(users),
|
||||
'data': users
|
||||
}), 200
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 修改密码
|
||||
@user_bp.route('/change-password', methods=['POST'])
|
||||
@login_required
|
||||
@@ -406,3 +547,4 @@ def delete_account():
|
||||
'success': False,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
}), 500
|
||||
#==========================对外暴露的HTTP接口==========================
|
||||
100
InfoGenie-backend/test/test_add_coins.py
Normal file
100
InfoGenie-backend/test/test_add_coins.py
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试为指定账号增加萌芽币接口 (/api/user/add-coins)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 加入后端根目录到路径,导入create_app
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app import create_app
|
||||
from modules.auth import generate_token
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
|
||||
def run_test():
|
||||
"""运行加币接口测试,打印真实响应并断言结果"""
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
db = app.mongo.db
|
||||
users = db.userdata
|
||||
|
||||
# 构造一个临时测试用户(真实写库,测试结束删除)
|
||||
test_email = "infogenie.test.addcoins@foxmail.com"
|
||||
users.delete_many({'邮箱': test_email})
|
||||
test_user = {
|
||||
'邮箱': test_email,
|
||||
'用户名': '测试用户_加币',
|
||||
'密码': generate_password_hash('AddCoins123!'),
|
||||
'头像': None,
|
||||
'注册时间': datetime.now().isoformat(),
|
||||
'最后登录': None,
|
||||
'登录次数': 0,
|
||||
'用户状态': 'active',
|
||||
'等级': 0,
|
||||
'经验': 0,
|
||||
'萌芽币': 0,
|
||||
'签到系统': {
|
||||
'连续签到天数': 0,
|
||||
'今日是否已签到': False,
|
||||
'签到时间': datetime.now().strftime('%Y-%m-%d')
|
||||
}
|
||||
}
|
||||
insert_result = users.insert_one(test_user)
|
||||
test_user_id = str(insert_result.inserted_id)
|
||||
|
||||
# 生成有效JWT用于认证
|
||||
token = generate_token({
|
||||
'user_id': test_user_id,
|
||||
'email': test_email,
|
||||
'username': test_user['用户名']
|
||||
})
|
||||
|
||||
client = app.test_client()
|
||||
|
||||
# 第一次加币: +500
|
||||
resp1 = client.post(
|
||||
'/api/user/add-coins',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
json={'email': test_email, 'amount': 500}
|
||||
)
|
||||
print('第一次加币 状态码:', resp1.status_code)
|
||||
data1 = resp1.get_json()
|
||||
print('第一次加币 响应:')
|
||||
print(json.dumps(data1, ensure_ascii=False, indent=2))
|
||||
assert resp1.status_code == 200
|
||||
assert data1.get('success') is True
|
||||
assert data1['data']['before_coins'] == 0
|
||||
assert data1['data']['added'] == 500
|
||||
assert data1['data']['new_coins'] == 500
|
||||
|
||||
# 第二次加币: +200
|
||||
resp2 = client.post(
|
||||
'/api/user/add-coins',
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
json={'email': test_email, 'amount': 200}
|
||||
)
|
||||
print('第二次加币 状态码:', resp2.status_code)
|
||||
data2 = resp2.get_json()
|
||||
print('第二次加币 响应:')
|
||||
print(json.dumps(data2, ensure_ascii=False, indent=2))
|
||||
assert resp2.status_code == 200
|
||||
assert data2.get('success') is True
|
||||
assert data2['data']['before_coins'] == 500
|
||||
assert data2['data']['added'] == 200
|
||||
assert data2['data']['new_coins'] == 700
|
||||
|
||||
# 清理临时测试用户
|
||||
users.delete_many({'邮箱': test_email})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('🔧 开始测试 /api/user/add-coins 接口...')
|
||||
run_test()
|
||||
print('✅ 测试完成!')
|
||||
81
InfoGenie-backend/test/test_user_list.py
Normal file
81
InfoGenie-backend/test/test_user_list.py
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
测试列出所有用户的HTTP接口 (/api/user/list)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 将后端根目录加入路径,便于导入app
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app import create_app
|
||||
from modules.auth import generate_token
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
|
||||
def run_test():
|
||||
"""运行用户列表接口测试,输出真实数据"""
|
||||
# 使用.env中的真实Mongo配置,不造假
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
db = app.mongo.db
|
||||
users = db.userdata
|
||||
|
||||
# 插入一个测试用户(真实写入后再删除),确保可验证接口输出
|
||||
test_email = "infogenie.test.user@foxmail.com"
|
||||
users.delete_many({'邮箱': test_email})
|
||||
test_user = {
|
||||
'邮箱': test_email,
|
||||
'用户名': '测试用户_列表',
|
||||
'密码': generate_password_hash('TestPass123!'),
|
||||
'头像': None,
|
||||
'注册时间': datetime.now().isoformat(),
|
||||
'最后登录': None,
|
||||
'登录次数': 0,
|
||||
'用户状态': 'active',
|
||||
'等级': 0,
|
||||
'经验': 0,
|
||||
'萌芽币': 0,
|
||||
'签到系统': {
|
||||
'连续签到天数': 0,
|
||||
'今日是否已签到': False,
|
||||
'签到时间': datetime.now().strftime('%Y-%m-%d')
|
||||
}
|
||||
}
|
||||
insert_result = users.insert_one(test_user)
|
||||
test_user_id = str(insert_result.inserted_id)
|
||||
|
||||
# 生成有效JWT,满足认证要求
|
||||
token = generate_token({
|
||||
'user_id': test_user_id,
|
||||
'email': test_email,
|
||||
'username': test_user['用户名']
|
||||
})
|
||||
|
||||
client = app.test_client()
|
||||
resp = client.get('/api/user/list', headers={'Authorization': f'Bearer {token}'})
|
||||
|
||||
print("状态码:", resp.status_code)
|
||||
data = resp.get_json()
|
||||
print("响应内容:")
|
||||
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
|
||||
# 基本断言,确保返回真实列表数据且包含刚插入的测试用户
|
||||
assert resp.status_code == 200
|
||||
assert data.get('success') is True
|
||||
assert isinstance(data.get('data'), list)
|
||||
assert any(u.get('email') == test_email for u in data['data'])
|
||||
|
||||
# 清理测试数据
|
||||
users.delete_many({'邮箱': test_email})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('🔎 开始测试 /api/user/list 接口...')
|
||||
run_test()
|
||||
print('✅ 测试完成!')
|
||||
@@ -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 Token(PyJWT 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使用记录和萌芽币消费情况
|
||||
|
||||
---
|
||||
|
||||
11
InfoGenie-frontend/.env.development
Normal file
11
InfoGenie-frontend/.env.development
Normal file
@@ -0,0 +1,11 @@
|
||||
# React 开发环境变量
|
||||
|
||||
# API URL - 开发环境使用本地后端
|
||||
REACT_APP_API_URL=http://127.0.0.1:5002
|
||||
|
||||
# 应用信息
|
||||
REACT_APP_NAME=InfoGenie
|
||||
REACT_APP_VERSION=1.0.0
|
||||
|
||||
# 调试模式
|
||||
REACT_APP_DEBUG=true
|
||||
13
InfoGenie-frontend/.env.production
Normal file
13
InfoGenie-frontend/.env.production
Normal file
@@ -0,0 +1,13 @@
|
||||
# React 构建时环境变量
|
||||
# 用于 Docker 构建
|
||||
|
||||
# API URL - 在 Docker 环境下,前端和后端在同一个容器
|
||||
# 使用相对路径,这样前端会自动使用当前域名
|
||||
REACT_APP_API_URL=
|
||||
|
||||
# 应用信息
|
||||
REACT_APP_NAME=InfoGenie
|
||||
REACT_APP_VERSION=1.0.0
|
||||
|
||||
# 调试模式(生产环境关闭)
|
||||
REACT_APP_DEBUG=false
|
||||
54
InfoGenie-frontend/package-lock.json
generated
54
InfoGenie-frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
body {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#fff8dc 0%,
|
||||
#ffeaa7 25%,
|
||||
#fdcb6e 50%,
|
||||
#e17055 75%,
|
||||
#d63031 100%
|
||||
#f1f8e9 0%,
|
||||
#dcedc8 25%,
|
||||
#c8e6c8 50%,
|
||||
#a5d6a7 75%,
|
||||
#81c784 100%
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
@@ -24,9 +24,9 @@ body::before {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(255, 215, 0, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 223, 0, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(212, 175, 55, 0.05) 0%, transparent 50%);
|
||||
radial-gradient(circle at 20% 80%, rgba(129, 199, 132, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(165, 214, 167, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(102, 187, 106, 0.05) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -40,11 +40,11 @@ body::after {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
radial-gradient(2px 2px at 20px 30px, rgba(255, 215, 0, 0.3), transparent),
|
||||
radial-gradient(2px 2px at 40px 70px, rgba(255, 223, 0, 0.2), transparent),
|
||||
radial-gradient(1px 1px at 90px 40px, rgba(212, 175, 55, 0.4), transparent),
|
||||
radial-gradient(1px 1px at 130px 80px, rgba(255, 215, 0, 0.2), transparent),
|
||||
radial-gradient(2px 2px at 160px 30px, rgba(255, 223, 0, 0.3), transparent);
|
||||
radial-gradient(2px 2px at 20px 30px, rgba(129, 199, 132, 0.3), transparent),
|
||||
radial-gradient(2px 2px at 40px 70px, rgba(165, 214, 167, 0.2), transparent),
|
||||
radial-gradient(1px 1px at 90px 40px, rgba(102, 187, 106, 0.4), transparent),
|
||||
radial-gradient(1px 1px at 130px 80px, rgba(129, 199, 132, 0.2), transparent),
|
||||
radial-gradient(2px 2px at 160px 30px, rgba(165, 214, 167, 0.3), transparent);
|
||||
background-repeat: repeat;
|
||||
background-size: 200px 100px;
|
||||
animation: sparkle 20s linear infinite;
|
||||
@@ -106,8 +106,8 @@ body::after {
|
||||
|
||||
body::before {
|
||||
background:
|
||||
radial-gradient(circle at 30% 70%, rgba(255, 215, 0, 0.08) 0%, transparent 40%),
|
||||
radial-gradient(circle at 70% 30%, rgba(255, 223, 0, 0.08) 0%, transparent 40%);
|
||||
radial-gradient(circle at 30% 70%, rgba(129, 199, 132, 0.08) 0%, transparent 40%),
|
||||
radial-gradient(circle at 70% 30%, rgba(165, 214, 167, 0.08) 0%, transparent 40%);
|
||||
}
|
||||
|
||||
body::after {
|
||||
@@ -121,9 +121,9 @@ body::after {
|
||||
body {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#fff8dc 0%,
|
||||
#ffeaa7 50%,
|
||||
#fdcb6e 100%
|
||||
#f1f8e9 0%,
|
||||
#dcedc8 50%,
|
||||
#c8e6c8 100%
|
||||
);
|
||||
background-size: 150% 150%;
|
||||
}
|
||||
@@ -138,18 +138,18 @@ body::after {
|
||||
body {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#2c1810 0%,
|
||||
#3d2914 25%,
|
||||
#4a3319 50%,
|
||||
#5c3e1f 75%,
|
||||
#6b4423 100%
|
||||
#1b2e1b 0%,
|
||||
#2e4a2e 25%,
|
||||
#3e5e3e 50%,
|
||||
#4e6e4e 75%,
|
||||
#5e7e5e 100%
|
||||
);
|
||||
}
|
||||
|
||||
body::before {
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(255, 215, 0, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 223, 0, 0.05) 0%, transparent 50%);
|
||||
radial-gradient(circle at 20% 80%, rgba(129, 199, 132, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(165, 214, 167, 0.05) 0%, transparent 50%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,6 @@ body::after {
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #fff8dc 0%, #ffeaa7 50%, #fdcb6e 100%);
|
||||
background: linear-gradient(135deg, #f1f8e9 0%, #dcedc8 50%, #c8e6c8 100%);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #2c1810;
|
||||
color: #2e7d32;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@@ -33,20 +33,20 @@ body {
|
||||
.title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #d4af37;
|
||||
color: #2e7d32;
|
||||
text-shadow:
|
||||
0 0 10px rgba(212, 175, 55, 0.8),
|
||||
0 0 20px rgba(212, 175, 55, 0.6),
|
||||
0 0 30px rgba(212, 175, 55, 0.4);
|
||||
0 0 10px rgba(129, 199, 132, 0.8),
|
||||
0 0 20px rgba(129, 199, 132, 0.6),
|
||||
0 0 30px rgba(129, 199, 132, 0.4);
|
||||
margin-bottom: 10px;
|
||||
animation: titleGlow 3s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: #b8860b;
|
||||
color: #388e3c;
|
||||
opacity: 0.9;
|
||||
text-shadow: 0 0 5px rgba(184, 134, 11, 0.5);
|
||||
text-shadow: 0 0 5px rgba(102, 187, 106, 0.5);
|
||||
}
|
||||
|
||||
/* 主内容区域 */
|
||||
@@ -58,14 +58,14 @@ body {
|
||||
|
||||
/* 一言容器 */
|
||||
.quote-container {
|
||||
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1), rgba(255, 223, 0, 0.05));
|
||||
border: 2px solid rgba(212, 175, 55, 0.3);
|
||||
background: linear-gradient(135deg, rgba(129, 199, 132, 0.1), rgba(165, 214, 167, 0.05));
|
||||
border: 2px solid rgba(102, 187, 106, 0.3);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
margin-bottom: 30px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(212, 175, 55, 0.2),
|
||||
0 8px 32px rgba(102, 187, 106, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -78,7 +78,7 @@ body {
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(45deg, #ffd700, #ffed4e, #ffd700, #ffed4e);
|
||||
background: linear-gradient(45deg, #81c784, #a5d6a7, #81c784, #a5d6a7);
|
||||
border-radius: 22px;
|
||||
z-index: -1;
|
||||
animation: borderGlow 4s linear infinite;
|
||||
@@ -88,7 +88,7 @@ body {
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
color: #d4af37;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
@@ -98,8 +98,8 @@ body {
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(212, 175, 55, 0.3);
|
||||
border-top: 4px solid #d4af37;
|
||||
border: 4px solid rgba(102, 187, 106, 0.3);
|
||||
border-top: 4px solid #2e7d32;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto 15px;
|
||||
animation: spin 1s linear infinite;
|
||||
@@ -118,15 +118,15 @@ body {
|
||||
.quote-text {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.8;
|
||||
color: #2c1810;
|
||||
color: #2e7d32;
|
||||
margin-bottom: 20px;
|
||||
text-shadow: 0 1px 2px rgba(212, 175, 55, 0.1);
|
||||
text-shadow: 0 1px 2px rgba(102, 187, 106, 0.1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quote-index {
|
||||
font-size: 0.9rem;
|
||||
color: #b8860b;
|
||||
color: #388e3c;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ body {
|
||||
.error-message {
|
||||
display: none;
|
||||
text-align: center;
|
||||
color: #cd853f;
|
||||
color: #66bb6a;
|
||||
}
|
||||
|
||||
.error-message.show {
|
||||
@@ -157,20 +157,20 @@ body {
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
||||
background: linear-gradient(135deg, #81c784, #a5d6a7);
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
padding: 15px 30px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #2c1810;
|
||||
color: #2e7d32;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow:
|
||||
0 4px 15px rgba(212, 175, 55, 0.3),
|
||||
0 4px 15px rgba(102, 187, 106, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -179,7 +179,7 @@ body {
|
||||
.refresh-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(212, 175, 55, 0.4),
|
||||
0 6px 20px rgba(102, 187, 106, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ body {
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
color: #b8860b;
|
||||
color: #388e3c;
|
||||
opacity: 0.8;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -215,15 +215,15 @@ body {
|
||||
@keyframes titleGlow {
|
||||
0% {
|
||||
text-shadow:
|
||||
0 0 10px rgba(212, 175, 55, 0.8),
|
||||
0 0 20px rgba(212, 175, 55, 0.6),
|
||||
0 0 30px rgba(212, 175, 55, 0.4);
|
||||
0 0 10px rgba(129, 199, 132, 0.8),
|
||||
0 0 20px rgba(129, 199, 132, 0.6),
|
||||
0 0 30px rgba(129, 199, 132, 0.4);
|
||||
}
|
||||
100% {
|
||||
text-shadow:
|
||||
0 0 15px rgba(212, 175, 55, 1),
|
||||
0 0 25px rgba(212, 175, 55, 0.8),
|
||||
0 0 35px rgba(212, 175, 55, 0.6);
|
||||
0 0 15px rgba(129, 199, 132, 1),
|
||||
0 0 25px rgba(129, 199, 132, 0.8),
|
||||
0 0 35px rgba(129, 199, 132, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ body {
|
||||
transition: background 0.5s ease;
|
||||
}
|
||||
|
||||
/* Hand-drawn Comic Theme Background - NEW VIBRANT VERSION */
|
||||
/* Hand-drawn Comic Theme Background - FRESH GREEN VERSION */
|
||||
body.theme-comic {
|
||||
background: linear-gradient(-45deg, #ff7e5f, #feb47b, #ffcc80, #ffecb3);
|
||||
background: linear-gradient(-45deg, #c8e6c9, #dcedc8, #f1f8e9, #e8f5e8);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientBG 15s ease infinite;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
.theme-icon.active {
|
||||
border-color: #ff7043;
|
||||
border-color: #66bb6a;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
@@ -41,26 +41,26 @@
|
||||
.theme-comic header h1 {
|
||||
font-family: 'Zhi Mang Xing', cursive;
|
||||
font-size: 4em;
|
||||
color: #d84315; /* Deep Orange */
|
||||
color: #2e7d32; /* Fresh Green */
|
||||
text-shadow: 2px 2px 0 #fff;
|
||||
margin: 0.2em 0;
|
||||
}
|
||||
|
||||
.theme-comic .divider {
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #ffca28, #ff7043, #29b6f6, #66bb6a);
|
||||
background: linear-gradient(90deg, #81c784, #a5d6a7, #c8e6c9, #66bb6a);
|
||||
border-radius: 3px;
|
||||
margin: 20px auto;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.theme-comic .joke-card {
|
||||
background: rgba(255, 255, 255, 0.85); /* White with transparency */
|
||||
background: rgba(248, 255, 248, 0.9); /* Light green tinted white */
|
||||
backdrop-filter: blur(5px);
|
||||
border-radius: 15px;
|
||||
padding: 40px;
|
||||
min-height: 200px;
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 8px 25px rgba(102, 187, 106, 0.15);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -68,6 +68,7 @@
|
||||
margin-bottom: 20px;
|
||||
transform: rotate(-1deg);
|
||||
transition: transform 0.2s ease;
|
||||
border: 1px solid rgba(129, 199, 132, 0.3);
|
||||
}
|
||||
.theme-comic .joke-card:hover {
|
||||
transform: rotate(1deg) scale(1.02);
|
||||
@@ -77,11 +78,11 @@
|
||||
font-family: 'Zhi Mang Xing', cursive;
|
||||
font-size: 2em;
|
||||
line-height: 1.6;
|
||||
color: #5d4037;
|
||||
color: #1b5e20;
|
||||
}
|
||||
|
||||
.theme-comic .new-joke-btn {
|
||||
background: #1e88e5; /* Vibrant Blue */
|
||||
background: linear-gradient(135deg, #66bb6a, #81c784); /* Fresh Green Gradient */
|
||||
color: white;
|
||||
font-family: 'Zhi Mang Xing', cursive;
|
||||
font-size: 2.5em;
|
||||
@@ -89,7 +90,7 @@
|
||||
border-radius: 50px;
|
||||
padding: 10px 30px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 5px 0 #1565c0; /* Darker Blue */
|
||||
box-shadow: 0 5px 0 #388e3c; /* Darker Green */
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
.theme-comic .new-joke-btn:active {
|
||||
@@ -120,8 +121,8 @@
|
||||
margin-top: -27.5px;
|
||||
}
|
||||
.book-page {
|
||||
background: #ffca28;
|
||||
border: 1px solid #ff7043;
|
||||
background: #a5d6a7;
|
||||
border: 1px solid #66bb6a;
|
||||
border-radius: 3px;
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
|
||||
<div class="theme-switcher">
|
||||
<div class="theme-icon" data-theme="theme-comic" title="手绘漫画">✏️</div>
|
||||
<div class="theme-icon" data-theme="theme-emoji" title="表情包狂欢">😂</div>
|
||||
<div class="theme-icon" data-theme="theme-retro" title="复古电视">📺</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
163
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/css/style.css
Normal file
163
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/css/style.css
Normal file
@@ -0,0 +1,163 @@
|
||||
/* 随机答案之书 - 淡绿色清新风格样式(与随机唱歌音频一致) */
|
||||
|
||||
/* 重置样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 50%, #ffd3a5 100%);
|
||||
min-height: 100vh;
|
||||
color: #2d5016;
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 20px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
color: #2d5016;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #5a7c65;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.btn {
|
||||
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 18px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 4px 12px rgba(129, 199, 132, 0.35);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 18px rgba(129, 199, 132, 0.45);
|
||||
}
|
||||
|
||||
/* 加载与错误 */
|
||||
.loading, .error {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 4px solid #e8f5e8;
|
||||
border-top: 4px solid #81c784;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 18px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 答案卡片 */
|
||||
.answer-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 16px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.answer-text {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
color: #1b5e20;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.answer-en {
|
||||
color: #5a7c65;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: #5a7c65;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 手机端优先优化 */
|
||||
@media (max-width: 767px) {
|
||||
.container { padding: 12px; }
|
||||
.header { padding: 18px; }
|
||||
.header h1 { font-size: 1.6rem; gap: 8px; }
|
||||
|
||||
.answer-card { padding: 16px; }
|
||||
.answer-text { font-size: 1.3rem; }
|
||||
.answer-en { font-size: 0.95rem; }
|
||||
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
45
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/index.html
Normal file
45
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/index.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>📘真理之道</title>
|
||||
<meta name="description" content="当你踌躇不定,犹豫不决时,不妨来这里看看吧。" />
|
||||
|
||||
<link rel="stylesheet" href="./css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>📘真理之道</h1>
|
||||
<p>当你踌躇不定,犹豫不决时,不妨来这里看看吧</p>
|
||||
</header>
|
||||
|
||||
<!-- 加载与错误状态 -->
|
||||
<section id="loading" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在加载中,请稍候…</p>
|
||||
</section>
|
||||
|
||||
<section id="error" class="error" style="display: none;">
|
||||
<p>获取数据失败,请稍后重试</p>
|
||||
</section>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<main id="content" style="display: none;" class="fade-in">
|
||||
<div class="answer-card">
|
||||
<div class="answer-text" id="answer">-</div>
|
||||
<div class="answer-en" id="answer-en" style="display: none;">-</div>
|
||||
<div class="meta">编号:<span id="index">-</span></div>
|
||||
<div class="actions">
|
||||
<button class="btn" id="refresh-btn">换一个</button>
|
||||
<button class="btn" id="copy-btn">复制</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="./js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
224
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/js/script.js
Normal file
224
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/js/script.js
Normal file
@@ -0,0 +1,224 @@
|
||||
// 随机答案之书 页面脚本
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const API = {
|
||||
endpoints: [],
|
||||
currentIndex: 0,
|
||||
params: {
|
||||
encoding: 'json'
|
||||
},
|
||||
localFallback: '返回接口.json',
|
||||
// 初始化API接口列表
|
||||
async init() {
|
||||
try {
|
||||
const res = await fetch('./接口集合.json');
|
||||
const endpoints = await res.json();
|
||||
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/answer`);
|
||||
} catch (e) {
|
||||
// 如果无法加载接口集合,使用默认接口
|
||||
this.endpoints = ['https://60s.api.shumengya.top/v2/answer'];
|
||||
}
|
||||
},
|
||||
// 获取当前接口URL
|
||||
getCurrentUrl() {
|
||||
if (this.endpoints.length === 0) return null;
|
||||
const url = new URL(this.endpoints[this.currentIndex]);
|
||||
Object.entries(this.params).forEach(([k, v]) => url.searchParams.append(k, v));
|
||||
return url.toString();
|
||||
},
|
||||
// 切换到下一个接口
|
||||
switchToNext() {
|
||||
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length;
|
||||
return this.currentIndex < this.endpoints.length;
|
||||
},
|
||||
// 重置到第一个接口
|
||||
reset() {
|
||||
this.currentIndex = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// DOM 元素引用
|
||||
const els = {
|
||||
loading: null,
|
||||
error: null,
|
||||
container: null,
|
||||
answer: null,
|
||||
answerEn: null,
|
||||
indexEl: null,
|
||||
refreshBtn: null,
|
||||
copyBtn: null,
|
||||
};
|
||||
|
||||
function initDom() {
|
||||
els.loading = document.getElementById('loading');
|
||||
els.error = document.getElementById('error');
|
||||
els.container = document.getElementById('content');
|
||||
|
||||
els.answer = document.getElementById('answer');
|
||||
els.answerEn = document.getElementById('answer-en');
|
||||
els.indexEl = document.getElementById('index');
|
||||
els.refreshBtn = document.getElementById('refresh-btn');
|
||||
els.copyBtn = document.getElementById('copy-btn');
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
els.loading.style.display = 'block';
|
||||
els.error.style.display = 'none';
|
||||
els.container.style.display = 'none';
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
els.loading.style.display = 'none';
|
||||
els.error.style.display = 'block';
|
||||
els.container.style.display = 'none';
|
||||
els.error.querySelector('p').textContent = msg || '获取数据失败,请稍后重试';
|
||||
}
|
||||
|
||||
function showContent() {
|
||||
els.loading.style.display = 'none';
|
||||
els.error.style.display = 'none';
|
||||
els.container.style.display = 'block';
|
||||
}
|
||||
|
||||
function safeText(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text == null ? '' : String(text);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function fetchFromAPI() {
|
||||
// 初始化API接口列表
|
||||
await API.init();
|
||||
|
||||
// 重置API索引到第一个接口
|
||||
API.reset();
|
||||
|
||||
// 尝试所有API接口
|
||||
for (let i = 0; i < API.endpoints.length; i++) {
|
||||
try {
|
||||
const url = API.getCurrentUrl();
|
||||
console.log(`尝试接口 ${i + 1}/${API.endpoints.length}: ${url}`);
|
||||
|
||||
const resp = await fetch(url, {
|
||||
cache: 'no-store',
|
||||
timeout: 10000 // 10秒超时(兼容同目录页面风格)
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
|
||||
if (data && data.code === 200) {
|
||||
console.log(`接口 ${i + 1} 请求成功`);
|
||||
return data;
|
||||
}
|
||||
|
||||
throw new Error(data && data.message ? data.message : '接口返回异常');
|
||||
|
||||
} catch (e) {
|
||||
console.warn(`接口 ${i + 1} 失败:`, e.message);
|
||||
|
||||
// 如果不是最后一个接口,切换到下一个
|
||||
if (i < API.endpoints.length - 1) {
|
||||
API.switchToNext();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 所有接口都失败了
|
||||
console.warn('所有远程接口都失败,尝试本地数据');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFromLocal() {
|
||||
try {
|
||||
const resp = await fetch(API.localFallback + `?t=${Date.now()}`);
|
||||
if (!resp.ok) throw new Error(`本地文件HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error('读取本地返回接口.json失败:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
const d = data?.data || {};
|
||||
|
||||
const cn = d.answer || '';
|
||||
const en = d.answer_en || '';
|
||||
const idx = d.index != null ? d.index : d.id != null ? d.id : '-';
|
||||
|
||||
els.answer.innerHTML = safeText(cn || '-');
|
||||
|
||||
if (en) {
|
||||
els.answerEn.style.display = 'block';
|
||||
els.answerEn.innerHTML = safeText(en);
|
||||
} else {
|
||||
els.answerEn.style.display = 'none';
|
||||
els.answerEn.innerHTML = '';
|
||||
}
|
||||
|
||||
els.indexEl.textContent = idx;
|
||||
showContent();
|
||||
}
|
||||
|
||||
async function load() {
|
||||
showLoading();
|
||||
try {
|
||||
// 先尝试远程API
|
||||
const data = await fetchFromAPI();
|
||||
if (data) {
|
||||
render(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// 远程API失败,尝试本地数据
|
||||
const localData = await fetchFromLocal();
|
||||
if (localData) {
|
||||
render(localData);
|
||||
return;
|
||||
}
|
||||
|
||||
// 都失败了
|
||||
showError('获取数据失败,请稍后重试');
|
||||
} catch (e) {
|
||||
console.error('加载数据时发生错误:', e);
|
||||
showError('获取数据失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
if (els.refreshBtn) {
|
||||
els.refreshBtn.addEventListener('click', load);
|
||||
}
|
||||
if (els.copyBtn) {
|
||||
els.copyBtn.addEventListener('click', async () => {
|
||||
const textParts = [];
|
||||
const cn = els.answer?.textContent?.trim();
|
||||
const en = els.answerEn?.textContent?.trim();
|
||||
if (cn) textParts.push(cn);
|
||||
if (en) textParts.push(en);
|
||||
const finalText = textParts.join('\n');
|
||||
try {
|
||||
await navigator.clipboard.writeText(finalText);
|
||||
const old = els.copyBtn.textContent;
|
||||
els.copyBtn.textContent = '已复制';
|
||||
setTimeout(() => { els.copyBtn.textContent = old; }, 1200);
|
||||
} catch (e) {
|
||||
alert('复制失败,请手动选择文本复制');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initDom();
|
||||
bindEvents();
|
||||
load();
|
||||
});
|
||||
})();
|
||||
3
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/接口集合.json
Normal file
3
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/接口集合.json
Normal file
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"https://60s.api.shumengya.top"
|
||||
]
|
||||
10
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/返回接口.json
Normal file
10
InfoGenie-frontend/public/60sapi/娱乐消遣/随机答案之书/返回接口.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"code": 200,
|
||||
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s,反馈群 595941841",
|
||||
"data": {
|
||||
"id": "63",
|
||||
"answer": "那不值得纠结",
|
||||
"answer_en": "It's not worth worrying about",
|
||||
"index": 62
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
body {
|
||||
background: linear-gradient(-45deg, #0a021a, #2a0d3f, #4a1a6c, #7b2f8f);
|
||||
background: linear-gradient(-45deg, #f1f8e9, #e8f5e8, #c8e6c9, #dcedc8);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientBG 20s ease infinite;
|
||||
color: #ffffff;
|
||||
color: #2e7d32;
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
|
||||
header h1 {
|
||||
font-size: 2.8em;
|
||||
color: #f0e6ff;
|
||||
text-shadow: 0 0 10px #d1a9ff, 0 0 20px #d1a9ff;
|
||||
color: #2e7d32;
|
||||
text-shadow: 0 0 10px #81c784, 0 0 20px #a5d6a7;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.2em;
|
||||
color: #e0c8ff;
|
||||
color: #388e3c;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ header p {
|
||||
.crystal-ball {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.6), rgba(200, 180, 255, 0.1));
|
||||
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.6), rgba(200, 230, 201, 0.3));
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
box-shadow: 0 0 30px #c390ff, 0 0 60px #a060e0, inset 0 0 20px rgba(255, 220, 255, 0.3);
|
||||
box-shadow: 0 0 30px #81c784, 0 0 60px #66bb6a, inset 0 0 20px rgba(220, 255, 220, 0.3);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
@@ -54,7 +54,7 @@ header p {
|
||||
left: 50%;
|
||||
width: 120%;
|
||||
height: 120%;
|
||||
background: linear-gradient(45deg, rgba(255, 192, 203, 0.1), rgba(128, 0, 128, 0.2));
|
||||
background: linear-gradient(45deg, rgba(200, 230, 201, 0.2), rgba(129, 199, 132, 0.3));
|
||||
border-radius: 50%;
|
||||
animation: swirl 10s linear infinite;
|
||||
transform: translate(-50%, -50%);
|
||||
@@ -71,7 +71,7 @@ header p {
|
||||
}
|
||||
|
||||
.fortune-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: rgba(248, 255, 248, 0.8);
|
||||
border-radius: 15px;
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
@@ -80,8 +80,8 @@ header p {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||
border: 1px solid rgba(129, 199, 132, 0.3);
|
||||
box-shadow: 0 8px 32px 0 rgba(102, 187, 106, 0.2);
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@@ -96,13 +96,13 @@ header p {
|
||||
|
||||
#luck-desc {
|
||||
font-size: 2em;
|
||||
color: #ffc0cb;
|
||||
color: #2e7d32;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
#luck-tip {
|
||||
font-size: 1.1em;
|
||||
color: #e0e0e0;
|
||||
color: #388e3c;
|
||||
margin: 0;
|
||||
padding-bottom: 20px; /* Add some space before the new details */
|
||||
}
|
||||
@@ -121,7 +121,7 @@ header p {
|
||||
|
||||
.detail-item h3 {
|
||||
font-size: 0.9em;
|
||||
color: #ffc0cb;
|
||||
color: #66bb6a;
|
||||
margin: 0 0 5px;
|
||||
font-weight: normal;
|
||||
}
|
||||
@@ -151,8 +151,8 @@ header p {
|
||||
|
||||
.tarot-container h2 {
|
||||
font-size: 1.5em;
|
||||
color: #f0e6ff;
|
||||
text-shadow: 0 0 8px #d1a9ff;
|
||||
color: #2e7d32;
|
||||
text-shadow: 0 0 8px #81c784;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@@ -188,23 +188,23 @@ header p {
|
||||
}
|
||||
|
||||
.tarot-card-back {
|
||||
background: linear-gradient(135deg, #4a1a6c, #2a0d3f);
|
||||
border: 2px solid #d1a9ff;
|
||||
background: linear-gradient(135deg, #66bb6a, #388e3c);
|
||||
border: 2px solid #81c784;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 3em;
|
||||
color: #d1a9ff;
|
||||
color: #c8e6c9;
|
||||
}
|
||||
|
||||
.tarot-card-back::after {
|
||||
content: '✧'; /* A simple star symbol */
|
||||
text-shadow: 0 0 10px #f0e6ff;
|
||||
text-shadow: 0 0 10px #e8f5e8;
|
||||
}
|
||||
|
||||
.tarot-card-front {
|
||||
background: linear-gradient(135deg, #3e165b, #592883);
|
||||
border: 2px solid #d1a9ff;
|
||||
background: linear-gradient(135deg, #4caf50, #66bb6a);
|
||||
border: 2px solid #81c784;
|
||||
color: white;
|
||||
transform: rotateY(180deg);
|
||||
padding: 20px;
|
||||
@@ -217,7 +217,7 @@ header p {
|
||||
|
||||
#tarot-name {
|
||||
font-size: 1.4em;
|
||||
color: #ffc0cb;
|
||||
color: #e8f5e8;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
@@ -248,8 +248,8 @@ header p {
|
||||
|
||||
.decor-symbol {
|
||||
position: absolute;
|
||||
color: rgba(209, 169, 255, 0.5);
|
||||
text-shadow: 0 0 10px rgba(240, 230, 255, 0.7);
|
||||
color: rgba(129, 199, 132, 0.5);
|
||||
text-shadow: 0 0 10px rgba(200, 230, 201, 0.7);
|
||||
animation: floatSymbol 20s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ header p {
|
||||
}
|
||||
|
||||
#get-fortune-btn {
|
||||
background: linear-gradient(45deg, #da70d6, #8a2be2);
|
||||
background: linear-gradient(45deg, #66bb6a, #4caf50);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
@@ -276,12 +276,12 @@ header p {
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 0 15px #c390ff;
|
||||
box-shadow: 0 0 15px #81c784;
|
||||
}
|
||||
|
||||
#get-fortune-btn:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 25px #d1a9ff;
|
||||
box-shadow: 0 0 25px #a5d6a7;
|
||||
}
|
||||
|
||||
#get-fortune-btn:active {
|
||||
@@ -289,8 +289,8 @@ header p {
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border: 4px solid rgba(255, 255, 255, 0.2);
|
||||
border-left-color: #ffc0cb;
|
||||
border: 4px solid rgba(129, 199, 132, 0.3);
|
||||
border-left-color: #66bb6a;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -308,7 +308,7 @@ header p {
|
||||
|
||||
footer {
|
||||
margin-top: 40px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
color: rgba(46, 125, 50, 0.7);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
|
||||
@@ -6,7 +6,7 @@ body::before {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #f0f8e8 0%, #e8f5e8 50%, #d4f4dd 100%);
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ body::after {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
radial-gradient(circle at 20% 80%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
|
||||
z-index: -1;
|
||||
animation: backgroundMove 20s ease-in-out infinite;
|
||||
}
|
||||
@@ -28,27 +28,27 @@ body::after {
|
||||
@keyframes backgroundMove {
|
||||
0%, 100% {
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
radial-gradient(circle at 20% 80%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
|
||||
}
|
||||
25% {
|
||||
background:
|
||||
radial-gradient(circle at 60% 30%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 30% 70%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 80%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
radial-gradient(circle at 60% 30%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 30% 70%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 80%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
|
||||
}
|
||||
50% {
|
||||
background:
|
||||
radial-gradient(circle at 80% 60%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 60% 70%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
radial-gradient(circle at 80% 60%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 20% 30%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 60% 70%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
|
||||
}
|
||||
75% {
|
||||
background:
|
||||
radial-gradient(circle at 40% 90%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 70% 10%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 20% 60%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
radial-gradient(circle at 40% 90%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 70% 10%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 20% 60%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,22 @@ body {
|
||||
color: #333;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
/* 隐藏滚动条但保留滚动功能 */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
/* 隐藏 Webkit 浏览器的滚动条 */
|
||||
body::-webkit-scrollbar,
|
||||
html::-webkit-scrollbar,
|
||||
*::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 全局隐藏滚动条但保留滚动功能 */
|
||||
html {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
/* 容器样式 */
|
||||
@@ -26,7 +42,7 @@ body {
|
||||
.header {
|
||||
text-align: center;
|
||||
padding: 3rem 2rem 2rem;
|
||||
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1), rgba(80, 200, 120, 0.1));
|
||||
background: linear-gradient(135deg, rgba(144, 238, 144, 0.15), rgba(152, 251, 152, 0.15));
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
@@ -34,7 +50,7 @@ body {
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #4a90e2, #50c878);
|
||||
background: linear-gradient(135deg, #228B22, #32CD32);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
@@ -44,7 +60,7 @@ body {
|
||||
|
||||
.header h1 i {
|
||||
margin-right: 0.5rem;
|
||||
background: linear-gradient(135deg, #4a90e2, #50c878);
|
||||
background: linear-gradient(135deg, #228B22, #32CD32);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
@@ -72,7 +88,7 @@ body {
|
||||
}
|
||||
|
||||
.query-btn {
|
||||
background: linear-gradient(135deg, #4a90e2, #50c878);
|
||||
background: linear-gradient(135deg, #228B22, #32CD32);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem 2rem;
|
||||
@@ -81,7 +97,7 @@ body {
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3);
|
||||
box-shadow: 0 4px 15px rgba(34, 139, 34, 0.3);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
@@ -91,8 +107,8 @@ body {
|
||||
|
||||
.query-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(74, 144, 226, 0.4);
|
||||
background: linear-gradient(135deg, #3a7bc8, #40a868);
|
||||
box-shadow: 0 6px 20px rgba(34, 139, 34, 0.4);
|
||||
background: linear-gradient(135deg, #1e7e1e, #2eb82e);
|
||||
}
|
||||
|
||||
.query-btn:active {
|
||||
|
||||
@@ -51,11 +51,6 @@
|
||||
<span class="label">查询时间:</span>
|
||||
<span id="query-time" class="value">--</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-server"></i>
|
||||
<span class="label">数据来源:</span>
|
||||
<span class="value">60s.viki.moe</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<span class="label">位置信息:</span>
|
||||
@@ -129,9 +124,6 @@
|
||||
</main>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer">
|
||||
<p>© 2024 公网IP地址查询工具 | 数据来源: 60s.viki.moe</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
/* 农历主题背景样式 - 柔和版本 */
|
||||
/* 全局滚动条隐藏样式 */
|
||||
html, body {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar,
|
||||
body::-webkit-scrollbar,
|
||||
*::-webkit-scrollbar {
|
||||
display: none; /* Webkit浏览器 */
|
||||
}
|
||||
|
||||
/* 农历主题背景样式 - 淡黄绿色到淡绿色清新渐变 */
|
||||
body {
|
||||
background: linear-gradient(135deg,
|
||||
#f8f9fa 0%, /* 浅灰白 */
|
||||
#fff3e0 20%, /* 淡橙色 */
|
||||
#fef7e0 40%, /* 极淡黄 */
|
||||
#f3e5ab 60%, /* 柔和金色 */
|
||||
#e8dcc6 80%, /* 米色 */
|
||||
#f8f9fa 100% /* 浅灰白 */
|
||||
#f0f8e8 0%, /* 淡黄绿色 */
|
||||
#e8f5e8 20%, /* 浅绿色 */
|
||||
#d4f4dd 40%, /* 淡绿色 */
|
||||
#c8f2d4 60%, /* 清新绿色 */
|
||||
#b8f0c8 80%, /* 柔和绿色 */
|
||||
#e8f5e8 100% /* 浅绿色 */
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gentleShift 30s ease infinite;
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
/* 隐藏滚动条但保留滚动功能 */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
|
||||
@keyframes gentleShift {
|
||||
@@ -23,7 +38,7 @@ body {
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* 动态颜色调节系统 - 柔和版本 */
|
||||
/* 动态颜色调节系统 - 绿色主题版本 */
|
||||
.adaptive-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -31,9 +46,9 @@ body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 70%, rgba(255, 255, 255, 0.25) 0%, transparent 50%),
|
||||
linear-gradient(45deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.3) 100%);
|
||||
radial-gradient(circle at 20% 30%, rgba(200, 242, 212, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 70%, rgba(184, 240, 200, 0.25) 0%, transparent 50%),
|
||||
linear-gradient(45deg, rgba(232, 245, 232, 0.2) 0%, rgba(212, 244, 221, 0.3) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
animation: adaptiveShift 60s ease infinite;
|
||||
@@ -42,33 +57,33 @@ body {
|
||||
@keyframes adaptiveShift {
|
||||
0% {
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 70%, rgba(255, 255, 255, 0.15) 0%, transparent 50%),
|
||||
linear-gradient(45deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.2) 100%);
|
||||
radial-gradient(circle at 20% 30%, rgba(232, 245, 232, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 70%, rgba(212, 244, 221, 0.15) 0%, transparent 50%),
|
||||
linear-gradient(45deg, rgba(240, 248, 232, 0.1) 0%, rgba(232, 245, 232, 0.2) 100%);
|
||||
}
|
||||
25% {
|
||||
background:
|
||||
radial-gradient(circle at 70% 20%, rgba(255, 255, 255, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 30% 80%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.25) 100%);
|
||||
radial-gradient(circle at 70% 20%, rgba(200, 242, 212, 0.2) 0%, transparent 50%),
|
||||
radial-gradient(circle at 30% 80%, rgba(184, 240, 200, 0.1) 0%, transparent 50%),
|
||||
linear-gradient(135deg, rgba(212, 244, 221, 0.15) 0%, rgba(200, 242, 212, 0.25) 100%);
|
||||
}
|
||||
50% {
|
||||
background:
|
||||
radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.15) 0%, transparent 50%),
|
||||
radial-gradient(circle at 10% 90%, rgba(255, 255, 255, 0.12) 0%, transparent 50%),
|
||||
linear-gradient(225deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.22) 100%);
|
||||
radial-gradient(circle at 50% 50%, rgba(220, 246, 228, 0.15) 0%, transparent 50%),
|
||||
radial-gradient(circle at 10% 90%, rgba(232, 245, 232, 0.12) 0%, transparent 50%),
|
||||
linear-gradient(225deg, rgba(240, 248, 232, 0.12) 0%, rgba(212, 244, 221, 0.22) 100%);
|
||||
}
|
||||
75% {
|
||||
background:
|
||||
radial-gradient(circle at 90% 60%, rgba(255, 255, 255, 0.18) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 10%, rgba(255, 255, 255, 0.08) 0%, transparent 50%),
|
||||
linear-gradient(315deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.2) 100%);
|
||||
radial-gradient(circle at 90% 60%, rgba(184, 240, 200, 0.18) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 10%, rgba(240, 248, 232, 0.08) 0%, transparent 50%),
|
||||
linear-gradient(315deg, rgba(232, 245, 232, 0.1) 0%, rgba(200, 242, 212, 0.2) 100%);
|
||||
}
|
||||
100% {
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 70%, rgba(255, 255, 255, 0.15) 0%, transparent 50%),
|
||||
linear-gradient(45deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.2) 100%);
|
||||
radial-gradient(circle at 20% 30%, rgba(232, 245, 232, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 70%, rgba(212, 244, 221, 0.15) 0%, transparent 50%),
|
||||
linear-gradient(45deg, rgba(240, 248, 232, 0.1) 0%, rgba(232, 245, 232, 0.2) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -266,12 +266,12 @@ body {
|
||||
|
||||
.date-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: #228B22;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(31, 38, 135, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3),
|
||||
0 0 0 3px rgba(255, 255, 255, 0.1);
|
||||
0 6px 20px rgba(34, 139, 34, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.8),
|
||||
0 0 0 3px rgba(34, 139, 34, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@@ -282,11 +282,11 @@ body {
|
||||
}
|
||||
|
||||
.query-btn {
|
||||
background: linear-gradient(135deg, #f0f0f0, #e0e0e0);
|
||||
background: linear-gradient(135deg, #228B22 0%, #32CD32 100%);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
color: #1a1a1a;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(34, 139, 34, 0.3);
|
||||
padding: 12px 28px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
@@ -298,10 +298,10 @@ body {
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-shadow: 0 1px 3px rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
box-shadow:
|
||||
0 4px 15px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
0 4px 15px rgba(34, 139, 34, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 移除按钮颜色动画,保持稳定的可读性 */
|
||||
@@ -326,12 +326,12 @@ body {
|
||||
}
|
||||
|
||||
.query-btn:hover {
|
||||
background: linear-gradient(135deg, #e8e8e8, #d8d8d8);
|
||||
border-color: rgba(0, 0, 0, 0.3);
|
||||
background: linear-gradient(135deg, #1e7e1e, #2eb82e);
|
||||
border-color: rgba(34, 139, 34, 0.5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 8px 25px rgba(0, 0, 0, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
0 8px 25px rgba(34, 139, 34, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🌙 农历信息查询</title>
|
||||
<title>🌙农历信息查询</title>
|
||||
<link rel="stylesheet" href="css/background.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
@@ -48,8 +48,7 @@
|
||||
|
||||
<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="date-selector">
|
||||
|
||||
@@ -287,15 +287,9 @@ function displayLunarInfo(lunarData) {
|
||||
<div class="item-label">本月进度</div>
|
||||
<div class="item-value">${lunarData.stats.percents_formatted.month}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="item-icon">🗓️</div>
|
||||
<div class="item-label">本周第几天</div>
|
||||
<div class="item-value">第${lunarData.stats.week_of_month}周</div>
|
||||
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="item-icon">⏰</div>
|
||||
<div class="item-label">今日进度</div>
|
||||
<div class="item-value">${lunarData.stats.percents_formatted.day}</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,26 @@
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #f0f8e8 0%, #e8f5e8 50%, #d4f4dd 100%);
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
overflow-x: hidden;
|
||||
/* 隐藏滚动条但保留滚动功能 */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
/* 隐藏 Webkit 浏览器的滚动条 */
|
||||
body::-webkit-scrollbar,
|
||||
html::-webkit-scrollbar,
|
||||
*::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 全局隐藏滚动条但保留滚动功能 */
|
||||
html {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -65,7 +81,7 @@ body {
|
||||
|
||||
.logo i {
|
||||
font-size: 48px;
|
||||
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
|
||||
background: linear-gradient(45deg, #228B22, #32CD32);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
@@ -174,7 +190,7 @@ body {
|
||||
|
||||
.card-header i {
|
||||
font-size: 24px;
|
||||
color: #667eea;
|
||||
color: #228B22;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
@@ -202,8 +218,8 @@ body {
|
||||
|
||||
#inputText:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
border-color: #228B22;
|
||||
box-shadow: 0 0 0 3px rgba(34, 139, 34, 0.1);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
@@ -247,14 +263,14 @@ body {
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #228B22 0%, #32CD32 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
box-shadow: 0 4px 15px rgba(34, 139, 34, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
||||
box-shadow: 0 8px 25px rgba(34, 139, 34, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@@ -306,7 +322,7 @@ body {
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4);
|
||||
background: linear-gradient(90deg, #228B22, #32CD32, #90EE90, #98FB98);
|
||||
}
|
||||
|
||||
.result-card:hover {
|
||||
@@ -356,7 +372,7 @@ body {
|
||||
|
||||
.result-value:hover {
|
||||
background: rgba(248, 250, 252, 0.95);
|
||||
border-color: #667eea;
|
||||
border-color: #228B22;
|
||||
}
|
||||
|
||||
.result-value .placeholder {
|
||||
@@ -367,7 +383,7 @@ body {
|
||||
.copy-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #667eea;
|
||||
color: #228B22;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
@@ -377,8 +393,8 @@ body {
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: #5a67d8;
|
||||
background: rgba(34, 139, 34, 0.1);
|
||||
color: #1e7e1e;
|
||||
}
|
||||
|
||||
/* Loading Overlay */
|
||||
@@ -429,7 +445,7 @@ body {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
background: linear-gradient(135deg, #4ecdc4, #44a08d);
|
||||
background: linear-gradient(135deg, #228B22, #32CD32);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 12px;
|
||||
|
||||
@@ -5,12 +5,27 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 隐藏滚动条但保留滚动功能 */
|
||||
html {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar,
|
||||
html::-webkit-scrollbar,
|
||||
*::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #2c3e50;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
background: linear-gradient(135deg, #f0f8e8 0%, #e8f5e8 50%, #d4f4dd 100%);
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
/* 容器布局 */
|
||||
@@ -28,9 +43,9 @@ body {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 40px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #228B22 0%, #32CD32 100%);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
|
||||
box-shadow: 0 10px 30px rgba(34, 139, 34, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -95,9 +110,9 @@ body {
|
||||
|
||||
.password-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
border-color: #228B22;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
|
||||
box-shadow: 0 0 0 4px rgba(34, 139, 34, 0.1);
|
||||
}
|
||||
|
||||
.password-input::placeholder {
|
||||
@@ -140,7 +155,7 @@ body {
|
||||
/* 检测按钮 */
|
||||
.check-btn {
|
||||
width: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(135deg, #228B22 0%, #32CD32 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 18px 32px;
|
||||
@@ -149,7 +164,7 @@ body {
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
|
||||
box-shadow: 0 4px 20px rgba(34, 139, 34, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -160,7 +175,7 @@ body {
|
||||
|
||||
.check-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 25px rgba(102, 126, 234, 0.4);
|
||||
box-shadow: 0 6px 25px rgba(34, 139, 34, 0.4);
|
||||
}
|
||||
|
||||
.check-btn:active {
|
||||
@@ -284,7 +299,7 @@ body {
|
||||
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #ef4444, #f97316, #eab308, #22c55e);
|
||||
background: linear-gradient(90deg, #90EE90, #98FB98, #32CD32, #228B22);
|
||||
border-radius: 6px;
|
||||
width: 0%;
|
||||
transition: width 0.8s ease;
|
||||
@@ -383,13 +398,13 @@ body {
|
||||
}
|
||||
|
||||
.char-type.has-type {
|
||||
background: #dcfce7;
|
||||
border-color: #bbf7d0;
|
||||
color: #166534;
|
||||
background: #f0f8e8;
|
||||
border-color: #d4f4dd;
|
||||
color: #1e7e1e;
|
||||
}
|
||||
|
||||
.char-type.has-type .type-icon {
|
||||
color: #22c55e;
|
||||
color: #228B22;
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
@@ -555,11 +570,11 @@ body {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #22c55e;
|
||||
background: #228B22;
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 20px rgba(34, 197, 94, 0.3);
|
||||
box-shadow: 0 4px 20px rgba(34, 139, 34, 0.3);
|
||||
z-index: 1000;
|
||||
animation: toastSlide 0.3s ease-out;
|
||||
font-weight: 500;
|
||||
@@ -586,11 +601,11 @@ body {
|
||||
}
|
||||
|
||||
.strength-strong {
|
||||
color: #059669 !important;
|
||||
color: #228B22 !important;
|
||||
}
|
||||
|
||||
.strength-very-strong {
|
||||
color: #047857 !important;
|
||||
color: #1e7e1e !important;
|
||||
}
|
||||
|
||||
/* 分数圆圈颜色 */
|
||||
@@ -603,11 +618,11 @@ body {
|
||||
}
|
||||
|
||||
.score-strong {
|
||||
background: conic-gradient(from 0deg, #059669 0deg, #059669 var(--score-deg), #e2e8f0 var(--score-deg), #e2e8f0 360deg) !important;
|
||||
background: conic-gradient(from 0deg, #228B22 0deg, #228B22 var(--score-deg), #e2e8f0 var(--score-deg), #e2e8f0 360deg) !important;
|
||||
}
|
||||
|
||||
.score-very-strong {
|
||||
background: conic-gradient(from 0deg, #047857 0deg, #047857 var(--score-deg), #e2e8f0 var(--score-deg), #e2e8f0 360deg) !important;
|
||||
background: conic-gradient(from 0deg, #1e7e1e 0deg, #1e7e1e var(--score-deg), #e2e8f0 var(--score-deg), #e2e8f0 360deg) !important;
|
||||
}
|
||||
|
||||
/* 平板端适配 (768px - 1024px) */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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%
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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%
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
432
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电影实时票房/css/style.css
Normal file
432
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电影实时票房/css/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
67
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电影实时票房/index.html
Normal file
67
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电影实时票房/index.html
Normal 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>
|
||||
297
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电影实时票房/js/main.js
Normal file
297
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电影实时票房/js/main.js
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
1861
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电影实时票房/返回接口.json
Normal file
1861
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电影实时票房/返回接口.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
414
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电视收视排行/css/style.css
Normal file
414
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电视收视排行/css/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
62
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电视收视排行/index.html
Normal file
62
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电视收视排行/index.html
Normal 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>
|
||||
290
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电视收视排行/js/main.js
Normal file
290
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电视收视排行/js/main.js
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
435
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电视收视排行/返回接口.json
Normal file
435
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼电视收视排行/返回接口.json
Normal 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%"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/* 电影内容 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
393
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼网剧实时热度/css/style.css
Normal file
393
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼网剧实时热度/css/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
62
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼网剧实时热度/index.html
Normal file
62
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼网剧实时热度/index.html
Normal 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>
|
||||
293
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼网剧实时热度/js/main.js
Normal file
293
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼网剧实时热度/js/main.js
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
311
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼网剧实时热度/返回接口.json
Normal file
311
InfoGenie-frontend/public/60sapi/热搜榜单/猫眼网剧实时热度/返回接口.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
25
InfoGenie-frontend/public/aimodelapp/AI中国亲戚称呼计算器/env.js
Normal file
25
InfoGenie-frontend/public/aimodelapp/AI中国亲戚称呼计算器/env.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// 环境配置文件 - AI中国亲戚称呼计算器
|
||||
// 复用 InfoGenie 的全局 ENV_CONFIG,支持独立打开的回退地址
|
||||
|
||||
const DEFAULT_API = (window.ENV_CONFIG && window.ENV_CONFIG.API_URL) || 'http://127.0.0.1:5002';
|
||||
|
||||
window.API_CONFIG = {
|
||||
baseUrl: window.parent?.ENV_CONFIG?.API_URL || DEFAULT_API,
|
||||
endpoints: {
|
||||
kinshipCalculator: '/api/aimodelapp/kinship-calculator'
|
||||
}
|
||||
};
|
||||
|
||||
window.AUTH_CONFIG = {
|
||||
tokenKey: 'token',
|
||||
getToken: () => localStorage.getItem('token'),
|
||||
isAuthenticated: () => !!localStorage.getItem('token')
|
||||
};
|
||||
|
||||
window.APP_CONFIG = {
|
||||
name: 'InfoGenie 中国亲戚称呼计算器',
|
||||
version: '1.0.0',
|
||||
debug: false
|
||||
};
|
||||
|
||||
console.log('中国亲戚称呼计算器 环境配置已加载');
|
||||
59
InfoGenie-frontend/public/aimodelapp/AI中国亲戚称呼计算器/index.html
Normal file
59
InfoGenie-frontend/public/aimodelapp/AI中国亲戚称呼计算器/index.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>中国亲戚称呼计算器</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<header class="app-header">
|
||||
<h1>中国亲戚称呼计算器</h1>
|
||||
<p class="subtitle">输入亲属关系链(如“妈妈的爸爸”、“爸爸的姐姐的儿子”),快速得到标准普通话称呼与各地方言称呼</p>
|
||||
</header>
|
||||
|
||||
<main class="card">
|
||||
<section class="input-section">
|
||||
<label for="relationInput" class="label">亲属关系链</label>
|
||||
<textarea id="relationInput" class="textarea" rows="2" placeholder="例如:妈妈的爸爸"></textarea>
|
||||
|
||||
<div class="hint">
|
||||
· 使用“的”连接每一层关系,例如:
|
||||
<span class="chip" onclick="setExample('妈妈的爸爸')">妈妈的爸爸</span>
|
||||
<span class="chip" onclick="setExample('爸爸的姐姐的儿子')">爸爸的姐姐的儿子</span>
|
||||
<span class="chip" onclick="setExample('妈妈的弟弟的女儿')">妈妈的弟弟的女儿</span>
|
||||
</div>
|
||||
|
||||
<button id="calcBtn" class="button primary">计算称呼</button>
|
||||
<div id="loading" class="loading" style="display:none">正在计算,请稍候…</div>
|
||||
<div id="error" class="error" style="display:none"></div>
|
||||
</section>
|
||||
|
||||
<section id="resultSection" class="result-section" style="display:none">
|
||||
<h2>计算结果</h2>
|
||||
<div class="result-block">
|
||||
<div class="result-title">标准普通话称呼</div>
|
||||
<div id="mandarinTitle" class="result-value"></div>
|
||||
<div class="actions">
|
||||
<button id="copyMandarinBtn" class="button">复制称呼</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-block">
|
||||
<div class="result-title">各地方言称呼</div>
|
||||
<div id="dialectList" class="dialect-list"></div>
|
||||
</div>
|
||||
|
||||
<div id="notesBlock" class="result-block" style="display:none">
|
||||
<div class="result-title">说明</div>
|
||||
<div id="notes" class="notes"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="env.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
135
InfoGenie-frontend/public/aimodelapp/AI中国亲戚称呼计算器/script.js
Normal file
135
InfoGenie-frontend/public/aimodelapp/AI中国亲戚称呼计算器/script.js
Normal file
@@ -0,0 +1,135 @@
|
||||
// 环境与认证在 env.js 中定义
|
||||
|
||||
const relationInput = document.getElementById('relationInput');
|
||||
const calcBtn = document.getElementById('calcBtn');
|
||||
const loadingDiv = document.getElementById('loading');
|
||||
const errorDiv = document.getElementById('error');
|
||||
const resultSection = document.getElementById('resultSection');
|
||||
const mandarinTitleEl = document.getElementById('mandarinTitle');
|
||||
const dialectListEl = document.getElementById('dialectList');
|
||||
const copyMandarinBtn = document.getElementById('copyMandarinBtn');
|
||||
const notesBlock = document.getElementById('notesBlock');
|
||||
const notesEl = document.getElementById('notes');
|
||||
|
||||
function setExample(text) {
|
||||
relationInput.value = text;
|
||||
}
|
||||
window.setExample = setExample;
|
||||
|
||||
function showLoading(show) {
|
||||
loadingDiv.style.display = show ? 'block' : 'none';
|
||||
calcBtn.disabled = show;
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
errorDiv.textContent = msg || '';
|
||||
errorDiv.style.display = msg ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
resultSection.style.display = 'none';
|
||||
mandarinTitleEl.textContent = '';
|
||||
dialectListEl.innerHTML = '';
|
||||
notesBlock.style.display = 'none';
|
||||
notesEl.textContent = '';
|
||||
}
|
||||
|
||||
async function callKinshipAPI(relationChain) {
|
||||
const token = window.AUTH_CONFIG.getToken();
|
||||
if (!token) throw new Error('未登录,请先登录后使用AI功能');
|
||||
|
||||
const url = `${window.API_CONFIG.baseUrl}${window.API_CONFIG.endpoints.kinshipCalculator}`;
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ relation_chain: relationChain })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
if (resp.status === 402) throw new Error('您的萌芽币余额不足,无法使用此功能');
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.error || `API请求失败: ${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
if (!data.success) throw new Error(data.error || 'API响应异常');
|
||||
return data;
|
||||
}
|
||||
|
||||
function renderDialects(dialectTitles) {
|
||||
dialectListEl.innerHTML = '';
|
||||
const order = ['粤语','闽南语','上海话','四川话','东北话','客家话'];
|
||||
const names = order.concat(Object.keys(dialectTitles || {}).filter(k => !order.includes(k)));
|
||||
|
||||
names.forEach(name => {
|
||||
const info = dialectTitles?.[name];
|
||||
if (!info || (!info.title && !info.romanization && !info.notes)) return;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'dialect-item';
|
||||
const title = (info.title || '').toString();
|
||||
const roman = (info.romanization || '').toString();
|
||||
const notes = (info.notes || '').toString();
|
||||
item.innerHTML = `
|
||||
<div class="dialect-name">${name}</div>
|
||||
<div class="dialect-title">${title}</div>
|
||||
${roman ? `<div class="dialect-roman">${roman}</div>` : ''}
|
||||
${notes ? `<div class="dialect-notes">${notes}</div>` : ''}
|
||||
`;
|
||||
dialectListEl.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
async function doCalculate() {
|
||||
const relation = (relationInput.value || '').trim();
|
||||
if (!relation) {
|
||||
showError('请输入亲属关系链');
|
||||
return;
|
||||
}
|
||||
showError('');
|
||||
showLoading(true);
|
||||
clearResults();
|
||||
|
||||
try {
|
||||
const data = await callKinshipAPI(relation);
|
||||
mandarinTitleEl.textContent = data.mandarin_title || '';
|
||||
renderDialects(data.dialect_titles || {});
|
||||
if (data.notes) {
|
||||
notesEl.textContent = data.notes;
|
||||
notesBlock.style.display = 'block';
|
||||
}
|
||||
resultSection.style.display = 'block';
|
||||
} catch (e) {
|
||||
console.error('计算失败:', e);
|
||||
showError(`计算失败: ${e.message}`);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function copyText(text) {
|
||||
try {
|
||||
navigator.clipboard.writeText(text);
|
||||
} catch (e) {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
}
|
||||
|
||||
copyMandarinBtn.addEventListener('click', () => {
|
||||
const t = mandarinTitleEl.textContent || '';
|
||||
if (!t) return;
|
||||
copyText(t);
|
||||
});
|
||||
|
||||
calcBtn.addEventListener('click', doCalculate);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
showError('');
|
||||
});
|
||||
189
InfoGenie-frontend/public/aimodelapp/AI中国亲戚称呼计算器/styles.css
Normal file
189
InfoGenie-frontend/public/aimodelapp/AI中国亲戚称呼计算器/styles.css
Normal file
@@ -0,0 +1,189 @@
|
||||
/* 渐变背景与毛玻璃风格,参考 AI文章排版 */
|
||||
:root {
|
||||
--green: #a8e6cf;
|
||||
--lime: #dcedc1;
|
||||
--dark: #2e7d32;
|
||||
--text: #1b5e20;
|
||||
--muted: #558b2f;
|
||||
--white: #ffffff;
|
||||
--shadow: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: linear-gradient(135deg, var(--green) 0%, var(--lime) 100%);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
text-align: center;
|
||||
margin: 8px 0 16px;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
font-size: 22px;
|
||||
margin: 0;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
background: rgba(255,255,255,0.75);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 24px var(--shadow);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(0,0,0,0.05);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
.textarea:focus {
|
||||
border-color: rgba(46,125,50,0.35);
|
||||
box-shadow: 0 0 0 3px rgba(46,125,50,0.12);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 10px 0 12px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.chip {
|
||||
display: inline-block;
|
||||
background: rgba(255,255,255,0.9);
|
||||
border: 1px solid rgba(46,125,50,0.2);
|
||||
color: var(--dark);
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.chip:hover { filter: brightness(0.98); }
|
||||
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
background: rgba(46,125,50,0.15);
|
||||
color: var(--dark);
|
||||
}
|
||||
.button.primary {
|
||||
background: linear-gradient(135deg, #81c784, #aed581);
|
||||
color: #fff;
|
||||
}
|
||||
.button:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
|
||||
.loading {
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.error {
|
||||
margin-top: 8px;
|
||||
padding: 8px 10px;
|
||||
border-left: 3px solid #e53935;
|
||||
background: rgba(229,57,53,0.08);
|
||||
border-radius: 8px;
|
||||
color: #c62828;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.result-section { margin-top: 14px; }
|
||||
.result-section h2 {
|
||||
font-size: 16px;
|
||||
margin: 0 0 8px;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.result-block {
|
||||
background: rgba(255,255,255,0.9);
|
||||
border: 1px solid rgba(0,0,0,0.05);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.result-title {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.result-value {
|
||||
font-size: 18px;
|
||||
color: var(--text);
|
||||
}
|
||||
.actions { margin-top: 8px; }
|
||||
|
||||
.dialect-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
@media (min-width: 540px) {
|
||||
.dialect-list { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
|
||||
.dialect-item {
|
||||
border: 1px solid rgba(46,125,50,0.18);
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
background: rgba(255,255,255,0.95);
|
||||
}
|
||||
.dialect-item .dialect-name {
|
||||
font-weight: 600;
|
||||
color: var(--dark);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.dialect-item .dialect-title { font-size: 15px; }
|
||||
.dialect-item .dialect-roman { font-size: 12px; color: var(--muted); }
|
||||
.dialect-item .dialect-notes { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
||||
|
||||
.notes { font-size: 13px; color: var(--muted); }
|
||||
|
||||
.app-footer {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
@@ -8,13 +8,31 @@
|
||||
/* 主体样式 - iOS风格 */
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #87CEEB 0%, #98FB98 100%);
|
||||
background: linear-gradient(135deg, #F0FFF0 0%, #98FB98 50%, #90EE90 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
color: #1D1D1F;
|
||||
line-height: 1.47;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/* 隐藏滚动条但保留滚动功能 */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
|
||||
/* 隐藏Webkit浏览器的滚动条 */
|
||||
body::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 全局滚动条隐藏 */
|
||||
* {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
}
|
||||
|
||||
/* 容器样式 - iOS毛玻璃效果 */
|
||||
@@ -81,9 +99,9 @@ body {
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #007AFF;
|
||||
border-color: #32CD32;
|
||||
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(50, 205, 50, 0.1);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
@@ -105,7 +123,7 @@ body {
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: #007AFF;
|
||||
background: #32CD32;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
@@ -114,18 +132,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(50, 205, 50, 0.25);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #0056CC;
|
||||
background: #228B22;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.35);
|
||||
box-shadow: 0 4px 16px rgba(50, 205, 50, 0.35);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
background: #004499;
|
||||
background: #006400;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
@@ -151,7 +169,7 @@ body {
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
color: #007AFF;
|
||||
color: #32CD32;
|
||||
font-style: normal;
|
||||
padding: 24px;
|
||||
font-weight: 500;
|
||||
@@ -161,9 +179,12 @@ body {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
padding: 20px;
|
||||
min-height: 150px;
|
||||
backdrop-filter: blur(10px);
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
@@ -176,15 +197,16 @@ body {
|
||||
|
||||
/* 分组标题样式 - iOS风格 */
|
||||
.convention-group-title {
|
||||
font-size: 1.0625rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin: 20px 0 12px 0;
|
||||
padding: 12px 16px;
|
||||
background: #007AFF;
|
||||
margin: 16px 0 12px 0;
|
||||
padding: 10px 16px;
|
||||
background: #32CD32;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.25);
|
||||
box-shadow: 0 2px 8px rgba(50, 205, 50, 0.25);
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.convention-group-title:first-child {
|
||||
@@ -196,17 +218,21 @@ body {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
margin-bottom: 0;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
backdrop-filter: blur(10px);
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.suggestion-item:hover {
|
||||
border-color: rgba(0, 122, 255, 0.3);
|
||||
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.1);
|
||||
border-color: rgba(50, 205, 50, 0.3);
|
||||
box-shadow: 0 4px 16px rgba(50, 205, 50, 0.1);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
@@ -216,33 +242,35 @@ body {
|
||||
|
||||
.variable-name {
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;
|
||||
font-size: 1.0625rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #1D1D1F;
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.variable-description {
|
||||
font-size: 0.9375rem;
|
||||
font-size: 0.875rem;
|
||||
color: #86868B;
|
||||
line-height: 1.47;
|
||||
line-height: 1.4;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
background: #007AFF;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: #32CD32;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 0.8125rem;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 122, 255, 0.25);
|
||||
box-shadow: 0 1px 3px rgba(50, 205, 50, 0.25);
|
||||
}
|
||||
|
||||
.suggestion-item:hover .copy-btn {
|
||||
@@ -250,7 +278,7 @@ body {
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: #0056CC;
|
||||
background: #228B22;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -314,17 +342,30 @@ body {
|
||||
|
||||
.suggestions-container {
|
||||
padding: 15px;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 12px;
|
||||
padding: 10px;
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
position: static;
|
||||
opacity: 1;
|
||||
margin-top: 8px;
|
||||
margin-top: 6px;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.variable-name {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.variable-description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,16 +383,30 @@ body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.suggestions-container {
|
||||
padding: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.variable-name {
|
||||
font-size: 1rem;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.variable-description {
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.convention-group-title {
|
||||
font-size: 0.9rem;
|
||||
padding: 8px 12px;
|
||||
margin: 12px 0 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
29
InfoGenie-frontend/public/aimodelapp/AI文章排版/env.js
Normal file
29
InfoGenie-frontend/public/aimodelapp/AI文章排版/env.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// 环境配置文件 - AI文章排版
|
||||
// 复用 InfoGenie 的全局 ENV_CONFIG
|
||||
|
||||
// 本地/独立打开页面的API回退地址(优先使用父窗口ENV_CONFIG)
|
||||
const DEFAULT_API = (window.ENV_CONFIG && window.ENV_CONFIG.API_URL) || 'http://127.0.0.1:5002';
|
||||
|
||||
// API配置
|
||||
window.API_CONFIG = {
|
||||
baseUrl: window.parent?.ENV_CONFIG?.API_URL || DEFAULT_API,
|
||||
endpoints: {
|
||||
markdownFormatting: '/api/aimodelapp/markdown_formatting'
|
||||
}
|
||||
};
|
||||
|
||||
// 认证配置
|
||||
window.AUTH_CONFIG = {
|
||||
tokenKey: 'token',
|
||||
getToken: () => localStorage.getItem('token'),
|
||||
isAuthenticated: () => !!localStorage.getItem('token')
|
||||
};
|
||||
|
||||
// 应用配置
|
||||
window.APP_CONFIG = {
|
||||
name: 'InfoGenie AI文章排版',
|
||||
version: '1.0.0',
|
||||
debug: false
|
||||
};
|
||||
|
||||
console.log('AI文章排版 环境配置已加载');
|
||||
92
InfoGenie-frontend/public/aimodelapp/AI文章排版/index.html
Normal file
92
InfoGenie-frontend/public/aimodelapp/AI文章排版/index.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AI文章排版助手</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 class="title">AI文章排版助手</h1>
|
||||
<p class="subtitle">保持原文不变 · 智能转为Markdown并点缀Emoji</p>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="articleText">请输入文章内容:</label>
|
||||
<textarea
|
||||
id="articleText"
|
||||
class="form-input textarea"
|
||||
placeholder="粘贴或输入原文内容,点击开始排版即可生成Markdown,并通过Emoji增强可读性..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group half-width">
|
||||
<label class="form-label" for="emojiStyle">Emoji风格:</label>
|
||||
<select id="emojiStyle" class="form-input select">
|
||||
<option value="balanced">适中(推荐)</option>
|
||||
<option value="light">清爽(少量Emoji)</option>
|
||||
<option value="rich">丰富(较多Emoji)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group half-width">
|
||||
<label class="form-label" for="markdownOption">排版偏好:</label>
|
||||
<select id="markdownOption" class="form-input select">
|
||||
<option value="standard">标准Markdown</option>
|
||||
<option value="compact">紧凑排版</option>
|
||||
<option value="readable">易读增强</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="formatBtn" class="btn">开始排版</button>
|
||||
</div>
|
||||
|
||||
<div class="result-section">
|
||||
<h3 class="result-title">排版结果</h3>
|
||||
<div id="loading" class="loading">正在排版中,请稍候...</div>
|
||||
<div id="resultContainer" class="conversion-container">
|
||||
<div class="placeholder">输入文章后点击“开始排版”,AI将把原文转换为规范的Markdown,并智能添加合适的Emoji</div>
|
||||
</div>
|
||||
|
||||
<div id="previewSection" class="preview-section" style="display:none;">
|
||||
<div class="preview-header">
|
||||
<span class="label">Markdown预览:</span>
|
||||
<button class="copy-btn" id="copyHtmlBtn">复制HTML</button>
|
||||
</div>
|
||||
<div id="markdownPreview" class="markdown-preview"></div>
|
||||
</div>
|
||||
|
||||
<div id="rawSection" class="raw-section" style="display:none;">
|
||||
<div class="raw-header">
|
||||
<span class="label">Markdown源文本:</span>
|
||||
<button class="copy-btn" id="copyMdBtn">复制Markdown</button>
|
||||
</div>
|
||||
<pre id="markdownRaw" class="markdown-raw"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 环境配置与功能脚本 -->
|
||||
<script src="env.js"></script>
|
||||
<!-- Markdown 渲染与安全过滤(CDN) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.9/dist/purify.min.js"></script>
|
||||
<script>
|
||||
// 检查库是否正确加载
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (typeof marked === 'undefined') {
|
||||
console.error('marked库加载失败');
|
||||
document.getElementById('resultContainer').innerHTML = '<div class="placeholder error">Markdown渲染库加载失败,请检查网络连接</div>';
|
||||
}
|
||||
if (typeof DOMPurify === 'undefined') {
|
||||
console.warn('DOMPurify库加载失败,将使用不安全的HTML渲染');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
152
InfoGenie-frontend/public/aimodelapp/AI文章排版/script.js
Normal file
152
InfoGenie-frontend/public/aimodelapp/AI文章排版/script.js
Normal file
@@ -0,0 +1,152 @@
|
||||
// 配置已在 env.js 中定义
|
||||
|
||||
// DOM元素
|
||||
const articleTextInput = document.getElementById('articleText');
|
||||
const emojiStyleSelect = document.getElementById('emojiStyle');
|
||||
const markdownOptionSelect = document.getElementById('markdownOption');
|
||||
const formatBtn = document.getElementById('formatBtn');
|
||||
const loadingDiv = document.getElementById('loading');
|
||||
const resultContainer = document.getElementById('resultContainer');
|
||||
const previewSection = document.getElementById('previewSection');
|
||||
const markdownPreview = document.getElementById('markdownPreview');
|
||||
const rawSection = document.getElementById('rawSection');
|
||||
const markdownRaw = document.getElementById('markdownRaw');
|
||||
const copyMdBtn = document.getElementById('copyMdBtn');
|
||||
const copyHtmlBtn = document.getElementById('copyHtmlBtn');
|
||||
|
||||
// 加载器控制
|
||||
function showLoading(show) {
|
||||
loadingDiv.style.display = show ? 'block' : 'none';
|
||||
formatBtn.disabled = show;
|
||||
}
|
||||
|
||||
// 错误提示
|
||||
function showErrorMessage(msg) {
|
||||
resultContainer.innerHTML = `<div class="placeholder">${msg}</div>`;
|
||||
}
|
||||
|
||||
// 调用后端API
|
||||
async function callBackendAPI(articleText, emojiStyle, markdownOption) {
|
||||
try {
|
||||
const token = window.AUTH_CONFIG.getToken();
|
||||
if (!token) throw new Error('未登录,请先登录后使用AI功能');
|
||||
|
||||
const url = `${window.API_CONFIG.baseUrl}${window.API_CONFIG.endpoints.markdownFormatting}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
article_text: articleText,
|
||||
emoji_style: emojiStyle,
|
||||
markdown_option: markdownOption
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 402) throw new Error('您的萌芽币余额不足,无法使用此功能');
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `API请求失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success && data.formatted_markdown) return data.formatted_markdown;
|
||||
throw new Error(data.error || 'API响应格式异常');
|
||||
} catch (error) {
|
||||
console.error('API调用错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示结果
|
||||
function displayFormattingResult(markdownText) {
|
||||
// 源Markdown
|
||||
markdownRaw.textContent = markdownText || '';
|
||||
rawSection.style.display = markdownText ? 'block' : 'none';
|
||||
|
||||
// 预览渲染(使用marked + DOMPurify)
|
||||
let html = '';
|
||||
try {
|
||||
// 兼容新旧版本的marked库
|
||||
if (typeof marked === 'function') {
|
||||
// 旧版本marked直接调用
|
||||
html = marked(markdownText || '');
|
||||
} else if (marked && typeof marked.parse === 'function') {
|
||||
// 新版本marked使用parse方法
|
||||
html = marked.parse(markdownText || '');
|
||||
} else {
|
||||
throw new Error('marked库未正确加载');
|
||||
}
|
||||
|
||||
// 使用DOMPurify清理HTML(如果可用)
|
||||
const safeHtml = typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(html) : html;
|
||||
markdownPreview.innerHTML = safeHtml;
|
||||
} catch (error) {
|
||||
console.error('Markdown渲染失败:', error);
|
||||
markdownPreview.innerHTML = `<div class="error">Markdown渲染失败: ${error.message}</div>`;
|
||||
}
|
||||
|
||||
previewSection.style.display = markdownText ? 'block' : 'none';
|
||||
|
||||
// 顶部结果容器状态
|
||||
resultContainer.innerHTML = '';
|
||||
resultContainer.classList.add('conversion-result');
|
||||
}
|
||||
|
||||
// 复制功能
|
||||
function copyToClipboard(text) {
|
||||
try {
|
||||
navigator.clipboard.writeText(text);
|
||||
} catch (e) {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
|
||||
copyMdBtn.addEventListener('click', () => copyToClipboard(markdownRaw.textContent || ''));
|
||||
copyHtmlBtn.addEventListener('click', () => copyToClipboard(markdownPreview.innerHTML || ''));
|
||||
|
||||
// 执行排版
|
||||
async function performFormatting() {
|
||||
const articleText = articleTextInput.value.trim();
|
||||
const emojiStyle = emojiStyleSelect.value;
|
||||
const markdownOption = markdownOptionSelect.value;
|
||||
|
||||
if (!articleText) {
|
||||
showErrorMessage('请输入需要排版的文章内容');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading(true);
|
||||
resultContainer.innerHTML = '';
|
||||
previewSection.style.display = 'none';
|
||||
rawSection.style.display = 'none';
|
||||
|
||||
try {
|
||||
const markdown = await callBackendAPI(articleText, emojiStyle, markdownOption);
|
||||
displayFormattingResult(markdown);
|
||||
} catch (error) {
|
||||
console.error('排版失败:', error);
|
||||
showErrorMessage(`排版失败: ${error.message}`);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 事件绑定
|
||||
formatBtn.addEventListener('click', performFormatting);
|
||||
|
||||
// 页面初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
resultContainer.innerHTML = '<div class="placeholder">请输入文章内容,选择Emoji风格与排版偏好,然后点击开始排版</div>';
|
||||
});
|
||||
|
||||
// 导出函数供HTML调用
|
||||
window.performFormatting = performFormatting;
|
||||
window.copyToClipboard = copyToClipboard;
|
||||
84
InfoGenie-frontend/public/aimodelapp/AI文章排版/styles.css
Normal file
84
InfoGenie-frontend/public/aimodelapp/AI文章排版/styles.css
Normal file
@@ -0,0 +1,84 @@
|
||||
/* 全局样式重置 */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
/* 主体样式 - 清新渐变 */
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc8 50%, #f1f8e9 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
color: #2e7d32;
|
||||
line-height: 1.47;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* 容器样式 - 毛玻璃效果 */
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 24px;
|
||||
padding: 32px;
|
||||
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(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.header { text-align: center; margin-bottom: 32px; }
|
||||
.title { font-size: 2.25rem; color: #1b5e20; margin-bottom: 8px; font-weight: 600; letter-spacing: -0.02em; }
|
||||
.subtitle { color: #4caf50; font-size: 1.0625rem; margin-bottom: 24px; font-weight: 400; }
|
||||
|
||||
/* 表单区域 */
|
||||
.form-section { margin-bottom: 32px; }
|
||||
.form-group { margin-bottom: 24px; }
|
||||
.form-row { display: flex; gap: 16px; margin-bottom: 24px; }
|
||||
.half-width { flex: 1; }
|
||||
.form-label { display: block; margin-bottom: 8px; font-weight: 600; color: #2e7d32; }
|
||||
.form-input { width: 100%; border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 12px; padding: 12px 14px; outline: none; background: rgba(255, 255, 255, 0.75); color: #1b5e20; font-size: 1rem; transition: all 0.2s ease; }
|
||||
.form-input:focus { border-color: rgba(76, 175, 80, 0.4); box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.15); }
|
||||
.textarea { min-height: 160px; resize: vertical; line-height: 1.6; }
|
||||
.select { appearance: none; background-image: linear-gradient(135deg, #f1f8e9 0%, #e8f5e9 100%); }
|
||||
|
||||
/* 操作按钮 */
|
||||
.btn { width: 100%; padding: 14px 18px; border: none; border-radius: 14px; font-weight: 600; font-size: 1.0625rem; color: #fff; background: linear-gradient(135deg, #43a047 0%, #66bb6a 50%, #81c784 100%); box-shadow: 0 4px 16px rgba(76, 175, 80, 0.3), 0 2px 8px rgba(76, 175, 80, 0.2); cursor: pointer; transition: all 0.2s ease; }
|
||||
.btn:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(76, 175, 80, 0.35); }
|
||||
.btn:active { transform: translateY(0); background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%); }
|
||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; background: #86868B; }
|
||||
|
||||
/* 结果区域 */
|
||||
.result-section { margin-top: 32px; }
|
||||
.result-title { font-size: 1.25rem; color: #1b5e20; margin-bottom: 16px; text-align: center; font-weight: 600; }
|
||||
.loading { display: none; text-align: center; color: #4caf50; padding: 24px; font-weight: 500; }
|
||||
.conversion-container { background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 16px; padding: 24px; min-height: 140px; backdrop-filter: blur(10px); }
|
||||
.placeholder { text-align: center; color: #86868B; padding: 32px 20px; font-weight: 400; }
|
||||
.placeholder.error { color: #d32f2f; background: rgba(244, 67, 54, 0.1); border: 1px solid rgba(244, 67, 54, 0.2); border-radius: 8px; }
|
||||
.error { color: #d32f2f; background: rgba(244, 67, 54, 0.1); padding: 12px; border-radius: 8px; border: 1px solid rgba(244, 67, 54, 0.2); }
|
||||
|
||||
/* 预览与原文区域 */
|
||||
.preview-section, .raw-section { margin-top: 24px; }
|
||||
.preview-header, .raw-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||||
.preview-header .label, .raw-header .label { font-weight: 600; color: #2e7d32; font-size: 1rem; }
|
||||
.copy-btn { padding: 6px 10px; border: none; border-radius: 10px; font-weight: 600; font-size: 0.9375rem; color: #fff; background: linear-gradient(135deg, #4caf50 0%, #81c784 100%); box-shadow: 0 2px 8px rgba(76, 175, 80, 0.25); cursor: pointer; }
|
||||
.copy-btn:hover { filter: brightness(1.05); }
|
||||
|
||||
.markdown-preview { background: rgba(255, 255, 255, 0.9); border: 1px solid rgba(0, 0, 0, 0.06); border-radius: 12px; padding: 20px; color: #2e7d32; line-height: 1.8; }
|
||||
.markdown-raw { background: rgba(255, 255, 255, 0.85); border: 1px solid rgba(0, 0, 0, 0.06); border-radius: 12px; padding: 16px; color: #1b5e20; font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace; font-size: 0.9375rem; white-space: pre-wrap; word-break: break-word; }
|
||||
|
||||
/* Markdown渲染细节 */
|
||||
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3 { color: #1b5e20; margin: 10px 0; }
|
||||
.markdown-preview p { margin: 10px 0; }
|
||||
.markdown-preview ul, .markdown-preview ol { padding-left: 24px; margin: 10px 0; }
|
||||
.markdown-preview blockquote { border-left: 4px solid rgba(76, 175, 80, 0.4); padding-left: 12px; color: #4caf50; background: rgba(76, 175, 80, 0.08); border-radius: 6px; }
|
||||
.markdown-preview code { background: rgba(0, 0, 0, 0.06); padding: 2px 6px; border-radius: 6px; font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace; }
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 480px) {
|
||||
.container { padding: 18px; border-radius: 18px; }
|
||||
.title { font-size: 1.75rem; }
|
||||
.subtitle { font-size: 0.95rem; }
|
||||
.form-row { flex-direction: column; gap: 12px; }
|
||||
.textarea { min-height: 200px; }
|
||||
.btn { font-size: 1rem; padding: 12px 16px; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
BIN
InfoGenie-frontend/public/assets/fonts/kaiti1.ttf
Normal file
BIN
InfoGenie-frontend/public/assets/fonts/kaiti1.ttf
Normal file
Binary file not shown.
BIN
InfoGenie-frontend/public/assets/圆角-logo.png
Normal file
BIN
InfoGenie-frontend/public/assets/圆角-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -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-desc">🎨 一个跨平台的多功能聚合应用(´。• ω •。`) 💬</div>
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-logo-container">
|
||||
<img class="loading-logo" src="%PUBLIC_URL%/assets/logo.png" alt="万象口袋" />
|
||||
</div>
|
||||
<div class="loading-text-container">
|
||||
<strong class="loading-text">万象口袋</strong>
|
||||
</div>
|
||||
<strong class="loading-desc">🎨 一个跨平台的多功能聚合应用(´。• ω •。`) 💬</strong>
|
||||
<div class="loading-spinner-container">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
146
InfoGenie-frontend/public/smallgame/2048/controls.js
vendored
146
InfoGenie-frontend/public/smallgame/2048/controls.js
vendored
@@ -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;
|
||||
@@ -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;
|
||||
@@ -30,6 +30,122 @@ class Game2048 {
|
||||
// 开始计时
|
||||
this.startTimer();
|
||||
}
|
||||
|
||||
// 依据分数计算权重(0.1 ~ 0.95)
|
||||
calculateWeightByScore(score) {
|
||||
const w = score / 4000; // 4000分约接近满权重
|
||||
return Math.max(0.1, Math.min(0.95, w));
|
||||
}
|
||||
|
||||
// 按权重偏向生成0~10的随机整数,权重越高越偏向更大值
|
||||
biasedRandomInt(maxInclusive, weight) {
|
||||
const rand = Math.random();
|
||||
const biased = Math.pow(rand, 1 - weight); // weight越大,biased越接近1
|
||||
const val = Math.floor(biased * (maxInclusive + 1));
|
||||
return Math.max(0, Math.min(maxInclusive, val));
|
||||
}
|
||||
|
||||
// 附加结束信息到界面
|
||||
appendEndInfo(text, type = 'info') {
|
||||
const message = document.getElementById('game-message');
|
||||
if (!message) return;
|
||||
const info = document.createElement('div');
|
||||
info.style.marginTop = '10px';
|
||||
info.style.fontSize = '16px';
|
||||
info.style.color = type === 'error' ? '#d9534f' : (type === 'success' ? '#28a745' : '#776e65');
|
||||
info.textContent = text;
|
||||
message.appendChild(info);
|
||||
}
|
||||
|
||||
// 游戏结束时尝试给当前登录账户加“萌芽币”
|
||||
async tryAwardCoinsOnGameOver() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
this.appendEndInfo('未登录,无法获得萌芽币');
|
||||
return;
|
||||
}
|
||||
|
||||
let email = null;
|
||||
try {
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
const userObj = JSON.parse(userStr);
|
||||
email = userObj && (userObj.email || userObj['邮箱']);
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略解析错误
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
this.appendEndInfo('未找到账户信息(email),无法加币', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据分数计算权重与概率
|
||||
const weight = this.calculateWeightByScore(this.score);
|
||||
let awardProbability = weight; // 默认用权重作为概率
|
||||
let guaranteed = false;
|
||||
|
||||
// 分数≥500时必定触发奖励
|
||||
if (this.score >= 500) {
|
||||
awardProbability = 1;
|
||||
guaranteed = true;
|
||||
}
|
||||
|
||||
const roll = Math.random();
|
||||
if (roll > awardProbability) {
|
||||
this.appendEndInfo('本局未获得萌芽币');
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成0~10随机萌芽币数量,权重越高越偏向更大值
|
||||
let coins = this.biasedRandomInt(5, weight);
|
||||
// 保底至少 1 个(仅当分数≥500时)
|
||||
if (guaranteed) {
|
||||
coins = Math.max(1, coins);
|
||||
}
|
||||
coins = Math.max(0, Math.min(10, coins));
|
||||
|
||||
if (coins <= 0) {
|
||||
this.appendEndInfo('本局未获得萌芽币');
|
||||
return;
|
||||
}
|
||||
|
||||
// 后端 API base URL(从父窗口ENV_CONFIG获取,回退到本地默认)
|
||||
const apiBase = (window.parent && window.parent.ENV_CONFIG && window.parent.ENV_CONFIG.API_URL)
|
||||
? window.parent.ENV_CONFIG.API_URL
|
||||
: ((window.ENV_CONFIG && window.ENV_CONFIG.API_URL) ? window.ENV_CONFIG.API_URL : 'http://127.0.0.1:5002');
|
||||
|
||||
const resp = await fetch(`${apiBase}/api/user/add-coins`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ email, amount: coins })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
const msg = err && (err.message || err.error) ? (err.message || err.error) : `请求失败(${resp.status})`;
|
||||
this.appendEndInfo(`加币失败:${msg}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
if (data && data.success) {
|
||||
const newCoins = data.data && data.data.new_coins;
|
||||
this.appendEndInfo(`恭喜获得 ${coins} 个萌芽币!当前余额:${newCoins}`, 'success');
|
||||
} else {
|
||||
const msg = (data && (data.message || data.error)) || '未知错误';
|
||||
this.appendEndInfo(`加币失败:${msg}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加币流程发生错误:', e);
|
||||
this.appendEndInfo('加币失败:网络或系统错误', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
initializeGrid() {
|
||||
this.grid = [];
|
||||
@@ -98,7 +214,6 @@ class Game2048 {
|
||||
|
||||
// 更新分数
|
||||
document.getElementById('score').textContent = this.score;
|
||||
document.getElementById('best-score').textContent = this.bestScore;
|
||||
|
||||
// 更新统计数据显示
|
||||
if (window.gameStats) {
|
||||
@@ -316,6 +431,16 @@ class Game2048 {
|
||||
message.className = 'game-message game-won';
|
||||
message.style.display = 'flex';
|
||||
message.querySelector('p').textContent = '你赢了!';
|
||||
|
||||
// 胜利也尝试加币(异步,不阻塞UI)
|
||||
this.tryAwardCoinsOnGameOver();
|
||||
|
||||
// 显示最终统计
|
||||
setTimeout(() => {
|
||||
if (window.gameStats) {
|
||||
window.gameStats.showFinalStats();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
showGameOver() {
|
||||
@@ -324,6 +449,16 @@ class Game2048 {
|
||||
message.style.display = 'flex';
|
||||
message.querySelector('p').textContent = '游戏结束!';
|
||||
|
||||
// 渲染排行榜
|
||||
try {
|
||||
this.renderLeaderboard();
|
||||
} catch (e) {
|
||||
console.error('渲染排行榜时发生错误:', e);
|
||||
}
|
||||
|
||||
// 尝试加币(异步,不阻塞UI)
|
||||
this.tryAwardCoinsOnGameOver();
|
||||
|
||||
// 显示最终统计
|
||||
setTimeout(() => {
|
||||
if (window.gameStats) {
|
||||
@@ -359,9 +494,7 @@ class Game2048 {
|
||||
this.startTimer();
|
||||
}
|
||||
|
||||
keepPlaying() {
|
||||
document.getElementById('game-message').style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
startTimer() {
|
||||
this.stats.startTime = Date.now();
|
||||
@@ -380,29 +513,100 @@ class Game2048 {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 构建并渲染排行榜
|
||||
renderLeaderboard() {
|
||||
const container = document.getElementById('leaderboard');
|
||||
if (!container) return;
|
||||
|
||||
// 生成当前玩家数据
|
||||
const today = this.formatDate(new Date());
|
||||
const currentPlayer = {
|
||||
"名称": "我",
|
||||
"账号": "guest-local",
|
||||
"分数": this.score,
|
||||
"时间": today,
|
||||
_current: true
|
||||
};
|
||||
|
||||
// 合并并排序数据(分数由高到低)
|
||||
const baseData = (typeof playerdata !== 'undefined' && Array.isArray(playerdata)) ? playerdata : [];
|
||||
const merged = [...baseData.map(d => ({...d})), currentPlayer]
|
||||
.sort((a, b) => (b["分数"] || 0) - (a["分数"] || 0));
|
||||
|
||||
// 计算当前玩家排名
|
||||
const currentIndex = merged.findIndex(d => d._current);
|
||||
const rank = currentIndex >= 0 ? currentIndex + 1 : '-';
|
||||
|
||||
// 仅展示前10条
|
||||
const topN = merged.slice(0, 10);
|
||||
|
||||
// 生成 HTML
|
||||
const summaryHtml = `
|
||||
<div class="leaderboard-summary">
|
||||
<span>本局分数:<strong>${this.score}</strong></span>
|
||||
<span>用时:<strong>${this.stats.gameTime}</strong> 秒</span>
|
||||
<span>你的排名:<strong>${rank}</strong></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const headerHtml = `
|
||||
<div class="leaderboard-header">
|
||||
<div class="leaderboard-col rank">排名</div>
|
||||
<div class="leaderboard-col name">名称</div>
|
||||
<div class="leaderboard-col score">分数</div>
|
||||
<div class="leaderboard-col time">日期</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const rowsHtml = topN.map((d, i) => {
|
||||
const isCurrent = !!d._current;
|
||||
const rowClass = `leaderboard-row${isCurrent ? ' current' : ''}`;
|
||||
return `
|
||||
<div class="${rowClass}">
|
||||
<div class="leaderboard-col rank">${i + 1}</div>
|
||||
<div class="leaderboard-col name">${this.escapeHtml(d["名称"] || '未知')}</div>
|
||||
<div class="leaderboard-col score">${d["分数"] ?? 0}</div>
|
||||
<div class="leaderboard-col time">${this.escapeHtml(d["时间"] || '-')}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="leaderboard-title">排行榜</div>
|
||||
${summaryHtml}
|
||||
<div class="leaderboard-table">
|
||||
${headerHtml}
|
||||
<div class="leaderboard-body">${rowsHtml}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 工具:日期格式化 YYYY-MM-DD
|
||||
formatDate(date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
// 工具:简单转义以避免 XSS
|
||||
escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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 +616,7 @@ let game;
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
game = new Game2048();
|
||||
|
||||
// 监听分数变化以更新最高分
|
||||
const originalUpdateDisplay = game.updateDisplay.bind(game);
|
||||
game.updateDisplay = function() {
|
||||
originalUpdateDisplay();
|
||||
this.updateBestScore();
|
||||
};
|
||||
|
||||
|
||||
// 导出游戏实例供其他模块使用
|
||||
window.game2048 = game;
|
||||
|
||||
20
InfoGenie-frontend/public/smallgame/2048/gamedata.js
Normal file
20
InfoGenie-frontend/public/smallgame/2048/gamedata.js
Normal 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"
|
||||
}
|
||||
]
|
||||
@@ -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,16 @@
|
||||
<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 id="leaderboard" class="leaderboard" aria-live="polite"></div>
|
||||
<div class="lower">
|
||||
<a class="keep-playing-button" id="keep-playing">继续游戏</a>
|
||||
<a class="retry-button" id="retry-btn">重新开始</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,96 +61,13 @@
|
||||
</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">×</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="gamedata.js"></script>
|
||||
<script src="game-logic.js"></script>
|
||||
<script src="controls.js"></script>
|
||||
<script src="statistics.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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,101 @@ body {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.keep-playing-button:hover,
|
||||
/* 排行榜样式(与 2048 主题一致) */
|
||||
.leaderboard {
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
background: rgba(250, 248, 239, 0.95); /* #faf8ef */
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
color: #776e65;
|
||||
}
|
||||
|
||||
.leaderboard-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #8f7a66;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.leaderboard-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
color: #8f7a66;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaderboard-summary strong {
|
||||
color: #8f7a66;
|
||||
}
|
||||
|
||||
.leaderboard-table {
|
||||
border: 1px solid rgba(187, 173, 160, 0.3); /* #bbada0 */
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: rgba(238, 228, 218, 0.4); /* #eee4da */
|
||||
}
|
||||
|
||||
.leaderboard-header,
|
||||
.leaderboard-row {
|
||||
display: grid;
|
||||
grid-template-columns: 64px 1fr 90px 120px; /* 排名/名称/分数/日期 */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.leaderboard-header {
|
||||
background: #eee4da;
|
||||
color: #776e65;
|
||||
font-weight: 700;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid rgba(187, 173, 160, 0.3);
|
||||
}
|
||||
|
||||
.leaderboard-body {
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
background: rgba(238, 228, 218, 0.25);
|
||||
}
|
||||
|
||||
.leaderboard-row {
|
||||
padding: 8px 10px;
|
||||
border-top: 1px solid rgba(187, 173, 160, 0.15);
|
||||
}
|
||||
.leaderboard-row:nth-child(odd) {
|
||||
background: rgba(238, 228, 218, 0.22);
|
||||
}
|
||||
.leaderboard-row.current {
|
||||
background: #f3e9d4;
|
||||
box-shadow: inset 0 0 0 2px rgba(143, 122, 102, 0.35);
|
||||
}
|
||||
|
||||
.leaderboard-col.rank {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
color: #8f7a66;
|
||||
}
|
||||
.leaderboard-col.score {
|
||||
text-align: right;
|
||||
font-weight: 700;
|
||||
}
|
||||
.leaderboard-col.time {
|
||||
text-align: right;
|
||||
color: #776e65;
|
||||
}
|
||||
|
||||
.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 +378,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 +417,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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,338 +1,96 @@
|
||||
// 游戏统计和成就系统
|
||||
class GameStats {
|
||||
constructor() {
|
||||
this.achievements = [
|
||||
{
|
||||
id: 'first_game',
|
||||
name: '初次体验',
|
||||
description: '完成第一次游戏',
|
||||
condition: (stats) => true
|
||||
},
|
||||
{
|
||||
id: 'score_1000',
|
||||
name: '小试牛刀',
|
||||
description: '单局得分达到1000分',
|
||||
condition: (stats) => stats.score >= 1000
|
||||
},
|
||||
{
|
||||
id: 'score_5000',
|
||||
name: '游戏达人',
|
||||
description: '单局得分达到5000分',
|
||||
condition: (stats) => stats.score >= 5000
|
||||
},
|
||||
{
|
||||
id: 'score_10000',
|
||||
name: '方块大师',
|
||||
description: '单局得分达到10000分',
|
||||
condition: (stats) => stats.score >= 10000
|
||||
},
|
||||
{
|
||||
id: 'level_5',
|
||||
name: '步步高升',
|
||||
description: '达到第5级',
|
||||
condition: (stats) => stats.level >= 5
|
||||
},
|
||||
{
|
||||
id: 'level_10',
|
||||
name: '速度之王',
|
||||
description: '达到第10级',
|
||||
condition: (stats) => stats.level >= 10
|
||||
},
|
||||
{
|
||||
id: 'lines_50',
|
||||
name: '消除专家',
|
||||
description: '累计消除50行',
|
||||
condition: (stats) => stats.lines >= 50
|
||||
},
|
||||
{
|
||||
id: 'lines_100',
|
||||
name: '清理大师',
|
||||
description: '累计消除100行',
|
||||
condition: (stats) => stats.lines >= 100
|
||||
},
|
||||
{
|
||||
id: 'tetris',
|
||||
name: 'Tetris!',
|
||||
description: '一次消除4行',
|
||||
condition: (stats) => stats.maxCombo >= 4
|
||||
},
|
||||
{
|
||||
id: 'time_10min',
|
||||
name: '持久战士',
|
||||
description: '单局游戏时间超过10分钟',
|
||||
condition: (stats) => stats.playTime >= 600000
|
||||
},
|
||||
{
|
||||
id: 'efficiency',
|
||||
name: '效率专家',
|
||||
description: '平均每分钟得分超过500',
|
||||
condition: (stats) => stats.avgScore >= 500
|
||||
}
|
||||
];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const playAgainBtn = document.getElementById('playAgainBtn');
|
||||
playAgainBtn.addEventListener('click', () => {
|
||||
this.hideStats();
|
||||
game.restart();
|
||||
});
|
||||
}
|
||||
|
||||
showStats(gameData) {
|
||||
const playTimeMinutes = gameData.playTime / 60000;
|
||||
const avgScore = playTimeMinutes > 0 ? Math.round(gameData.score / playTimeMinutes) : 0;
|
||||
|
||||
const stats = {
|
||||
...gameData,
|
||||
avgScore: avgScore
|
||||
};
|
||||
|
||||
// 更新统计显示
|
||||
document.getElementById('finalScore').textContent = stats.score.toLocaleString();
|
||||
document.getElementById('finalLevel').textContent = stats.level;
|
||||
document.getElementById('finalLines').textContent = stats.lines;
|
||||
document.getElementById('playTime').textContent = this.formatTime(stats.playTime);
|
||||
document.getElementById('maxCombo').textContent = stats.maxCombo;
|
||||
document.getElementById('avgScore').textContent = stats.avgScore;
|
||||
|
||||
// 检查成就
|
||||
const achievement = this.checkAchievements(stats);
|
||||
this.displayAchievement(achievement);
|
||||
|
||||
// 显示统计界面
|
||||
document.getElementById('gameStats').style.display = 'flex';
|
||||
document.getElementById('gameStats').classList.add('fade-in');
|
||||
}
|
||||
|
||||
hideStats() {
|
||||
document.getElementById('gameStats').style.display = 'none';
|
||||
document.getElementById('gameStats').classList.remove('fade-in');
|
||||
}
|
||||
|
||||
formatTime(milliseconds) {
|
||||
const seconds = Math.floor(milliseconds / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
checkAchievements(stats) {
|
||||
// 获取已获得的成就
|
||||
const earnedAchievements = this.getEarnedAchievements();
|
||||
|
||||
// 检查新成就
|
||||
for (let achievement of this.achievements) {
|
||||
if (!earnedAchievements.includes(achievement.id) &&
|
||||
achievement.condition(stats)) {
|
||||
|
||||
// 保存新成就
|
||||
this.saveAchievement(achievement.id);
|
||||
return achievement;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
displayAchievement(achievement) {
|
||||
const achievementEl = document.getElementById('achievement');
|
||||
|
||||
if (achievement) {
|
||||
achievementEl.innerHTML = `
|
||||
🏆 <strong>成就解锁!</strong><br>
|
||||
<strong>${achievement.name}</strong><br>
|
||||
${achievement.description}
|
||||
`;
|
||||
achievementEl.classList.add('pulse');
|
||||
} else {
|
||||
// 显示随机鼓励话语
|
||||
const encouragements = [
|
||||
'继续努力,你会变得更强!',
|
||||
'每一次游戏都是进步的机会!',
|
||||
'方块世界需要你的智慧!',
|
||||
'熟能生巧,加油!',
|
||||
'下一局一定会更好!',
|
||||
'坚持就是胜利!',
|
||||
'你的反应速度在提升!',
|
||||
'策略思维正在增强!'
|
||||
];
|
||||
|
||||
const randomEncouragement = encouragements[Math.floor(Math.random() * encouragements.length)];
|
||||
achievementEl.innerHTML = `💪 ${randomEncouragement}`;
|
||||
achievementEl.classList.remove('pulse');
|
||||
}
|
||||
}
|
||||
|
||||
getEarnedAchievements() {
|
||||
const saved = localStorage.getItem('tetris_achievements');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
}
|
||||
|
||||
saveAchievement(achievementId) {
|
||||
const earned = this.getEarnedAchievements();
|
||||
if (!earned.includes(achievementId)) {
|
||||
earned.push(achievementId);
|
||||
localStorage.setItem('tetris_achievements', JSON.stringify(earned));
|
||||
}
|
||||
}
|
||||
|
||||
// 获取历史最佳记录
|
||||
getBestStats() {
|
||||
const saved = localStorage.getItem('tetris_best_stats');
|
||||
return saved ? JSON.parse(saved) : {
|
||||
score: 0,
|
||||
level: 0,
|
||||
lines: 0,
|
||||
maxCombo: 0
|
||||
};
|
||||
}
|
||||
|
||||
// 保存最佳记录
|
||||
saveBestStats(stats) {
|
||||
const best = this.getBestStats();
|
||||
let updated = false;
|
||||
|
||||
if (stats.score > best.score) {
|
||||
best.score = stats.score;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (stats.level > best.level) {
|
||||
best.level = stats.level;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (stats.lines > best.lines) {
|
||||
best.lines = stats.lines;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (stats.maxCombo > best.maxCombo) {
|
||||
best.maxCombo = stats.maxCombo;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
localStorage.setItem('tetris_best_stats', JSON.stringify(best));
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
// 显示排行榜
|
||||
showLeaderboard() {
|
||||
const best = this.getBestStats();
|
||||
const earned = this.getEarnedAchievements();
|
||||
|
||||
console.log('最佳记录:', best);
|
||||
console.log('已获得成就:', earned.length + '/' + this.achievements.length);
|
||||
}
|
||||
}
|
||||
// 游戏结束排行榜展示
|
||||
const gameStats = {
|
||||
showStats({ score, playTime }) {
|
||||
// 将毫秒转为 mm:ss
|
||||
const formatDuration = (ms) => {
|
||||
const totalSec = Math.max(0, Math.floor(ms / 1000));
|
||||
const m = String(Math.floor(totalSec / 60)).padStart(2, '0');
|
||||
const s = String(totalSec % 60).padStart(2, '0');
|
||||
return `${m}:${s}`;
|
||||
};
|
||||
|
||||
// 高级特效系统
|
||||
class GameEffects {
|
||||
constructor(game) {
|
||||
this.game = game;
|
||||
this.particles = [];
|
||||
this.effects = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 创建特效canvas
|
||||
this.effectsCanvas = document.createElement('canvas');
|
||||
this.effectsCanvas.width = this.game.canvas.width;
|
||||
this.effectsCanvas.height = this.game.canvas.height;
|
||||
this.effectsCanvas.style.position = 'absolute';
|
||||
this.effectsCanvas.style.top = '0';
|
||||
this.effectsCanvas.style.left = '0';
|
||||
this.effectsCanvas.style.pointerEvents = 'none';
|
||||
this.effectsCanvas.style.zIndex = '10';
|
||||
|
||||
this.effectsCtx = this.effectsCanvas.getContext('2d');
|
||||
|
||||
// 将特效canvas添加到游戏板容器中
|
||||
this.game.canvas.parentElement.style.position = 'relative';
|
||||
this.game.canvas.parentElement.appendChild(this.effectsCanvas);
|
||||
}
|
||||
|
||||
// 行消除特效
|
||||
lineCleared(row) {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
this.particles.push({
|
||||
x: Math.random() * this.game.canvas.width,
|
||||
y: row * this.game.CELL_SIZE + this.game.CELL_SIZE / 2,
|
||||
vx: (Math.random() - 0.5) * 10,
|
||||
vy: (Math.random() - 0.5) * 10,
|
||||
life: 1,
|
||||
decay: 0.02,
|
||||
color: `hsl(${Math.random() * 360}, 100%, 50%)`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 方块锁定特效
|
||||
pieceLocked(piece) {
|
||||
const centerX = (piece.x + piece.matrix[0].length / 2) * this.game.CELL_SIZE;
|
||||
const centerY = (piece.y + piece.matrix.length / 2) * this.game.CELL_SIZE;
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
this.particles.push({
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
vx: (Math.random() - 0.5) * 8,
|
||||
vy: (Math.random() - 0.5) * 8,
|
||||
life: 0.8,
|
||||
decay: 0.03,
|
||||
color: piece.color
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新特效
|
||||
update() {
|
||||
// 更新粒子
|
||||
for (let i = this.particles.length - 1; i >= 0; i--) {
|
||||
const particle = this.particles[i];
|
||||
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
particle.life -= particle.decay;
|
||||
|
||||
if (particle.life <= 0) {
|
||||
this.particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制特效
|
||||
draw() {
|
||||
this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height);
|
||||
|
||||
// 绘制粒子
|
||||
for (let particle of this.particles) {
|
||||
this.effectsCtx.save();
|
||||
this.effectsCtx.globalAlpha = particle.life;
|
||||
this.effectsCtx.fillStyle = particle.color;
|
||||
this.effectsCtx.beginPath();
|
||||
this.effectsCtx.arc(particle.x, particle.y, 3, 0, Math.PI * 2);
|
||||
this.effectsCtx.fill();
|
||||
this.effectsCtx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 构造排行榜数据(模拟),将当前成绩与 gamedata.js 合并
|
||||
const todayStr = (() => {
|
||||
const d = new Date();
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
})();
|
||||
|
||||
// 创建统计系统实例
|
||||
const gameStats = new GameStats();
|
||||
// 当前玩家信息(可根据实际项目替换为真实用户)
|
||||
const currentEntry = {
|
||||
名称: localStorage.getItem('tetris_player_name') || '我',
|
||||
账号: localStorage.getItem('tetris_player_account') || 'guest@local',
|
||||
分数: score,
|
||||
时间: formatDuration(playTime), // 排行榜展示“游戏时长”
|
||||
isCurrent: true,
|
||||
};
|
||||
|
||||
// 在适当的地方创建特效系统
|
||||
// const gameEffects = new GameEffects(game);
|
||||
// 注意:在浏览器中,使用 const 声明的全局变量不会挂载到 window 上
|
||||
// 因此这里直接使用 playerdata,而不是 window.playerdata
|
||||
const baseData = (typeof playerdata !== 'undefined' && Array.isArray(playerdata)) ? playerdata : [];
|
||||
|
||||
// 为基础数据模拟“游戏时长”(mm:ss),以满足展示需求
|
||||
const simulateDuration = (scoreVal) => {
|
||||
const sec = Math.max(30, Math.min(30 * 60, Math.round((Number(scoreVal) || 0) * 1.2)));
|
||||
return formatDuration(sec * 1000);
|
||||
};
|
||||
|
||||
const merged = [...baseData.map((d) => ({
|
||||
...d,
|
||||
// 使用已有分数推导一个模拟时长
|
||||
时间: simulateDuration(d.分数),
|
||||
isCurrent: false,
|
||||
})), currentEntry]
|
||||
.sort((a, b) => (b.分数 || 0) - (a.分数 || 0));
|
||||
|
||||
// 3) 渲染排行榜(取前10)
|
||||
const tbody = document.getElementById('leaderboardBody');
|
||||
tbody.innerHTML = '';
|
||||
const topN = merged.slice(0, 10);
|
||||
topN.forEach((item, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
if (item.isCurrent) {
|
||||
tr.classList.add('current-row');
|
||||
}
|
||||
const rankCell = document.createElement('td');
|
||||
const nameCell = document.createElement('td');
|
||||
const scoreCell = document.createElement('td');
|
||||
const timeCell = document.createElement('td');
|
||||
|
||||
const rankBadge = document.createElement('span');
|
||||
rankBadge.className = 'rank-badge';
|
||||
rankBadge.textContent = String(idx + 1);
|
||||
rankCell.appendChild(rankBadge);
|
||||
|
||||
nameCell.textContent = item.名称 || '未知';
|
||||
scoreCell.textContent = item.分数 || 0;
|
||||
timeCell.textContent = item.时间 || formatDuration(playTime);
|
||||
|
||||
tr.appendChild(rankCell);
|
||||
tr.appendChild(nameCell);
|
||||
tr.appendChild(scoreCell);
|
||||
tr.appendChild(timeCell);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// 4) 展示排行榜界面
|
||||
const statsEl = document.getElementById('gameStats');
|
||||
statsEl.style.display = 'flex';
|
||||
|
||||
// 5) 再玩一次按钮
|
||||
const playAgainBtn = document.getElementById('playAgainBtn');
|
||||
if (playAgainBtn) {
|
||||
playAgainBtn.onclick = () => {
|
||||
statsEl.style.display = 'none';
|
||||
if (window.game && typeof window.game.restart === 'function') {
|
||||
window.game.restart();
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 暴露到全局
|
||||
window.gameStats = gameStats;
|
||||
20
InfoGenie-frontend/public/smallgame/俄罗斯方块/gamedata.js
Normal file
20
InfoGenie-frontend/public/smallgame/俄罗斯方块/gamedata.js
Normal 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"
|
||||
}
|
||||
]
|
||||
@@ -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>
|
||||
|
||||
@@ -85,40 +61,32 @@
|
||||
<!-- 游戏结束统计界面 -->
|
||||
<div class="game-stats" id="gameStats">
|
||||
<div class="stats-content">
|
||||
<h2>游戏结束</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">最终分数</span>
|
||||
<span class="stat-value" id="finalScore">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">达到等级</span>
|
||||
<span class="stat-value" id="finalLevel">1</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">消除行数</span>
|
||||
<span class="stat-value" id="finalLines">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">游戏时长</span>
|
||||
<span class="stat-value" id="playTime">00:00</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">单次消除最大行数</span>
|
||||
<span class="stat-value" id="maxCombo">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">平均每分钟分数</span>
|
||||
<span class="stat-value" id="avgScore">0</span>
|
||||
<h2>游戏结束排行榜</h2>
|
||||
<!-- 排行榜 -->
|
||||
<div class="leaderboard" id="leaderboard">
|
||||
<div class="leaderboard-title">本局排行榜</div>
|
||||
<div class="leaderboard-wrap">
|
||||
<table class="leaderboard-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>排名</th>
|
||||
<th>名称</th>
|
||||
<th>分数</th>
|
||||
<th>游戏时长</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="leaderboardBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="leaderboard-tip">仅显示前10名;“游戏时长”为模拟数据,已与您的成绩合并</div>
|
||||
</div>
|
||||
<div class="achievement" id="achievement"></div>
|
||||
<button class="game-btn" id="playAgainBtn">再玩一次</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="tetris.js"></script>
|
||||
<script src="game-controls.js"></script>
|
||||
<script src="gamedata.js"></script>
|
||||
<script src="game-stats.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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,85 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 8px rgba(46, 125, 50, 0.3);
|
||||
}
|
||||
|
||||
/* 排行榜样式 */
|
||||
.leaderboard {
|
||||
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%);
|
||||
color: #2e7d32;
|
||||
border: 1px solid rgba(46, 125, 50, 0.3);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 6px 18px rgba(46, 125, 50, 0.25);
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.leaderboard-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 12px;
|
||||
background: linear-gradient(135deg, #4caf50 0%, #8bc34a 50%, #cddc39 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.leaderboard-wrap {
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.leaderboard-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.leaderboard-table thead tr {
|
||||
background: linear-gradient(135deg, #66bb6a 0%, #8bc34a 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.leaderboard-table th,
|
||||
.leaderboard-table td {
|
||||
text-align: left;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid rgba(46, 125, 50, 0.15);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.leaderboard-table tbody tr {
|
||||
background: linear-gradient(135deg, rgba(46,125,50,0.08) 0%, rgba(46,125,50,0.03) 100%);
|
||||
transition: background 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.leaderboard-table tbody tr:hover {
|
||||
background: linear-gradient(135deg, rgba(46,125,50,0.12) 0%, rgba(46,125,50,0.06) 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.rank-badge {
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
text-align: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(45deg, #66bb6a, #8bc34a);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.current-row {
|
||||
outline: 2px solid rgba(76, 175, 80, 0.7);
|
||||
box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.15) inset;
|
||||
}
|
||||
|
||||
.leaderboard-tip {
|
||||
margin-top: 10px;
|
||||
font-size: 0.85rem;
|
||||
color: #388e3c;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@@ -359,11 +436,7 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.next-piece,
|
||||
.controls-info {
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
|
||||
.mobile-controls {
|
||||
display: block;
|
||||
@@ -383,6 +456,15 @@ body {
|
||||
padding: 20px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.leaderboard-wrap {
|
||||
max-height: 200px;
|
||||
}
|
||||
.leaderboard-table th,
|
||||
.leaderboard-table td {
|
||||
padding: 8px 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@@ -428,10 +510,7 @@ body {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.next-piece,
|
||||
.controls-info {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* 隐藏类 */
|
||||
@@ -457,3 +536,39 @@ body {
|
||||
.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
/* 摘要卡片 */
|
||||
.leaderboard-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%);
|
||||
color: #fff;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(46, 125, 50, 0.3);
|
||||
box-shadow: 0 4px 8px rgba(46, 125, 50, 0.2);
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
display: block;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.leaderboard-summary {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user