继续提交
64
.gitignore
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Python (Backend)
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
instance/
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Node.js (Frontend)
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
.npm/
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Frontend Build Output
|
||||||
|
mengyadriftbottle-frontend/dist/
|
||||||
|
mengyadriftbottle-frontend/build/
|
||||||
|
|
||||||
|
# Local Environment Variables
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
48
Dockerfile
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
FROM python:3.11-slim AS base
|
||||||
|
|
||||||
|
# Install Node.js 20 + nginx for building frontend and serving
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y curl gnupg nginx \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& apt-get install -y nodejs build-essential \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY mengyadriftbottle-backend ./mengyadriftbottle-backend
|
||||||
|
COPY mengyadriftbottle-frontend ./mengyadriftbottle-frontend
|
||||||
|
|
||||||
|
# -------- Build frontend --------
|
||||||
|
WORKDIR /app/mengyadriftbottle-frontend
|
||||||
|
RUN npm install \
|
||||||
|
&& npm run build
|
||||||
|
|
||||||
|
# -------- Install backend deps --------
|
||||||
|
WORKDIR /app/mengyadriftbottle-backend
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt \
|
||||||
|
&& pip install --no-cache-dir gunicorn
|
||||||
|
|
||||||
|
# Prepare runtime artifacts
|
||||||
|
WORKDIR /app
|
||||||
|
RUN mkdir -p frontend-dist \
|
||||||
|
&& cp -r /app/mengyadriftbottle-frontend/dist/* /app/frontend-dist/
|
||||||
|
|
||||||
|
# Seed data directory (can be overridden via volume)
|
||||||
|
RUN mkdir -p /app/data \
|
||||||
|
&& cp /app/mengyadriftbottle-backend/*.json /app/data/
|
||||||
|
|
||||||
|
# Copy nginx config and startup script
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
COPY start.sh /app/start.sh
|
||||||
|
RUN chmod +x /app/start.sh
|
||||||
|
|
||||||
|
EXPOSE 6767
|
||||||
|
|
||||||
|
ENV PORT=6767 \
|
||||||
|
BACKEND_PORT=5002 \
|
||||||
|
DRIFT_BOTTLE_FRONTEND_DIST=/app/frontend-dist \
|
||||||
|
DRIFT_BOTTLE_DATA_DIR=/app/data
|
||||||
|
|
||||||
|
CMD ["/app/start.sh"]
|
||||||
68
README.md
@@ -1,2 +1,66 @@
|
|||||||
# mengyadriftbottle
|
# 萌芽漂流瓶(前后端分离版)
|
||||||
萌芽匿名漂流瓶
|
|
||||||
|
- `mengyadriftbottle-backend/`: Flask API(`/api`)+ 管理后台视图(`/admin`)。
|
||||||
|
- `mengyadriftbottle-frontend/`: React + Vite 单页应用,调用上述 API 并提供用户交互界面。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
```cmd
|
||||||
|
cd mengyadriftbottle-backend
|
||||||
|
python -m venv .venv
|
||||||
|
.venv\Scripts\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
服务器默认监听 `http://localhost:5002`,所有公开接口挂载在 `http://localhost:5002/api`。
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
确保后端已运行后启动 Vite:
|
||||||
|
```cmd
|
||||||
|
cd mengyadriftbottle-frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
此命令会在 `http://localhost:5173` 打开 React 界面,所有 `/api/*` 请求会被代理到本地 Flask 服务。
|
||||||
|
|
||||||
|
### 自定义配置
|
||||||
|
在 `mengyadriftbottle-frontend` 下创建 `.env` 覆盖默认地址:
|
||||||
|
```
|
||||||
|
VITE_API_BASE_URL=http://your-domain/api
|
||||||
|
VITE_ADMIN_URL=https://your-domain/admin/login
|
||||||
|
```
|
||||||
|
|
||||||
|
后端使用根目录的 `templates/`(管理后台视图)和 `static/`(样式文件),数据文件(`bottles.json`、`config.json` 等)现已迁移至 `mengyadriftbottle-backend/` 目录,所有前端页面由 React 单页应用提供。
|
||||||
|
|
||||||
|
### 轻量后台
|
||||||
|
|
||||||
|
- 访问 `http://localhost:5002/admin?token=shumengya520`(或自定义 `DRIFT_BOTTLE_ADMIN_TOKEN`)即可查看/删除漂流瓶列表。
|
||||||
|
- 旧版登录后台依然可通过 `http://localhost:5002/admin/login` 使用。
|
||||||
|
|
||||||
|
## Docker 部署
|
||||||
|
|
||||||
|
若希望以单一容器运行前后端,可使用根目录提供的 `Dockerfile`:
|
||||||
|
|
||||||
|
1. 构建镜像:
|
||||||
|
```cmd
|
||||||
|
cd e:\Python\前后端分离项目\萌芽漂流瓶
|
||||||
|
docker build -t mengyadriftbottle:latest .
|
||||||
|
```
|
||||||
|
2. 创建持久化目录(宿主机):
|
||||||
|
```bash
|
||||||
|
mkdir -p /shumengya/docker/storage/mengyadriftbottle
|
||||||
|
```
|
||||||
|
3. 运行容器(对外暴露 6767 端口,并挂载数据目录):
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name mengyadriftbottle \
|
||||||
|
-p 6767:6767 \
|
||||||
|
-v /shumengya/docker/storage/mengyadriftbottle:/app/mengyadriftbottle-backend \
|
||||||
|
mengyadriftbottle:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
容器内会:
|
||||||
|
- 使用 `serve` 在 6767 端口提供构建后的前端静态文件。
|
||||||
|
- 使用 `gunicorn` 在 5002 端口运行 Flask API(前端通过 `VITE_API_BASE_URL` 访问 `http://localhost:5002/api`)。
|
||||||
|
- 将 `/app/mengyadriftbottle-backend` 作为数据目录,因此挂载即可让 `bottles.json` 等文件持久化在宿主机 `/shumengya/docker/storage/mengyadriftbottle`。
|
||||||
|
|||||||
35
docker-build-run.sh
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
IMAGE_NAME=${IMAGE_NAME:-mengyadriftbottle}
|
||||||
|
CONTAINER_NAME=${CONTAINER_NAME:-mengyadriftbottle}
|
||||||
|
FRONTEND_PORT=${FRONTEND_PORT:-6767}
|
||||||
|
BACKEND_PORT=${BACKEND_PORT:-5002}
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
DATA_DIR="${DATA_DIR:-}"
|
||||||
|
|
||||||
|
echo "[1/3] Building image $IMAGE_NAME ..."
|
||||||
|
docker build -t "$IMAGE_NAME" "$PROJECT_DIR"
|
||||||
|
|
||||||
|
echo "[2/3] Removing old container if it exists ..."
|
||||||
|
if docker ps -a --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}$"; then
|
||||||
|
docker rm -f "$CONTAINER_NAME" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[3/3] Starting container $CONTAINER_NAME ..."
|
||||||
|
RUN_ARGS=(
|
||||||
|
-d
|
||||||
|
--name "$CONTAINER_NAME"
|
||||||
|
-p ${FRONTEND_PORT}:6767
|
||||||
|
-e BACKEND_PORT=5002
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -n "$DATA_DIR" ]; then
|
||||||
|
mkdir -p "$DATA_DIR"
|
||||||
|
RUN_ARGS+=( -v "$DATA_DIR:/app/data" )
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run "${RUN_ARGS[@]}" "$IMAGE_NAME"
|
||||||
|
|
||||||
|
echo "Container is up. Frontend: http://localhost:${FRONTEND_PORT}"
|
||||||
|
echo "Logs: docker logs -f ${CONTAINER_NAME}"
|
||||||
14
docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
mengyadriftbottle:
|
||||||
|
build: .
|
||||||
|
container_name: mengyadriftbottle
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6767:6767"
|
||||||
|
environment:
|
||||||
|
BACKEND_PORT: "5002"
|
||||||
|
DRIFT_BOTTLE_DATA_DIR: "/app/data"
|
||||||
|
volumes:
|
||||||
|
- /shumengya/docker/storage/mengyadriftbottle:/app/data
|
||||||
35
mengyadriftbottle-backend/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Mengya Drift Bottle – Backend
|
||||||
|
|
||||||
|
Flask API that powers the React frontend located in `../mengyadriftbottle-frontend`.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- JSON-first `/api` endpoints for throwing, picking up, and reacting to bottles
|
||||||
|
- Rate limiting per IP (5 seconds) for throw/pickup actions
|
||||||
|
- File-backed persistence with automatic schema upgrades
|
||||||
|
- Lightweight token portal via `/admin?token=...` for quick moderation
|
||||||
|
- Full legacy dashboard (`/admin/login`) preserved for session-based moderation
|
||||||
|
- CORS enabled for local development via Vite
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mengyadriftbottle-backend
|
||||||
|
python -m venv .venv
|
||||||
|
.venv\Scripts\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The server listens on `http://localhost:5002` by default and exposes the API under `http://localhost:5002/api`.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Name | Description | Default |
|
||||||
|
| ---- | ----------- | ------- |
|
||||||
|
| `DRIFT_BOTTLE_SECRET` | Secret key for Flask sessions | Random value generated at runtime |
|
||||||
|
| `DRIFT_BOTTLE_ADMIN_TOKEN` | Token required for `/admin?token=...` | `shumengya520` |
|
||||||
|
|
||||||
|
## File Storage
|
||||||
|
|
||||||
|
Bottle data, config, mottos, and filter words continue to use the JSON files located at the repository root (`../bottles.json`, etc.). The backend automatically creates them if missing.
|
||||||
572
mengyadriftbottle-backend/app.py
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
"""Flask backend for the Mengya Drift Bottle application.
|
||||||
|
|
||||||
|
This module exposes a JSON-first API that is consumed by the React
|
||||||
|
frontend located in ``mengyadriftbottle-frontend``. The historical
|
||||||
|
server-rendered templates are preserved for the admin console, while the
|
||||||
|
public-facing experience now lives entirely in the frontend project.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import secrets
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
|
from functools import wraps
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Iterable
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Flask,
|
||||||
|
abort,
|
||||||
|
flash,
|
||||||
|
jsonify,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
send_from_directory,
|
||||||
|
session,
|
||||||
|
url_for,
|
||||||
|
)
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Paths & global configuration
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
BACKEND_ROOT = Path(__file__).resolve().parent # Backend directory
|
||||||
|
PROJECT_ROOT = BACKEND_ROOT.parent # Root project directory
|
||||||
|
FRONTEND_ROOT = PROJECT_ROOT / "mengyadriftbottle-frontend" # Frontend directory
|
||||||
|
TEMPLATES_DIR = FRONTEND_ROOT / "templates" # Templates in frontend
|
||||||
|
STATIC_DIR = FRONTEND_ROOT / "static" # Static files in frontend
|
||||||
|
FRONTEND_DIST = Path(
|
||||||
|
os.environ.get("DRIFT_BOTTLE_FRONTEND_DIST") or PROJECT_ROOT / "frontend-dist"
|
||||||
|
)
|
||||||
|
DATA_DIR = Path(os.environ.get("DRIFT_BOTTLE_DATA_DIR", BACKEND_ROOT))
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
BOTTLES_FILE = DATA_DIR / "bottles.json"
|
||||||
|
FILTER_WORDS_FILE = DATA_DIR / "filter_words.json"
|
||||||
|
MOTTOS_FILE = DATA_DIR / "mottos.json"
|
||||||
|
CONFIG_FILE = DATA_DIR / "config.json"
|
||||||
|
|
||||||
|
API_PREFIX = "/api"
|
||||||
|
OPERATION_INTERVAL = 5 # seconds
|
||||||
|
|
||||||
|
app = Flask(
|
||||||
|
__name__,
|
||||||
|
static_folder=str(STATIC_DIR),
|
||||||
|
template_folder=str(TEMPLATES_DIR),
|
||||||
|
)
|
||||||
|
app.config["JSON_AS_ASCII"] = False
|
||||||
|
app.config["JSON_SORT_KEYS"] = False
|
||||||
|
app.secret_key = os.environ.get("DRIFT_BOTTLE_SECRET") or secrets.token_hex(24)
|
||||||
|
|
||||||
|
# Only allow cross-origin requests for the API endpoints so the React app
|
||||||
|
# running on Vite's dev server can communicate with Flask during local dev.
|
||||||
|
CORS(app, resources={f"{API_PREFIX}/*": {"origins": "*"}})
|
||||||
|
|
||||||
|
last_operation_time: Dict[str, Dict[str, float]] = defaultdict(
|
||||||
|
lambda: {"throw": 0.0, "pickup": 0.0}
|
||||||
|
)
|
||||||
|
last_picked_bottle: Dict[str, str | None] = defaultdict(lambda: None)
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
"name_limit": 7,
|
||||||
|
"message_limit": 100,
|
||||||
|
"admin_username": "shumengya",
|
||||||
|
"admin_password": hashlib.sha256("tyh@19900420".encode()).hexdigest(),
|
||||||
|
}
|
||||||
|
|
||||||
|
SIMPLE_ADMIN_TOKEN = os.environ.get("DRIFT_BOTTLE_ADMIN_TOKEN", "shumengya520")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def ensure_file(path: Path, default_content: Any) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if path.exists():
|
||||||
|
return
|
||||||
|
with path.open("w", encoding="utf-8") as fh:
|
||||||
|
json.dump(default_content, fh, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_config_file() -> Dict[str, Any]:
|
||||||
|
ensure_file(CONFIG_FILE, DEFAULT_CONFIG)
|
||||||
|
return read_config()
|
||||||
|
|
||||||
|
|
||||||
|
def read_config() -> Dict[str, Any]:
|
||||||
|
if CONFIG_FILE.exists():
|
||||||
|
try:
|
||||||
|
with CONFIG_FILE.open("r", encoding="utf-8") as fh:
|
||||||
|
config = json.load(fh)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
config = DEFAULT_CONFIG.copy()
|
||||||
|
else:
|
||||||
|
config = DEFAULT_CONFIG.copy()
|
||||||
|
|
||||||
|
for key, value in DEFAULT_CONFIG.items():
|
||||||
|
config.setdefault(key, value)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def save_config(config: Dict[str, Any]) -> bool:
|
||||||
|
try:
|
||||||
|
with CONFIG_FILE.open("w", encoding="utf-8") as fh:
|
||||||
|
json.dump(config, fh, indent=4, ensure_ascii=False)
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_mottos_file() -> None:
|
||||||
|
default_mottos = [
|
||||||
|
"良言一句三冬暖,恶语伤人六月寒",
|
||||||
|
"己所不欲,勿施于人",
|
||||||
|
"赠人玫瑰,手有余香",
|
||||||
|
"海内存知己,天涯若比邻",
|
||||||
|
"一期一会,珍惜每一次相遇",
|
||||||
|
"星河滚烫,你是人间理想",
|
||||||
|
]
|
||||||
|
ensure_file(MOTTOS_FILE, default_mottos)
|
||||||
|
|
||||||
|
|
||||||
|
def read_mottos() -> Iterable[str]:
|
||||||
|
ensure_mottos_file()
|
||||||
|
try:
|
||||||
|
with MOTTOS_FILE.open("r", encoding="utf-8") as fh:
|
||||||
|
return json.load(fh)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return ["良言一句三冬暖,恶语伤人六月寒"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_motto() -> str:
|
||||||
|
mottos = list(read_mottos())
|
||||||
|
if not mottos:
|
||||||
|
return "良言一句三冬暖,恶语伤人六月寒"
|
||||||
|
return random.choice(mottos)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_filter_words_file() -> None:
|
||||||
|
ensure_file(FILTER_WORDS_FILE, ["敏感词1", "敏感词2"])
|
||||||
|
|
||||||
|
|
||||||
|
def read_filter_words() -> Iterable[str]:
|
||||||
|
ensure_filter_words_file()
|
||||||
|
try:
|
||||||
|
with FILTER_WORDS_FILE.open("r", encoding="utf-8") as fh:
|
||||||
|
return json.load(fh)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def filter_sensitive_words(text: str) -> str:
|
||||||
|
filtered = text
|
||||||
|
for word in read_filter_words():
|
||||||
|
filtered = re.sub(re.escape(word), "*" * len(word), filtered)
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
def read_bottles() -> list[Dict[str, Any]]:
|
||||||
|
if not BOTTLES_FILE.exists():
|
||||||
|
with BOTTLES_FILE.open("w", encoding="utf-8") as fh:
|
||||||
|
json.dump([], fh)
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
with BOTTLES_FILE.open("r", encoding="utf-8") as fh:
|
||||||
|
bottles = json.load(fh)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
for bottle in bottles:
|
||||||
|
bottle.setdefault("likes", 0)
|
||||||
|
bottle.setdefault("dislikes", 0)
|
||||||
|
write_bottles(bottles)
|
||||||
|
return bottles
|
||||||
|
|
||||||
|
|
||||||
|
def write_bottles(bottles: list[Dict[str, Any]]) -> None:
|
||||||
|
BOTTLES_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with BOTTLES_FILE.open("w", encoding="utf-8") as fh:
|
||||||
|
json.dump(bottles, fh, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_ip() -> str:
|
||||||
|
forwarded = request.headers.get("X-Forwarded-For")
|
||||||
|
if forwarded:
|
||||||
|
return forwarded.split(",")[0].strip()
|
||||||
|
real_ip = request.headers.get("X-Real-IP")
|
||||||
|
if real_ip:
|
||||||
|
return real_ip
|
||||||
|
return request.remote_addr or "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def check_operation_interval(operation_type: str) -> float:
|
||||||
|
ip = get_client_ip()
|
||||||
|
current_time = time.time()
|
||||||
|
last_time = last_operation_time[ip][operation_type]
|
||||||
|
remaining = OPERATION_INTERVAL - (current_time - last_time)
|
||||||
|
if remaining > 0:
|
||||||
|
return max(0.0, remaining)
|
||||||
|
last_operation_time[ip][operation_type] = current_time
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def admin_required(func):
|
||||||
|
@wraps(func)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
if not session.get("admin_logged_in"):
|
||||||
|
return redirect(url_for("admin_login"))
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def get_request_payload() -> Dict[str, Any]:
|
||||||
|
if request.is_json:
|
||||||
|
payload = request.get_json(silent=True)
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
return payload
|
||||||
|
if request.form:
|
||||||
|
return request.form.to_dict()
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_name(value: str | None) -> str | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
stripped = value.strip()
|
||||||
|
return stripped or None
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_simple_admin_token(token: str | None) -> bool:
|
||||||
|
if not token:
|
||||||
|
return False
|
||||||
|
# compare_digest防止时间侧信道
|
||||||
|
return secrets.compare_digest(str(token), SIMPLE_ADMIN_TOKEN)
|
||||||
|
|
||||||
|
|
||||||
|
def bootstrap_storage() -> None:
|
||||||
|
ensure_filter_words_file()
|
||||||
|
ensure_mottos_file()
|
||||||
|
ensure_config_file()
|
||||||
|
if not BOTTLES_FILE.exists():
|
||||||
|
write_bottles([])
|
||||||
|
|
||||||
|
|
||||||
|
bootstrap_storage()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
# ==============================后端公开API============================== #
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def root() -> Any:
|
||||||
|
return jsonify({
|
||||||
|
"message": "Mengya Drift Bottle后端API正在运行中...",
|
||||||
|
"api_base": API_PREFIX,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(f"{API_PREFIX}/health")
|
||||||
|
def health_check() -> Any:
|
||||||
|
return jsonify({"status": "ok", "timestamp": datetime.utcnow().isoformat()})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(f"{API_PREFIX}/motto")
|
||||||
|
def random_motto() -> Any:
|
||||||
|
return jsonify({"success": True, "motto": get_random_motto()})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(f"{API_PREFIX}/stats")
|
||||||
|
def get_stats() -> Any:
|
||||||
|
bottles = read_bottles()
|
||||||
|
return jsonify({"success": True, "stats": {"total_bottles": len(bottles)}})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(f"{API_PREFIX}/config")
|
||||||
|
def get_config() -> Any:
|
||||||
|
config = read_config()
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"config": {
|
||||||
|
"name_limit": config.get("name_limit", 7),
|
||||||
|
"message_limit": config.get("message_limit", 100),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post(f"{API_PREFIX}/throw")
|
||||||
|
def throw_bottle() -> Any:
|
||||||
|
wait_time = check_operation_interval("throw")
|
||||||
|
if wait_time > 0:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"error": f"操作太频繁,请等待 {round(wait_time, 1)} 秒后再试。",
|
||||||
|
"wait_time": round(wait_time, 1),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
429,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = get_request_payload()
|
||||||
|
name = normalize_name(data.get("name"))
|
||||||
|
message = normalize_name(data.get("message"))
|
||||||
|
gender = data.get("gender") or "保密"
|
||||||
|
qq_number = normalize_name(data.get("qq") or data.get("qq_number"))
|
||||||
|
|
||||||
|
if not name or not message or not gender:
|
||||||
|
return jsonify({"success": False, "error": "姓名、消息和性别是必需的"}), 400
|
||||||
|
|
||||||
|
config = read_config()
|
||||||
|
name_limit = int(config.get("name_limit", 7))
|
||||||
|
message_limit = int(config.get("message_limit", 100))
|
||||||
|
|
||||||
|
if len(name) > name_limit:
|
||||||
|
return jsonify({"success": False, "error": f"名字最多{name_limit}个字符"}), 400
|
||||||
|
if len(message) > message_limit:
|
||||||
|
return jsonify({"success": False, "error": f"消息内容最多{message_limit}个字符"}), 400
|
||||||
|
|
||||||
|
filtered_name = filter_sensitive_words(name)
|
||||||
|
filtered_message = filter_sensitive_words(message)
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
ip_address = get_client_ip()
|
||||||
|
|
||||||
|
qq_avatar_url = None
|
||||||
|
if qq_number and qq_number.isdigit():
|
||||||
|
qq_avatar_url = f"http://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100"
|
||||||
|
|
||||||
|
new_bottle = {
|
||||||
|
"id": f"{timestamp}_{random.randint(1000, 9999)}",
|
||||||
|
"name": filtered_name,
|
||||||
|
"message": filtered_message,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"ip_address": ip_address,
|
||||||
|
"gender": gender,
|
||||||
|
"qq_number": qq_number,
|
||||||
|
"qq_avatar_url": qq_avatar_url,
|
||||||
|
"likes": 0,
|
||||||
|
"dislikes": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
bottles = read_bottles()
|
||||||
|
bottles.append(new_bottle)
|
||||||
|
write_bottles(bottles)
|
||||||
|
|
||||||
|
return jsonify({"success": True, "message": "漂流瓶投放成功!"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(f"{API_PREFIX}/pickup")
|
||||||
|
def pickup_bottle() -> Any:
|
||||||
|
wait_time = check_operation_interval("pickup")
|
||||||
|
if wait_time > 0:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"message": f"操作太频繁,请等待 {round(wait_time, 1)} 秒后再试。",
|
||||||
|
"wait_time": round(wait_time, 1),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
429,
|
||||||
|
)
|
||||||
|
|
||||||
|
bottles = read_bottles()
|
||||||
|
if not bottles:
|
||||||
|
return jsonify({"success": False, "message": "海里没有漂流瓶。"})
|
||||||
|
|
||||||
|
ip_address = get_client_ip()
|
||||||
|
last_bottle_id = last_picked_bottle[ip_address]
|
||||||
|
|
||||||
|
if len(bottles) == 1:
|
||||||
|
selected_bottle = bottles[0]
|
||||||
|
else:
|
||||||
|
available = [b for b in bottles if b["id"] != last_bottle_id]
|
||||||
|
available = available or bottles
|
||||||
|
weights = [max(1, 10 - min(9, b.get("dislikes", 0))) for b in available]
|
||||||
|
selected_bottle = random.choices(available, weights=weights, k=1)[0]
|
||||||
|
|
||||||
|
last_picked_bottle[ip_address] = selected_bottle["id"]
|
||||||
|
return jsonify({"success": True, "bottle": selected_bottle})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post(f"{API_PREFIX}/react")
|
||||||
|
def react_to_bottle() -> Any:
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
bottle_id = data.get("bottle_id")
|
||||||
|
reaction = data.get("reaction")
|
||||||
|
|
||||||
|
if not bottle_id or reaction not in {"like", "dislike"}:
|
||||||
|
return jsonify({"success": False, "error": "参数错误"}), 400
|
||||||
|
|
||||||
|
bottles = read_bottles()
|
||||||
|
for bottle in bottles:
|
||||||
|
if bottle["id"] == bottle_id:
|
||||||
|
key = "likes" if reaction == "like" else "dislikes"
|
||||||
|
bottle[key] = bottle.get(key, 0) + 1
|
||||||
|
write_bottles(bottles)
|
||||||
|
return jsonify({"success": True, "message": "反馈已记录"})
|
||||||
|
|
||||||
|
return jsonify({"success": False, "error": "未找到漂流瓶"}), 404
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 管理员相关操作API
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_bottles(bottles: list[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"total": len(bottles),
|
||||||
|
"likes": sum(int(b.get("likes", 0) or 0) for b in bottles),
|
||||||
|
"dislikes": sum(int(b.get("dislikes", 0) or 0) for b in bottles),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/admin")
|
||||||
|
def admin_simple_portal():
|
||||||
|
token = request.args.get("token", "")
|
||||||
|
status_key = request.args.get("status")
|
||||||
|
status_messages = {
|
||||||
|
"deleted": "漂流瓶已删除",
|
||||||
|
"missing": "未找到指定漂流瓶,可能已经被删除",
|
||||||
|
"unauthorized": "无权执行该操作,请检查token",
|
||||||
|
}
|
||||||
|
feedback = status_messages.get(status_key)
|
||||||
|
|
||||||
|
authorized = is_valid_simple_admin_token(token)
|
||||||
|
bottles_raw = read_bottles() if authorized else []
|
||||||
|
bottles = sorted(
|
||||||
|
bottles_raw,
|
||||||
|
key=lambda item: item.get("timestamp", ""),
|
||||||
|
reverse=True,
|
||||||
|
) if authorized else []
|
||||||
|
stats = summarize_bottles(bottles) if authorized else None
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"admin_simple.html",
|
||||||
|
authorized=authorized,
|
||||||
|
token=token,
|
||||||
|
stats=stats,
|
||||||
|
bottles=bottles,
|
||||||
|
feedback=feedback,
|
||||||
|
sample_token=SIMPLE_ADMIN_TOKEN if os.environ.get("DRIFT_BOTTLE_ADMIN_TOKEN") is None else "***",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/admin/simple/delete/<bottle_id>")
|
||||||
|
def admin_simple_delete(bottle_id: str):
|
||||||
|
token = request.form.get("token") or request.args.get("token")
|
||||||
|
if not is_valid_simple_admin_token(token):
|
||||||
|
return redirect(url_for("admin_simple_portal", status="unauthorized"))
|
||||||
|
|
||||||
|
bottles = read_bottles()
|
||||||
|
new_bottles = [b for b in bottles if b["id"] != bottle_id]
|
||||||
|
status = "missing"
|
||||||
|
if len(new_bottles) != len(bottles):
|
||||||
|
write_bottles(new_bottles)
|
||||||
|
status = "deleted"
|
||||||
|
|
||||||
|
return redirect(url_for("admin_simple_portal", token=token, status=status))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/login", methods=["GET", "POST"])
|
||||||
|
def admin_login():
|
||||||
|
if request.method == "POST":
|
||||||
|
username = request.form.get("username", "")
|
||||||
|
password = request.form.get("password", "")
|
||||||
|
|
||||||
|
config = read_config()
|
||||||
|
if (
|
||||||
|
username == config.get("admin_username")
|
||||||
|
and hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
== config.get("admin_password")
|
||||||
|
):
|
||||||
|
session["admin_logged_in"] = True
|
||||||
|
return redirect(url_for("admin_dashboard"))
|
||||||
|
|
||||||
|
flash("用户名或密码错误", "error")
|
||||||
|
|
||||||
|
return render_template("admin_login.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/admin/logout")
|
||||||
|
def admin_logout():
|
||||||
|
session.pop("admin_logged_in", None)
|
||||||
|
return redirect(url_for("admin_login"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/admin/dashboard")
|
||||||
|
@admin_required
|
||||||
|
def admin_dashboard():
|
||||||
|
bottles = read_bottles()
|
||||||
|
return render_template("admin_dashboard.html", bottles=bottles)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/admin/delete_bottle/<bottle_id>")
|
||||||
|
@admin_required
|
||||||
|
def delete_bottle(bottle_id: str):
|
||||||
|
bottles = [b for b in read_bottles() if b["id"] != bottle_id]
|
||||||
|
write_bottles(bottles)
|
||||||
|
flash("漂流瓶已成功删除", "success")
|
||||||
|
return redirect(url_for("admin_dashboard"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/settings", methods=["GET", "POST"])
|
||||||
|
@admin_required
|
||||||
|
def admin_settings():
|
||||||
|
config = read_config()
|
||||||
|
if request.method == "POST":
|
||||||
|
try:
|
||||||
|
name_limit = int(request.form.get("name_limit", 7))
|
||||||
|
message_limit = int(request.form.get("message_limit", 100))
|
||||||
|
if name_limit < 1 or message_limit < 1:
|
||||||
|
raise ValueError
|
||||||
|
config["name_limit"] = name_limit
|
||||||
|
config["message_limit"] = message_limit
|
||||||
|
if save_config(config):
|
||||||
|
flash("设置已成功保存", "success")
|
||||||
|
else:
|
||||||
|
flash("保存设置时出错", "error")
|
||||||
|
except ValueError:
|
||||||
|
flash("请输入有效的数值", "error")
|
||||||
|
|
||||||
|
return render_template("admin_settings.html", config=config)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
# ==============================后端公开API============================== #
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/", defaults={"path": ""})
|
||||||
|
@app.route("/<path:path>")
|
||||||
|
def serve_frontend_app(path: str):
|
||||||
|
"""Serve the compiled React frontend (single-page app)."""
|
||||||
|
|
||||||
|
if path.startswith("api/"):
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if not FRONTEND_DIST.exists():
|
||||||
|
return ("Frontend build not found. Please run npm run build first.", 404)
|
||||||
|
|
||||||
|
if path and (FRONTEND_DIST / path).is_file():
|
||||||
|
return send_from_directory(str(FRONTEND_DIST), path)
|
||||||
|
|
||||||
|
return send_from_directory(str(FRONTEND_DIST), "index.html")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=5002, debug=True)
|
||||||
14
mengyadriftbottle-backend/bottles.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "2025-11-16 19:00:54_5309",
|
||||||
|
"name": "树萌芽",
|
||||||
|
"message": "Hello World!",
|
||||||
|
"timestamp": "2025-11-16 19:00:54",
|
||||||
|
"ip_address": "127.0.0.1",
|
||||||
|
"gender": "男",
|
||||||
|
"qq_number": "3205788256",
|
||||||
|
"qq_avatar_url": "http://q1.qlogo.cn/g?b=qq&nk=3205788256&s=100",
|
||||||
|
"likes": 1,
|
||||||
|
"dislikes": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
6
mengyadriftbottle-backend/config.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name_limit": 7,
|
||||||
|
"message_limit": 150,
|
||||||
|
"admin_username": "shumengya",
|
||||||
|
"admin_password": "06dc4b37c16a43fa94a3191e3be039ab6b05dbecc1c55e7860cf9911c72e71f8"
|
||||||
|
}
|
||||||
19
mengyadriftbottle-backend/filter_words.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[
|
||||||
|
"你TM",
|
||||||
|
"唐伟",
|
||||||
|
"糖伟",
|
||||||
|
"糖萎",
|
||||||
|
"月抛",
|
||||||
|
"约炮",
|
||||||
|
"cnm",
|
||||||
|
"傻逼",
|
||||||
|
"狗日的",
|
||||||
|
"妈卖批",
|
||||||
|
"nmsl",
|
||||||
|
"杂种",
|
||||||
|
"操你妈",
|
||||||
|
"草你妈",
|
||||||
|
"日你妈",
|
||||||
|
"你他妈",
|
||||||
|
"超你妈"
|
||||||
|
]
|
||||||
8
mengyadriftbottle-backend/mottos.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[
|
||||||
|
"良言一句三冬暖,恶语伤人六月寒",
|
||||||
|
"己所不欲,勿施于人",
|
||||||
|
"赠人玫瑰,手有余香",
|
||||||
|
"海内存知己,天涯若比邻",
|
||||||
|
"一期一会,珍惜每一次相遇",
|
||||||
|
"星河滚烫,你是人间理想"
|
||||||
|
]
|
||||||
2
mengyadriftbottle-backend/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Flask==3.0.3
|
||||||
|
flask-cors==4.0.0
|
||||||
24
mengyadriftbottle-frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
29
mengyadriftbottle-frontend/README.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
## Mengya Drift Bottle – Frontend
|
||||||
|
|
||||||
|
React + Vite single-page application that consumes the Flask API exposed under `/api`.
|
||||||
|
|
||||||
|
### Available Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# start dev server with API proxy to http://localhost:5002
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# lint with ESLint
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# production build
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The dev server proxies every `/api/*` request to the backend, so start the Flask app before opening the React UI.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Set the following environment variables in a `.env` file if you need to override defaults:
|
||||||
|
|
||||||
|
```
|
||||||
|
VITE_API_BASE_URL=http://localhost:5002/api
|
||||||
|
VITE_ADMIN_URL=http://localhost:5002/admin/login
|
||||||
|
```
|
||||||
|
|
||||||
|
In production you typically serve the static build (`dist/`) behind the same origin as the backend, allowing the default relative `/api` base to keep working.
|
||||||
29
mengyadriftbottle-frontend/eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
17
mengyadriftbottle-frontend/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>萌芽漂流瓶(´,,•ω•,,)♡</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="让心意随海浪飘向远方,邂逅那个懂你的人——萌芽漂流瓶 React 前端"
|
||||||
|
/>
|
||||||
|
<link rel="icon" type="image/png" href="/logo.png" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2891
mengyadriftbottle-frontend/package-lock.json
generated
Normal file
28
mengyadriftbottle-frontend/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "mengyadriftbottle-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"@types/react-dom": "^19.2.2",
|
||||||
|
"@vitejs/plugin-react": "^5.1.0",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"vite": "^7.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
mengyadriftbottle-frontend/public/background/image1.png
Normal file
|
After Width: | Height: | Size: 18 MiB |
BIN
mengyadriftbottle-frontend/public/background/image10.png
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
mengyadriftbottle-frontend/public/background/image11.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
mengyadriftbottle-frontend/public/background/image12.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
mengyadriftbottle-frontend/public/background/image13.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
mengyadriftbottle-frontend/public/background/image14.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
mengyadriftbottle-frontend/public/background/image15.png
Normal file
|
After Width: | Height: | Size: 920 KiB |
BIN
mengyadriftbottle-frontend/public/background/image16.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
mengyadriftbottle-frontend/public/background/image17.png
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
BIN
mengyadriftbottle-frontend/public/background/image18.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
mengyadriftbottle-frontend/public/background/image19.png
Normal file
|
After Width: | Height: | Size: 265 KiB |
BIN
mengyadriftbottle-frontend/public/background/image2.png
Normal file
|
After Width: | Height: | Size: 9.0 MiB |
BIN
mengyadriftbottle-frontend/public/background/image20.png
Normal file
|
After Width: | Height: | Size: 4.2 MiB |
BIN
mengyadriftbottle-frontend/public/background/image21.png
Normal file
|
After Width: | Height: | Size: 8.4 MiB |
BIN
mengyadriftbottle-frontend/public/background/image22.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
mengyadriftbottle-frontend/public/background/image23.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
mengyadriftbottle-frontend/public/background/image24.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
mengyadriftbottle-frontend/public/background/image25.png
Normal file
|
After Width: | Height: | Size: 5.5 MiB |
BIN
mengyadriftbottle-frontend/public/background/image26.png
Normal file
|
After Width: | Height: | Size: 5.7 MiB |
BIN
mengyadriftbottle-frontend/public/background/image27.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
mengyadriftbottle-frontend/public/background/image28.png
Normal file
|
After Width: | Height: | Size: 5.2 MiB |
BIN
mengyadriftbottle-frontend/public/background/image29.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
mengyadriftbottle-frontend/public/background/image3.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
mengyadriftbottle-frontend/public/background/image4.png
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
BIN
mengyadriftbottle-frontend/public/background/image5.png
Normal file
|
After Width: | Height: | Size: 4.8 MiB |
BIN
mengyadriftbottle-frontend/public/background/image6.png
Normal file
|
After Width: | Height: | Size: 9.6 MiB |
BIN
mengyadriftbottle-frontend/public/background/image7.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
mengyadriftbottle-frontend/public/background/image8.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
mengyadriftbottle-frontend/public/background/image9.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
560
mengyadriftbottle-frontend/public/legacy-style.css
Normal file
@@ -0,0 +1,560 @@
|
|||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #f5e8f0 0%, #f3e5fc 100%);
|
||||||
|
color: #5e5166;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 25px 30px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 32px rgba(155, 89, 182, 0.15);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 40px auto;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: '楷体', 'STKaiti', '华文楷体', KaiTi, '宋体', SimSun, sans-serif;
|
||||||
|
color: #d873a9;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 2.4em;
|
||||||
|
text-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #b07cc6;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 i {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #c27ba0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 25px 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.throw-section {
|
||||||
|
border-top: 4px solid #ffb6c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-section {
|
||||||
|
border-top: 4px solid #c5a3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-section::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23f9e0f0' fill-opacity='0.2' fill-rule='evenodd'%3E%3Ccircle cx='3' cy='3' r='3'/%3E%3Ccircle cx='13' cy='13' r='3'/%3E%3C/g%3E%3C/svg%3E");
|
||||||
|
opacity: 0.5;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #7d5ba6;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
border: 1px solid #e1d1f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fdfaff;
|
||||||
|
color: #5e4b6b;
|
||||||
|
font-size: 0.95em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #d291bc;
|
||||||
|
box-shadow: 0 0 0 3px rgba(219, 112, 194, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
color: #cbb8db;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #aa67e5;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 25px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.05em;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 8px rgba(170, 103, 229, 0.3);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-throw {
|
||||||
|
background: linear-gradient(135deg, #ff8fbc 0%, #eb6dab 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pickup {
|
||||||
|
background: linear-gradient(135deg, #a47aed 0%, #876bd3 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
button i {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 15px rgba(170, 103, 229, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#throw-status,
|
||||||
|
#pickup-status {
|
||||||
|
margin-top: 15px;
|
||||||
|
font-style: italic;
|
||||||
|
color: #b56ab0;
|
||||||
|
min-height: 1.5em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottle-display {
|
||||||
|
margin-top: 25px;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 5px 15px rgba(138, 80, 201, 0.1);
|
||||||
|
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
border: 1px solid #f1e1fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottle-header {
|
||||||
|
background: linear-gradient(135deg, #f9ddff 0%, #e9cdff 100%);
|
||||||
|
padding: 15px 20px;
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 1px solid #f1e1ff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottle-avatar {
|
||||||
|
max-width: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid white;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gender-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-left: 8px;
|
||||||
|
background-color: #f0e6ff;
|
||||||
|
color: #7155a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottle-display h3 {
|
||||||
|
color: #8156c5;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2em;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottle-display p {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin: 0;
|
||||||
|
color: #51456a;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 1.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottle-footer {
|
||||||
|
padding: 12px 20px;
|
||||||
|
background-color: #fbf8ff;
|
||||||
|
border-top: 1px solid #f1e8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottle-info small {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 15px;
|
||||||
|
color: #9d8aaf;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottle-info i {
|
||||||
|
margin-right: 5px;
|
||||||
|
color: #b67fdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottle-reactions {
|
||||||
|
margin-top: 15px;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-btn {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e9d8ff;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 6px 15px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-btn i {
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.like-btn {
|
||||||
|
color: #5aaa9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.like-btn:hover:not(:disabled) {
|
||||||
|
background-color: #e5fff8;
|
||||||
|
border-color: #93e7d7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dislike-btn {
|
||||||
|
color: #d76b8b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dislike-btn:hover:not(:disabled) {
|
||||||
|
background-color: #fff1f5;
|
||||||
|
border-color: #ffb8c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooldown-timer {
|
||||||
|
margin-top: 15px;
|
||||||
|
background-color: #f8f0ff;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 25px;
|
||||||
|
color: #aa67e5;
|
||||||
|
font-size: 0.95em;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px dashed #d7c3f0;
|
||||||
|
animation: pulse-soft 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooldown-timer i {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #d873a9;
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-count {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #aa67e5;
|
||||||
|
margin-left: 10px;
|
||||||
|
background-color: #f8f4ff;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: normal;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-count-limit {
|
||||||
|
background-color: #ffebf3;
|
||||||
|
color: #e65c8f;
|
||||||
|
animation: pulse 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-soft {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(170, 103, 229, 0.2);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 8px rgba(170, 103, 229, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(170, 103, 229, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.appear {
|
||||||
|
animation: appear 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes appear {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
color: #a07bb8;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer i {
|
||||||
|
color: #ff85a2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-link {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-link a {
|
||||||
|
color: #a07bb8;
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-link a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
padding: 20px 15px;
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
.wave-container {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 1.7em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
.bottle-info small {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 波浪动画 */
|
||||||
|
.wave-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 200%;
|
||||||
|
height: 100%;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
background-position: 0 bottom;
|
||||||
|
transform-origin: center bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave1 {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 88.7'%3E%3Cpath d='M800 56.9c-155.5 0-204.9-50-405.5-49.9-200 0-250 49.9-394.5 49.9v31.8h800v-.2-31.6z' fill='%23f8d8eb' fill-opacity='0.4'/%3E%3C/svg%3E");
|
||||||
|
background-size: 50% 100px;
|
||||||
|
animation: wave 25s -3s linear infinite;
|
||||||
|
opacity: 0.6;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave2 {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 88.7'%3E%3Cpath d='M800 56.9c-155.5 0-204.9-50-405.5-49.9-200 0-250 49.9-394.5 49.9v31.8h800v-.2-31.6z' fill='%23e6c0e9' fill-opacity='0.3'/%3E%3C/svg%3E");
|
||||||
|
background-size: 50% 120px;
|
||||||
|
animation: wave 20s linear reverse infinite;
|
||||||
|
opacity: 0.4;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wave {
|
||||||
|
0% {transform: translateX(0) translateZ(0) scaleY(1)}
|
||||||
|
50% {transform: translateX(-25%) translateZ(0) scaleY(0.8)}
|
||||||
|
100% {transform: translateX(-50%) translateZ(0) scaleY(1)}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题和介绍文字 */
|
||||||
|
.heart-icon {
|
||||||
|
color: #ff6b9c;
|
||||||
|
margin: 0 8px;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {transform: scale(1);}
|
||||||
|
50% {transform: scale(1.2);}
|
||||||
|
100% {transform: scale(1);}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
color: #9d7bb0;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motto-container {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border-left: 5px solid #aa67e5;
|
||||||
|
box-shadow: 0 3px 15px rgba(160, 120, 200, 0.12);
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
font-family: '楷体', 'STKaiti', '华文楷体', KaiTi, '宋体', SimSun, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motto-container i {
|
||||||
|
color: #d291bc;
|
||||||
|
font-size: 1rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motto-container i.fa-quote-left {
|
||||||
|
position: relative;
|
||||||
|
top: -5px;
|
||||||
|
left: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motto-container i.fa-quote-right {
|
||||||
|
position: relative;
|
||||||
|
bottom: -5px;
|
||||||
|
right: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#random-motto {
|
||||||
|
font-size: 1.3em;
|
||||||
|
color: #7d5ba6;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-container {
|
||||||
|
background-color: #f8e4ff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border: 1px dashed #d7b5f3;
|
||||||
|
box-shadow: 0 3px 10px rgba(160, 120, 200, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-container p {
|
||||||
|
margin: 0;
|
||||||
|
color: #8a5fad;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-container i {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #d873a9;
|
||||||
|
}
|
||||||
|
|
||||||
|
#total-bottles {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #aa67e5;
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 30px;
|
||||||
|
}
|
||||||
BIN
mengyadriftbottle-frontend/public/logo.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
mengyadriftbottle-frontend/public/logo2.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
mengyadriftbottle-frontend/public/logo3.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
1
mengyadriftbottle-frontend/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
507
mengyadriftbottle-frontend/src/App.css
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--app-background-image: url('/background/image1.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: #f7eaf3;
|
||||||
|
background-image: var(--app-background-image);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-attachment: fixed;
|
||||||
|
color: #5e5166;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(255, 214, 233, 0.35);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
-webkit-backdrop-filter: blur(5px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-wrapper {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 30px 10px 60px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
padding: 25px 30px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 32px rgba(155, 89, 182, 0.15);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 650px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: '楷体', 'STKaiti', '华文楷体', KaiTi, '宋体', SimSun, sans-serif;
|
||||||
|
color: #d873a9;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 2.4em;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #b07cc6;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 i {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #c27ba0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
color: #9d7bb0;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motto-container {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border-left: 5px solid #aa67e5;
|
||||||
|
box-shadow: 0 3px 15px rgba(160, 120, 200, 0.12);
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
font-family: '楷体', 'STKaiti', '华文楷体', KaiTi, '宋体', SimSun, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motto-container i {
|
||||||
|
color: #d291bc;
|
||||||
|
font-size: 1rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#random-motto {
|
||||||
|
font-size: 1.3em;
|
||||||
|
color: #7d5ba6;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-motto {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #aa67e5;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85em;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-motto:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-container {
|
||||||
|
background-color: #f8e4ff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border: 1px dashed #d7b5f3;
|
||||||
|
box-shadow: 0 3px 10px rgba(160, 120, 200, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-container p {
|
||||||
|
margin: 0;
|
||||||
|
color: #8a5fad;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-container i {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #d873a9;
|
||||||
|
}
|
||||||
|
|
||||||
|
#total-bottles {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #aa67e5;
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 25px 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.throw-section {
|
||||||
|
border-top: 4px solid #ffb6c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-section {
|
||||||
|
border-top: 4px solid #c5a3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-section::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23f9e0f0' fill-opacity='0.2' fill-rule='evenodd'%3E%3Ccircle cx='3' cy='3' r='3'/%3E%3Ccircle cx='13' cy='13' r='3'/%3E%3C/g%3E%3C/svg%3E");
|
||||||
|
opacity: 0.5;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #7d5ba6;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'],
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
border: 1px solid #e1d1f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fdfaff;
|
||||||
|
color: #5e4b6b;
|
||||||
|
font-size: 0.95em;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text']:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #d291bc;
|
||||||
|
box-shadow: 0 0 0 3px rgba(219, 112, 194, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
color: #cbb8db;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #aa67e5;
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px 25px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.05em;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 8px rgba(170, 103, 229, 0.3);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-throw {
|
||||||
|
background: linear-gradient(135deg, #ff8fbc 0%, #eb6dab 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pickup {
|
||||||
|
background: linear-gradient(135deg, #a47aed 0%, #876bd3 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 15px rgba(170, 103, 229, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#throw-status,
|
||||||
|
#pickup-status {
|
||||||
|
margin-top: 15px;
|
||||||
|
font-style: italic;
|
||||||
|
color: #b56ab0;
|
||||||
|
min-height: 1.5em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottle-display {
|
||||||
|
margin-top: 25px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: left;
|
||||||
|
box-shadow: 0 5px 15px rgba(138, 80, 201, 0.1);
|
||||||
|
border: 1px solid #f1e1fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottle-header {
|
||||||
|
background: linear-gradient(135deg, #f9ddff 0%, #e9cdff 100%);
|
||||||
|
padding: 15px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottle-avatar {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gender-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-left: 8px;
|
||||||
|
background-color: #f0e6ff;
|
||||||
|
color: #7155a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bottle-message {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin: 0;
|
||||||
|
color: #51456a;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 1.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottle-footer {
|
||||||
|
padding: 12px 20px;
|
||||||
|
background-color: #fbf8ff;
|
||||||
|
border-top: 1px solid #f1e8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottle-info small {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 15px;
|
||||||
|
color: #9d8aaf;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottle-reactions {
|
||||||
|
margin-top: 15px;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-btn {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e9d8ff;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 6px 15px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.like-btn {
|
||||||
|
color: #5aaa9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dislike-btn {
|
||||||
|
color: #d76b8b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-btn:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cooldown-timer {
|
||||||
|
margin-top: 15px;
|
||||||
|
background-color: #f8f0ff;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 25px;
|
||||||
|
color: #aa67e5;
|
||||||
|
font-size: 0.95em;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px dashed #d7c3f0;
|
||||||
|
animation: pulse-soft 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-count {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #aa67e5;
|
||||||
|
margin-left: 10px;
|
||||||
|
background-color: #f8f4ff;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-count-limit {
|
||||||
|
background-color: #ffebf3;
|
||||||
|
color: #e65c8f;
|
||||||
|
animation: pulse 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-soft {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(170, 103, 229, 0.2);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 8px rgba(170, 103, 229, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(170, 103, 229, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.appear {
|
||||||
|
animation: appear 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes appear {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.heart-icon {
|
||||||
|
color: #ff6b9c;
|
||||||
|
margin: 0 8px;
|
||||||
|
animation: heart 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes heart {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
color: #a07bb8;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer i {
|
||||||
|
color: #ff85a2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-link {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-link a {
|
||||||
|
color: #a07bb8;
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-link a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
width: 95%;
|
||||||
|
padding: 20px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 98%;
|
||||||
|
padding: 18px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottle-info small {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
446
mengyadriftbottle-frontend/src/App.jsx
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||||
|
import './App.css'
|
||||||
|
|
||||||
|
const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || '/api').replace(/\/$/, '')
|
||||||
|
const ADMIN_URL = import.meta.env.VITE_ADMIN_URL || 'http://localhost:5002/admin/login'
|
||||||
|
const DEFAULT_FORM = Object.freeze({
|
||||||
|
name: '',
|
||||||
|
message: '',
|
||||||
|
gender: '保密',
|
||||||
|
qq: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const BACKGROUND_IMAGES = Array.from({ length: 29 }, (_, index) => `/background/image${index + 1}.png`)
|
||||||
|
|
||||||
|
const formatCount = (value) => {
|
||||||
|
const num = typeof value === 'number' ? value : Number(value || 0)
|
||||||
|
return Number.isNaN(num) ? 0 : num
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [formData, setFormData] = useState({ ...DEFAULT_FORM })
|
||||||
|
const [limits, setLimits] = useState({ name: 7, message: 100 })
|
||||||
|
const [stats, setStats] = useState({ total_bottles: 0 })
|
||||||
|
const [motto, setMotto] = useState('载入中...')
|
||||||
|
const [throwStatus, setThrowStatus] = useState('')
|
||||||
|
const [pickupStatus, setPickupStatus] = useState('')
|
||||||
|
const [currentBottle, setCurrentBottle] = useState(null)
|
||||||
|
const [cooldowns, setCooldowns] = useState({ throw: 0, pickup: 0 })
|
||||||
|
const [loadingAction, setLoadingAction] = useState({ throw: false, pickup: false })
|
||||||
|
const [reactionDisabled, setReactionDisabled] = useState(false)
|
||||||
|
const isThrowing = loadingAction.throw
|
||||||
|
const isPicking = loadingAction.pickup
|
||||||
|
|
||||||
|
const randomBackground = useMemo(() => {
|
||||||
|
if (!BACKGROUND_IMAGES.length) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const index = Math.floor(Math.random() * BACKGROUND_IMAGES.length)
|
||||||
|
return BACKGROUND_IMAGES[index]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (randomBackground) {
|
||||||
|
document.documentElement.style.setProperty('--app-background-image', `url(${randomBackground})`)
|
||||||
|
}
|
||||||
|
}, [randomBackground])
|
||||||
|
|
||||||
|
const startCooldown = useCallback((type, seconds = 5) => {
|
||||||
|
const duration = Math.max(1, Math.ceil(seconds))
|
||||||
|
setCooldowns((prev) => ({ ...prev, [type]: duration }))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCooldowns((prev) => {
|
||||||
|
const next = {
|
||||||
|
throw: prev.throw > 0 ? prev.throw - 1 : 0,
|
||||||
|
pickup: prev.pickup > 0 ? prev.pickup - 1 : 0,
|
||||||
|
}
|
||||||
|
if (next.throw === prev.throw && next.pickup === prev.pickup) {
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchConfig = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/config`)
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.success && data.config) {
|
||||||
|
setLimits({
|
||||||
|
name: data.config.name_limit ?? 7,
|
||||||
|
message: data.config.message_limit ?? 100,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch config', error)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchStats = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/stats`)
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.success && data.stats) {
|
||||||
|
setStats(data.stats)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch stats', error)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchMotto = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/motto`)
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.success) {
|
||||||
|
setMotto(data.motto)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch motto', error)
|
||||||
|
setMotto('良言一句三冬暖,恶语伤人六月寒')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConfig()
|
||||||
|
fetchStats()
|
||||||
|
fetchMotto()
|
||||||
|
}, [fetchConfig, fetchStats, fetchMotto])
|
||||||
|
|
||||||
|
const handleInputChange = useCallback((event) => {
|
||||||
|
const { name, value } = event.target
|
||||||
|
if (name === 'qq' && value && /[^0-9]/.test(value)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setFormData((prev) => ({ ...prev, [name]: value }))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const resetForm = useCallback(() => {
|
||||||
|
setFormData({ ...DEFAULT_FORM })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleThrowSubmit = useCallback(
|
||||||
|
async (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (cooldowns.throw > 0 || isThrowing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingAction((prev) => ({ ...prev, throw: true }))
|
||||||
|
setThrowStatus('正在扔瓶子...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/throw`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: formData.name,
|
||||||
|
message: formData.message,
|
||||||
|
gender: formData.gender,
|
||||||
|
qq: formData.qq,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
if (response.ok && data.success) {
|
||||||
|
setThrowStatus('瓶子已成功扔出!祝你好运~')
|
||||||
|
resetForm()
|
||||||
|
fetchStats()
|
||||||
|
startCooldown('throw', 5)
|
||||||
|
} else {
|
||||||
|
const waitTime = data.wait_time ?? 0
|
||||||
|
setThrowStatus(`出错了: ${data.error || data.message || '未知错误'}`)
|
||||||
|
if (waitTime) {
|
||||||
|
startCooldown('throw', waitTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Throw request failed', error)
|
||||||
|
setThrowStatus('请求失败,请检查网络连接。')
|
||||||
|
} finally {
|
||||||
|
setLoadingAction((prev) => ({ ...prev, throw: false }))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[cooldowns.throw, isThrowing, formData, fetchStats, resetForm, startCooldown],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handlePickup = useCallback(async () => {
|
||||||
|
if (cooldowns.pickup > 0 || isPicking) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoadingAction((prev) => ({ ...prev, pickup: true }))
|
||||||
|
setPickupStatus('正在打捞瓶子...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/pickup`)
|
||||||
|
const data = await response.json()
|
||||||
|
if (response.ok && data.success && data.bottle) {
|
||||||
|
setCurrentBottle(data.bottle)
|
||||||
|
setReactionDisabled(false)
|
||||||
|
setPickupStatus('捡到了一个瓶子!缘分来了~')
|
||||||
|
startCooldown('pickup', 5)
|
||||||
|
} else {
|
||||||
|
setCurrentBottle(null)
|
||||||
|
const waitTime = data.wait_time ?? 0
|
||||||
|
setPickupStatus(data.message || data.error || '海里没有瓶子了,或者出错了。')
|
||||||
|
if (waitTime) {
|
||||||
|
startCooldown('pickup', waitTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Pickup request failed', error)
|
||||||
|
setPickupStatus('请求失败,请检查网络连接。')
|
||||||
|
setCurrentBottle(null)
|
||||||
|
} finally {
|
||||||
|
setLoadingAction((prev) => ({ ...prev, pickup: false }))
|
||||||
|
}
|
||||||
|
}, [cooldowns.pickup, isPicking, startCooldown])
|
||||||
|
|
||||||
|
const handleReaction = useCallback(
|
||||||
|
async (reaction) => {
|
||||||
|
if (!currentBottle) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setReactionDisabled(true)
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/react`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ bottle_id: currentBottle.id, reaction }),
|
||||||
|
})
|
||||||
|
const data = await response.json()
|
||||||
|
if (response.ok && data.success) {
|
||||||
|
setCurrentBottle((prev) =>
|
||||||
|
prev
|
||||||
|
? {
|
||||||
|
...prev,
|
||||||
|
likes: reaction === 'like' ? formatCount(prev.likes) + 1 : prev.likes,
|
||||||
|
dislikes: reaction === 'dislike' ? formatCount(prev.dislikes) + 1 : prev.dislikes,
|
||||||
|
}
|
||||||
|
: prev,
|
||||||
|
)
|
||||||
|
setPickupStatus(
|
||||||
|
reaction === 'like' ? '感谢您的点赞!' : '已记录您的反馈,该瓶子被捡起的概率将会降低。',
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setPickupStatus(data.error || '记录反馈时出错。')
|
||||||
|
setReactionDisabled(false)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Reaction request failed', error)
|
||||||
|
setPickupStatus('请求失败,请稍后再试。')
|
||||||
|
setReactionDisabled(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentBottle],
|
||||||
|
)
|
||||||
|
|
||||||
|
const nameCharCount = useMemo(() => formData.name.length, [formData.name])
|
||||||
|
const messageCharCount = useMemo(() => formData.message.length, [formData.message])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-wrapper">
|
||||||
|
<div className="background-overlay" aria-hidden="true" />
|
||||||
|
|
||||||
|
<div className="container">
|
||||||
|
<h1>
|
||||||
|
<i className="fas fa-heart heart-icon" /> 萌芽漂流瓶{' '}
|
||||||
|
<i className="fas fa-heart heart-icon" />
|
||||||
|
</h1>
|
||||||
|
<p className="tagline">让心意随海浪飘向远方,邂逅那个懂你的人(´,,•ω•,,)♡...</p>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="stats-container">
|
||||||
|
<p>
|
||||||
|
<i className="fas fa-wine-bottle" /> 海洋中共有{' '}
|
||||||
|
<span id="total-bottles">{formatCount(stats.total_bottles)}</span> 个漂流瓶在寻找有缘人
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="action-section throw-section">
|
||||||
|
<h2>
|
||||||
|
<i className="fas fa-paper-plane" /> 扔一个漂流瓶(,,・ω・,,)
|
||||||
|
</h2>
|
||||||
|
<form id="throw-bottle-form" onSubmit={handleThrowSubmit}>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name">
|
||||||
|
你的昵称:{' '}
|
||||||
|
<span className={`char-count ${nameCharCount >= limits.name ? 'char-count-limit' : ''}`}>
|
||||||
|
<span id="name-char-count">{nameCharCount}</span>/{' '}
|
||||||
|
<span id="name-limit">{limits.name}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
maxLength={limits.name}
|
||||||
|
placeholder="告诉对方你是谁..."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="message">
|
||||||
|
漂流瓶内容:{' '}
|
||||||
|
<span
|
||||||
|
className={`char-count ${
|
||||||
|
messageCharCount >= limits.message ? 'char-count-limit' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span id="message-char-count">{messageCharCount}</span>/{' '}
|
||||||
|
<span id="message-limit">{limits.message}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="message"
|
||||||
|
name="message"
|
||||||
|
rows="4"
|
||||||
|
value={formData.message}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
maxLength={limits.message}
|
||||||
|
placeholder="写下你想说的话,也许会有人懂..."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="gender">性别:</label>
|
||||||
|
<select id="gender" name="gender" value={formData.gender} onChange={handleInputChange}>
|
||||||
|
<option value="保密">保密</option>
|
||||||
|
<option value="男">男</option>
|
||||||
|
<option value="女">女</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="qq">QQ号 (选填):</label>
|
||||||
|
<input
|
||||||
|
id="qq"
|
||||||
|
name="qq"
|
||||||
|
type="text"
|
||||||
|
value={formData.qq}
|
||||||
|
inputMode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="填写QQ号展示头像..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{cooldowns.throw > 0 ? (
|
||||||
|
<div id="throw-cooldown" className="cooldown-timer">
|
||||||
|
<i className="fas fa-hourglass-half" /> 冷却中: <span id="throw-countdown">{cooldowns.throw}</span> 秒
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button type="submit" id="throw-button" className="btn-throw" disabled={isThrowing}>
|
||||||
|
<i className="fas fa-paper-plane" /> {isThrowing ? '正在扔瓶子...' : '扔出去'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
<p id="throw-status">{throwStatus}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="action-section pickup-section">
|
||||||
|
<h2>
|
||||||
|
<i className="fas fa-search-location" /> 捡一个漂流瓶(,,・ω・,,)
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{cooldowns.pickup > 0 ? (
|
||||||
|
<div id="pickup-cooldown" className="cooldown-timer">
|
||||||
|
<i className="fas fa-hourglass-half" /> 冷却中: <span id="pickup-countdown">{cooldowns.pickup}</span> 秒
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="pickup-bottle-button"
|
||||||
|
className="btn-pickup"
|
||||||
|
onClick={handlePickup}
|
||||||
|
disabled={isPicking}
|
||||||
|
>
|
||||||
|
<i className="fas fa-hand-paper" /> {isPicking ? '正在打捞...' : '捡瓶子'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentBottle && (
|
||||||
|
<div id="bottle-display" className="appear">
|
||||||
|
<div className="bottle-header">
|
||||||
|
{currentBottle.qq_avatar_url ? (
|
||||||
|
<img id="bottle-avatar" src={currentBottle.qq_avatar_url} alt="QQ Avatar" />
|
||||||
|
) : null}
|
||||||
|
<h3>
|
||||||
|
来自 <span id="bottle-name">{currentBottle.name}</span>{' '}
|
||||||
|
<span className="gender-badge" id="bottle-gender">
|
||||||
|
{currentBottle.gender || '保密'}
|
||||||
|
</span>{' '}
|
||||||
|
的漂流瓶
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="message-content">
|
||||||
|
<p id="bottle-message">{currentBottle.message}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bottle-footer">
|
||||||
|
<div className="bottle-info">
|
||||||
|
<small>
|
||||||
|
<i className="far fa-clock" /> 时间: <span id="bottle-timestamp">{currentBottle.timestamp}</span>
|
||||||
|
</small>
|
||||||
|
<small>
|
||||||
|
<i className="fas fa-map-marker-alt" /> IP:{' '}
|
||||||
|
<span id="bottle-ip">{currentBottle.ip_address || '未知'}</span>
|
||||||
|
</small>
|
||||||
|
{currentBottle.qq_number ? (
|
||||||
|
<small id="bottle-qq-number">
|
||||||
|
<i className="fab fa-qq" /> QQ: <span id="qq-number-val">{currentBottle.qq_number}</span>
|
||||||
|
</small>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bottle-reactions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="like-button"
|
||||||
|
className="reaction-btn like-btn"
|
||||||
|
onClick={() => handleReaction('like')}
|
||||||
|
disabled={reactionDisabled}
|
||||||
|
>
|
||||||
|
<i className="far fa-thumbs-up" /> <span id="like-count">{formatCount(currentBottle.likes)}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="dislike-button"
|
||||||
|
className="reaction-btn dislike-btn"
|
||||||
|
onClick={() => handleReaction('dislike')}
|
||||||
|
disabled={reactionDisabled}
|
||||||
|
>
|
||||||
|
<i className="far fa-thumbs-down" />{' '}
|
||||||
|
<span id="dislike-count">{formatCount(currentBottle.dislikes)}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p id="pickup-status">{pickupStatus}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>
|
||||||
|
© 2025 萌芽漂流瓶-蜀ICP备2025151694号 <i className="fas fa-heart" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
1
mengyadriftbottle-frontend/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
12
mengyadriftbottle-frontend/src/index.css
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
:root {
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
10
mengyadriftbottle-frontend/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.jsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
453
mengyadriftbottle-frontend/static/admin.css
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
/* 管理后台样式 */
|
||||||
|
:root {
|
||||||
|
--admin-primary: #8a5fad;
|
||||||
|
--admin-secondary: #d291bc;
|
||||||
|
--admin-dark: #5e4b6b;
|
||||||
|
--admin-light: #f8f4ff;
|
||||||
|
--admin-gray: #e9e3f5;
|
||||||
|
--admin-success: #6bbd85;
|
||||||
|
--admin-error: #e65c8f;
|
||||||
|
--admin-warning: #f3a754;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f3f0f8;
|
||||||
|
color: #333;
|
||||||
|
font-family: 'Segoe UI', Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录页面样式 */
|
||||||
|
.admin-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(135deg, #f5e8f0 0%, #f3e5fc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-card {
|
||||||
|
width: 400px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 5px 20px rgba(138, 95, 173, 0.1);
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header h1 {
|
||||||
|
color: var(--admin-primary);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header p {
|
||||||
|
color: var(--admin-secondary);
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background-color: #ffecf1;
|
||||||
|
color: var(--admin-error);
|
||||||
|
border-left: 4px solid var(--admin-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: #ecf9f2;
|
||||||
|
color: var(--admin-success);
|
||||||
|
border-left: 4px solid var(--admin-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-form .form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-form label {
|
||||||
|
display: block;
|
||||||
|
color: var(--admin-dark);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-form input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 1px solid var(--admin-gray);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 1em;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-form input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--admin-primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(138, 95, 173, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: var(--admin-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-btn:hover {
|
||||||
|
background-color: #7b4f9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-footer {
|
||||||
|
margin-top: 25px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
color: var(--admin-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 仪表盘布局 */
|
||||||
|
.admin-sidebar {
|
||||||
|
width: 250px;
|
||||||
|
background-color: var(--admin-dark);
|
||||||
|
color: white;
|
||||||
|
height: 100vh;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar-header {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar-header h2 {
|
||||||
|
margin: 0 0 5px;
|
||||||
|
font-size: 1.4em;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar-header p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu li {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu li a {
|
||||||
|
display: block;
|
||||||
|
padding: 12px 20px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu li a i {
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu li a:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu li.active a {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
font-weight: 600;
|
||||||
|
border-left: 4px solid var(--admin-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-main {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-left: 250px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
background-color: white;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header-bar h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
color: var(--admin-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-user {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--admin-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-content {
|
||||||
|
background-color: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计卡片 */
|
||||||
|
.admin-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background-color: var(--admin-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background-color: var(--admin-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon i {
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.8em;
|
||||||
|
color: var(--admin-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info p {
|
||||||
|
margin: 5px 0 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--admin-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格样式 */
|
||||||
|
.admin-table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table-container h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: var(--admin-dark);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table th,
|
||||||
|
.admin-table td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-bottom: 1px solid var(--admin-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table th {
|
||||||
|
background-color: var(--admin-light);
|
||||||
|
color: var(--admin-dark);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table tr:hover {
|
||||||
|
background-color: #fdfbff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottle-id {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottle-message {
|
||||||
|
max-width: 300px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
background-color: #ffeaef;
|
||||||
|
color: var(--admin-error);
|
||||||
|
border: none;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85em;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background-color: var(--admin-error);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px;
|
||||||
|
color: #888;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 设置页面 */
|
||||||
|
.settings-card {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: var(--admin-dark);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-description {
|
||||||
|
color: #777;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-form .form-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-form label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--admin-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-hint {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--admin-secondary);
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-form input[type="number"] {
|
||||||
|
width: 120px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--admin-gray);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-form input[type="number"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--admin-primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(138, 95, 173, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
background-color: var(--admin-success);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover {
|
||||||
|
background-color: #5aa975;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-note {
|
||||||
|
background-color: #fffbeb;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 30px;
|
||||||
|
border-left: 4px solid var(--admin-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-note p {
|
||||||
|
margin: 0;
|
||||||
|
color: #997328;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-body {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-main {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stats {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-card {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
Fonticons, Inc. (https://fontawesome.com)
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Font Awesome Free License
|
||||||
|
|
||||||
|
Font Awesome Free is free, open source, and GPL friendly. You can use it for
|
||||||
|
commercial projects, open source projects, or really almost whatever you want.
|
||||||
|
Full Font Awesome Free license: https://fontawesome.com/license/free.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
|
||||||
|
|
||||||
|
The Font Awesome Free download is licensed under a Creative Commons
|
||||||
|
Attribution 4.0 International License and applies to all icons packaged
|
||||||
|
as SVG and JS file types.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Fonts: SIL OFL 1.1 License
|
||||||
|
|
||||||
|
In the Font Awesome Free download, the SIL OFL license applies to all icons
|
||||||
|
packaged as web and desktop font files.
|
||||||
|
|
||||||
|
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
|
||||||
|
with Reserved Font Name: "Font Awesome".
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
SIL OPEN FONT LICENSE
|
||||||
|
Version 1.1 - 26 February 2007
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting — in part or in whole — any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Code: MIT License (https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
In the Font Awesome Free download, the MIT license applies to all non-font and
|
||||||
|
non-icon files.
|
||||||
|
|
||||||
|
Copyright 2024 Fonticons, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in the
|
||||||
|
Software without restriction, including without limitation the rights to use, copy,
|
||||||
|
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Attribution
|
||||||
|
|
||||||
|
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
|
||||||
|
Awesome Free files already contain embedded comments with sufficient
|
||||||
|
attribution, so you shouldn't need to do anything additional when using these
|
||||||
|
files normally.
|
||||||
|
|
||||||
|
We've kept attribution comments terse, so we ask that you do not actively work
|
||||||
|
to remove them from files, especially code. They're a great way for folks to
|
||||||
|
learn about Font Awesome.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Brand Icons
|
||||||
|
|
||||||
|
All brand icons are trademarks of their respective owners. The use of these
|
||||||
|
trademarks does not indicate endorsement of the trademark holder by Font
|
||||||
|
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
|
||||||
|
to represent the company, product, or service to which they refer.**
|
||||||
9
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/css/all.min.css
vendored
Normal file
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/css/brands.min.css
vendored
Normal file
6243
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/css/fontawesome.css
vendored
Normal file
9
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/css/fontawesome.min.css
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-classic: 'Font Awesome 6 Free';
|
||||||
|
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Free';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.far,
|
||||||
|
.fa-regular {
|
||||||
|
font-weight: 400; }
|
||||||
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/css/regular.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-classic: 'Font Awesome 6 Free';
|
||||||
|
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Free';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.fas,
|
||||||
|
.fa-solid {
|
||||||
|
font-weight: 900; }
|
||||||
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/css/solid.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free';
|
||||||
|
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free';
|
||||||
|
--fa-font-light: normal 300 1em/1 'Font Awesome 6 Pro';
|
||||||
|
--fa-font-thin: normal 100 1em/1 'Font Awesome 6 Pro';
|
||||||
|
--fa-font-duotone: normal 900 1em/1 'Font Awesome 6 Duotone';
|
||||||
|
--fa-font-duotone-regular: normal 400 1em/1 'Font Awesome 6 Duotone';
|
||||||
|
--fa-font-duotone-light: normal 300 1em/1 'Font Awesome 6 Duotone';
|
||||||
|
--fa-font-duotone-thin: normal 100 1em/1 'Font Awesome 6 Duotone';
|
||||||
|
--fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands';
|
||||||
|
--fa-font-sharp-solid: normal 900 1em/1 'Font Awesome 6 Sharp';
|
||||||
|
--fa-font-sharp-regular: normal 400 1em/1 'Font Awesome 6 Sharp';
|
||||||
|
--fa-font-sharp-light: normal 300 1em/1 'Font Awesome 6 Sharp';
|
||||||
|
--fa-font-sharp-thin: normal 100 1em/1 'Font Awesome 6 Sharp';
|
||||||
|
--fa-font-sharp-duotone-solid: normal 900 1em/1 'Font Awesome 6 Sharp Duotone';
|
||||||
|
--fa-font-sharp-duotone-regular: normal 400 1em/1 'Font Awesome 6 Sharp Duotone';
|
||||||
|
--fa-font-sharp-duotone-light: normal 300 1em/1 'Font Awesome 6 Sharp Duotone';
|
||||||
|
--fa-font-sharp-duotone-thin: normal 100 1em/1 'Font Awesome 6 Sharp Duotone'; }
|
||||||
|
|
||||||
|
svg.svg-inline--fa:not(:root), svg.svg-inline--fa:not(:host) {
|
||||||
|
overflow: visible;
|
||||||
|
box-sizing: content-box; }
|
||||||
|
|
||||||
|
.svg-inline--fa {
|
||||||
|
display: var(--fa-display, inline-block);
|
||||||
|
height: 1em;
|
||||||
|
overflow: visible;
|
||||||
|
vertical-align: -.125em; }
|
||||||
|
.svg-inline--fa.fa-2xs {
|
||||||
|
vertical-align: 0.1em; }
|
||||||
|
.svg-inline--fa.fa-xs {
|
||||||
|
vertical-align: 0em; }
|
||||||
|
.svg-inline--fa.fa-sm {
|
||||||
|
vertical-align: -0.07143em; }
|
||||||
|
.svg-inline--fa.fa-lg {
|
||||||
|
vertical-align: -0.2em; }
|
||||||
|
.svg-inline--fa.fa-xl {
|
||||||
|
vertical-align: -0.25em; }
|
||||||
|
.svg-inline--fa.fa-2xl {
|
||||||
|
vertical-align: -0.3125em; }
|
||||||
|
.svg-inline--fa.fa-pull-left {
|
||||||
|
margin-right: var(--fa-pull-margin, 0.3em);
|
||||||
|
width: auto; }
|
||||||
|
.svg-inline--fa.fa-pull-right {
|
||||||
|
margin-left: var(--fa-pull-margin, 0.3em);
|
||||||
|
width: auto; }
|
||||||
|
.svg-inline--fa.fa-li {
|
||||||
|
width: var(--fa-li-width, 2em);
|
||||||
|
top: 0.25em; }
|
||||||
|
.svg-inline--fa.fa-fw {
|
||||||
|
width: var(--fa-fw-width, 1.25em); }
|
||||||
|
|
||||||
|
.fa-layers svg.svg-inline--fa {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0; }
|
||||||
|
|
||||||
|
.fa-layers-counter, .fa-layers-text {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center; }
|
||||||
|
|
||||||
|
.fa-layers {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1em;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: -.125em;
|
||||||
|
width: 1em; }
|
||||||
|
.fa-layers svg.svg-inline--fa {
|
||||||
|
transform-origin: center center; }
|
||||||
|
|
||||||
|
.fa-layers-text {
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transform-origin: center center; }
|
||||||
|
|
||||||
|
.fa-layers-counter {
|
||||||
|
background-color: var(--fa-counter-background-color, #ff253a);
|
||||||
|
border-radius: var(--fa-counter-border-radius, 1em);
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--fa-inverse, #fff);
|
||||||
|
line-height: var(--fa-counter-line-height, 1);
|
||||||
|
max-width: var(--fa-counter-max-width, 5em);
|
||||||
|
min-width: var(--fa-counter-min-width, 1.5em);
|
||||||
|
overflow: hidden;
|
||||||
|
padding: var(--fa-counter-padding, 0.25em 0.5em);
|
||||||
|
right: var(--fa-right, 0);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
top: var(--fa-top, 0);
|
||||||
|
transform: scale(var(--fa-counter-scale, 0.25));
|
||||||
|
transform-origin: top right; }
|
||||||
|
|
||||||
|
.fa-layers-bottom-right {
|
||||||
|
bottom: var(--fa-bottom, 0);
|
||||||
|
right: var(--fa-right, 0);
|
||||||
|
top: auto;
|
||||||
|
transform: scale(var(--fa-layers-scale, 0.25));
|
||||||
|
transform-origin: bottom right; }
|
||||||
|
|
||||||
|
.fa-layers-bottom-left {
|
||||||
|
bottom: var(--fa-bottom, 0);
|
||||||
|
left: var(--fa-left, 0);
|
||||||
|
right: auto;
|
||||||
|
top: auto;
|
||||||
|
transform: scale(var(--fa-layers-scale, 0.25));
|
||||||
|
transform-origin: bottom left; }
|
||||||
|
|
||||||
|
.fa-layers-top-right {
|
||||||
|
top: var(--fa-top, 0);
|
||||||
|
right: var(--fa-right, 0);
|
||||||
|
transform: scale(var(--fa-layers-scale, 0.25));
|
||||||
|
transform-origin: top right; }
|
||||||
|
|
||||||
|
.fa-layers-top-left {
|
||||||
|
left: var(--fa-left, 0);
|
||||||
|
right: auto;
|
||||||
|
top: var(--fa-top, 0);
|
||||||
|
transform: scale(var(--fa-layers-scale, 0.25));
|
||||||
|
transform-origin: top left; }
|
||||||
|
|
||||||
|
.fa-1x {
|
||||||
|
font-size: 1em; }
|
||||||
|
|
||||||
|
.fa-2x {
|
||||||
|
font-size: 2em; }
|
||||||
|
|
||||||
|
.fa-3x {
|
||||||
|
font-size: 3em; }
|
||||||
|
|
||||||
|
.fa-4x {
|
||||||
|
font-size: 4em; }
|
||||||
|
|
||||||
|
.fa-5x {
|
||||||
|
font-size: 5em; }
|
||||||
|
|
||||||
|
.fa-6x {
|
||||||
|
font-size: 6em; }
|
||||||
|
|
||||||
|
.fa-7x {
|
||||||
|
font-size: 7em; }
|
||||||
|
|
||||||
|
.fa-8x {
|
||||||
|
font-size: 8em; }
|
||||||
|
|
||||||
|
.fa-9x {
|
||||||
|
font-size: 9em; }
|
||||||
|
|
||||||
|
.fa-10x {
|
||||||
|
font-size: 10em; }
|
||||||
|
|
||||||
|
.fa-2xs {
|
||||||
|
font-size: 0.625em;
|
||||||
|
line-height: 0.1em;
|
||||||
|
vertical-align: 0.225em; }
|
||||||
|
|
||||||
|
.fa-xs {
|
||||||
|
font-size: 0.75em;
|
||||||
|
line-height: 0.08333em;
|
||||||
|
vertical-align: 0.125em; }
|
||||||
|
|
||||||
|
.fa-sm {
|
||||||
|
font-size: 0.875em;
|
||||||
|
line-height: 0.07143em;
|
||||||
|
vertical-align: 0.05357em; }
|
||||||
|
|
||||||
|
.fa-lg {
|
||||||
|
font-size: 1.25em;
|
||||||
|
line-height: 0.05em;
|
||||||
|
vertical-align: -0.075em; }
|
||||||
|
|
||||||
|
.fa-xl {
|
||||||
|
font-size: 1.5em;
|
||||||
|
line-height: 0.04167em;
|
||||||
|
vertical-align: -0.125em; }
|
||||||
|
|
||||||
|
.fa-2xl {
|
||||||
|
font-size: 2em;
|
||||||
|
line-height: 0.03125em;
|
||||||
|
vertical-align: -0.1875em; }
|
||||||
|
|
||||||
|
.fa-fw {
|
||||||
|
text-align: center;
|
||||||
|
width: 1.25em; }
|
||||||
|
|
||||||
|
.fa-ul {
|
||||||
|
list-style-type: none;
|
||||||
|
margin-left: var(--fa-li-margin, 2.5em);
|
||||||
|
padding-left: 0; }
|
||||||
|
.fa-ul > li {
|
||||||
|
position: relative; }
|
||||||
|
|
||||||
|
.fa-li {
|
||||||
|
left: calc(-1 * var(--fa-li-width, 2em));
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
width: var(--fa-li-width, 2em);
|
||||||
|
line-height: inherit; }
|
||||||
|
|
||||||
|
.fa-border {
|
||||||
|
border-color: var(--fa-border-color, #eee);
|
||||||
|
border-radius: var(--fa-border-radius, 0.1em);
|
||||||
|
border-style: var(--fa-border-style, solid);
|
||||||
|
border-width: var(--fa-border-width, 0.08em);
|
||||||
|
padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); }
|
||||||
|
|
||||||
|
.fa-pull-left {
|
||||||
|
float: left;
|
||||||
|
margin-right: var(--fa-pull-margin, 0.3em); }
|
||||||
|
|
||||||
|
.fa-pull-right {
|
||||||
|
float: right;
|
||||||
|
margin-left: var(--fa-pull-margin, 0.3em); }
|
||||||
|
|
||||||
|
.fa-beat {
|
||||||
|
animation-name: fa-beat;
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
|
||||||
|
|
||||||
|
.fa-bounce {
|
||||||
|
animation-name: fa-bounce;
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); }
|
||||||
|
|
||||||
|
.fa-fade {
|
||||||
|
animation-name: fa-fade;
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
|
||||||
|
|
||||||
|
.fa-beat-fade {
|
||||||
|
animation-name: fa-beat-fade;
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
|
||||||
|
|
||||||
|
.fa-flip {
|
||||||
|
animation-name: fa-flip;
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
|
||||||
|
|
||||||
|
.fa-shake {
|
||||||
|
animation-name: fa-shake;
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-timing-function: var(--fa-animation-timing, linear); }
|
||||||
|
|
||||||
|
.fa-spin {
|
||||||
|
animation-name: fa-spin;
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-duration: var(--fa-animation-duration, 2s);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-timing-function: var(--fa-animation-timing, linear); }
|
||||||
|
|
||||||
|
.fa-spin-reverse {
|
||||||
|
--fa-animation-direction: reverse; }
|
||||||
|
|
||||||
|
.fa-pulse,
|
||||||
|
.fa-spin-pulse {
|
||||||
|
animation-name: fa-spin;
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-timing-function: var(--fa-animation-timing, steps(8)); }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.fa-beat,
|
||||||
|
.fa-bounce,
|
||||||
|
.fa-fade,
|
||||||
|
.fa-beat-fade,
|
||||||
|
.fa-flip,
|
||||||
|
.fa-pulse,
|
||||||
|
.fa-shake,
|
||||||
|
.fa-spin,
|
||||||
|
.fa-spin-pulse {
|
||||||
|
animation-delay: -1ms;
|
||||||
|
animation-duration: 1ms;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
transition-delay: 0s;
|
||||||
|
transition-duration: 0s; } }
|
||||||
|
|
||||||
|
@keyframes fa-beat {
|
||||||
|
0%, 90% {
|
||||||
|
transform: scale(1); }
|
||||||
|
45% {
|
||||||
|
transform: scale(var(--fa-beat-scale, 1.25)); } }
|
||||||
|
|
||||||
|
@keyframes fa-bounce {
|
||||||
|
0% {
|
||||||
|
transform: scale(1, 1) translateY(0); }
|
||||||
|
10% {
|
||||||
|
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
|
||||||
|
30% {
|
||||||
|
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
|
||||||
|
50% {
|
||||||
|
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
|
||||||
|
57% {
|
||||||
|
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
|
||||||
|
64% {
|
||||||
|
transform: scale(1, 1) translateY(0); }
|
||||||
|
100% {
|
||||||
|
transform: scale(1, 1) translateY(0); } }
|
||||||
|
|
||||||
|
@keyframes fa-fade {
|
||||||
|
50% {
|
||||||
|
opacity: var(--fa-fade-opacity, 0.4); } }
|
||||||
|
|
||||||
|
@keyframes fa-beat-fade {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: var(--fa-beat-fade-opacity, 0.4);
|
||||||
|
transform: scale(1); }
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
|
||||||
|
|
||||||
|
@keyframes fa-flip {
|
||||||
|
50% {
|
||||||
|
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
|
||||||
|
|
||||||
|
@keyframes fa-shake {
|
||||||
|
0% {
|
||||||
|
transform: rotate(-15deg); }
|
||||||
|
4% {
|
||||||
|
transform: rotate(15deg); }
|
||||||
|
8%, 24% {
|
||||||
|
transform: rotate(-18deg); }
|
||||||
|
12%, 28% {
|
||||||
|
transform: rotate(18deg); }
|
||||||
|
16% {
|
||||||
|
transform: rotate(-22deg); }
|
||||||
|
20% {
|
||||||
|
transform: rotate(22deg); }
|
||||||
|
32% {
|
||||||
|
transform: rotate(-12deg); }
|
||||||
|
36% {
|
||||||
|
transform: rotate(12deg); }
|
||||||
|
40%, 100% {
|
||||||
|
transform: rotate(0deg); } }
|
||||||
|
|
||||||
|
@keyframes fa-spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg); }
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
.fa-rotate-90 {
|
||||||
|
transform: rotate(90deg); }
|
||||||
|
|
||||||
|
.fa-rotate-180 {
|
||||||
|
transform: rotate(180deg); }
|
||||||
|
|
||||||
|
.fa-rotate-270 {
|
||||||
|
transform: rotate(270deg); }
|
||||||
|
|
||||||
|
.fa-flip-horizontal {
|
||||||
|
transform: scale(-1, 1); }
|
||||||
|
|
||||||
|
.fa-flip-vertical {
|
||||||
|
transform: scale(1, -1); }
|
||||||
|
|
||||||
|
.fa-flip-both,
|
||||||
|
.fa-flip-horizontal.fa-flip-vertical {
|
||||||
|
transform: scale(-1, -1); }
|
||||||
|
|
||||||
|
.fa-rotate-by {
|
||||||
|
transform: rotate(var(--fa-rotate-angle, 0)); }
|
||||||
|
|
||||||
|
.fa-stack {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 2em;
|
||||||
|
position: relative;
|
||||||
|
width: 2.5em; }
|
||||||
|
|
||||||
|
.fa-stack-1x,
|
||||||
|
.fa-stack-2x {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: var(--fa-stack-z-index, auto); }
|
||||||
|
|
||||||
|
.svg-inline--fa.fa-stack-1x {
|
||||||
|
height: 1em;
|
||||||
|
width: 1.25em; }
|
||||||
|
|
||||||
|
.svg-inline--fa.fa-stack-2x {
|
||||||
|
height: 2em;
|
||||||
|
width: 2.5em; }
|
||||||
|
|
||||||
|
.fa-inverse {
|
||||||
|
color: var(--fa-inverse, #fff); }
|
||||||
|
|
||||||
|
.sr-only,
|
||||||
|
.fa-sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0; }
|
||||||
|
|
||||||
|
.sr-only-focusable:not(:focus),
|
||||||
|
.fa-sr-only-focusable:not(:focus) {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0; }
|
||||||
|
|
||||||
|
.svg-inline--fa .fa-primary {
|
||||||
|
fill: var(--fa-primary-color, currentColor);
|
||||||
|
opacity: var(--fa-primary-opacity, 1); }
|
||||||
|
|
||||||
|
.svg-inline--fa .fa-secondary {
|
||||||
|
fill: var(--fa-secondary-color, currentColor);
|
||||||
|
opacity: var(--fa-secondary-opacity, 0.4); }
|
||||||
|
|
||||||
|
.svg-inline--fa.fa-swap-opacity .fa-primary {
|
||||||
|
opacity: var(--fa-secondary-opacity, 0.4); }
|
||||||
|
|
||||||
|
.svg-inline--fa.fa-swap-opacity .fa-secondary {
|
||||||
|
opacity: var(--fa-primary-opacity, 1); }
|
||||||
|
|
||||||
|
.svg-inline--fa mask .fa-primary,
|
||||||
|
.svg-inline--fa mask .fa-secondary {
|
||||||
|
fill: black; }
|
||||||
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/css/svg-with-js.min.css
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FontAwesome';
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FontAwesome';
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FontAwesome';
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype");
|
||||||
|
unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FontAwesome';
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype");
|
||||||
|
unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F27A; }
|
||||||
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/css/v4-font-face.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}
|
||||||
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/css/v4-shims.min.css
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 5 Brands';
|
||||||
|
font-display: block;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-display: block;
|
||||||
|
font-weight: 900;
|
||||||
|
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-display: block;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
|
||||||
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/css/v5-font-face.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
|
* Copyright 2024 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}
|
||||||
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/js/all.min.js
vendored
Normal file
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/js/brands.min.js
vendored
Normal file
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/js/conflict-detection.min.js
vendored
Normal file
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/js/fontawesome.min.js
vendored
Normal file
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/js/regular.min.js
vendored
Normal file
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/js/solid.min.js
vendored
Normal file
6
mengyadriftbottle-frontend/static/fontawesome-free-6.7.2-web/js/v4-shims.min.js
vendored
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
// animating icons
|
||||||
|
// --------------------------
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-beat {
|
||||||
|
animation-name: ~'@{fa-css-prefix}-beat';
|
||||||
|
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
|
||||||
|
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
|
||||||
|
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
|
||||||
|
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
|
||||||
|
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, ease-in-out)';
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-bounce {
|
||||||
|
animation-name: ~'@{fa-css-prefix}-bounce';
|
||||||
|
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
|
||||||
|
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
|
||||||
|
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
|
||||||
|
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
|
||||||
|
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, cubic-bezier(0.280, 0.840, 0.420, 1))';
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-fade {
|
||||||
|
animation-name: ~'@{fa-css-prefix}-fade';
|
||||||
|
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
|
||||||
|
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
|
||||||
|
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
|
||||||
|
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
|
||||||
|
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, cubic-bezier(.4,0,.6,1))';
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-beat-fade {
|
||||||
|
animation-name: ~'@{fa-css-prefix}-beat-fade';
|
||||||
|
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
|
||||||
|
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
|
||||||
|
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
|
||||||
|
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
|
||||||
|
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, cubic-bezier(.4,0,.6,1))';
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-flip {
|
||||||
|
animation-name: ~'@{fa-css-prefix}-flip';
|
||||||
|
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
|
||||||
|
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
|
||||||
|
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
|
||||||
|
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
|
||||||
|
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, ease-in-out)';
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-shake {
|
||||||
|
animation-name: ~'@{fa-css-prefix}-shake';
|
||||||
|
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
|
||||||
|
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
|
||||||
|
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
|
||||||
|
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
|
||||||
|
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, linear)';
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-spin {
|
||||||
|
animation-name: ~'@{fa-css-prefix}-spin';
|
||||||
|
animation-delay: ~'var(--@{fa-css-prefix}-animation-delay, 0s)';
|
||||||
|
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
|
||||||
|
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 2s)';
|
||||||
|
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
|
||||||
|
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, linear)';
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-spin-reverse {
|
||||||
|
--@{fa-css-prefix}-animation-direction: reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-pulse,
|
||||||
|
.@{fa-css-prefix}-spin-pulse {
|
||||||
|
animation-name: ~'@{fa-css-prefix}-spin';
|
||||||
|
animation-direction: ~'var(--@{fa-css-prefix}-animation-direction, normal)';
|
||||||
|
animation-duration: ~'var(--@{fa-css-prefix}-animation-duration, 1s)';
|
||||||
|
animation-iteration-count: ~'var(--@{fa-css-prefix}-animation-iteration-count, infinite)';
|
||||||
|
animation-timing-function: ~'var(--@{fa-css-prefix}-animation-timing, steps(8));';
|
||||||
|
}
|
||||||
|
|
||||||
|
// if agent or operating system prefers reduced motion, disable animations
|
||||||
|
// see: https://www.smashingmagazine.com/2020/09/design-reduced-motion-sensitivities/
|
||||||
|
// see: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.@{fa-css-prefix}-beat,
|
||||||
|
.@{fa-css-prefix}-bounce,
|
||||||
|
.@{fa-css-prefix}-fade,
|
||||||
|
.@{fa-css-prefix}-beat-fade,
|
||||||
|
.@{fa-css-prefix}-flip,
|
||||||
|
.@{fa-css-prefix}-pulse,
|
||||||
|
.@{fa-css-prefix}-shake,
|
||||||
|
.@{fa-css-prefix}-spin,
|
||||||
|
.@{fa-css-prefix}-spin-pulse {
|
||||||
|
animation-delay: -1ms;
|
||||||
|
animation-duration: 1ms;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
transition-delay: 0s;
|
||||||
|
transition-duration: 0s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ~'@{fa-css-prefix}-beat' {
|
||||||
|
0%, 90% { transform: scale(1); }
|
||||||
|
45% { transform: ~'scale(var(--@{fa-css-prefix}-beat-scale, 1.25))'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ~'@{fa-css-prefix}-bounce' {
|
||||||
|
0% { transform: scale(1,1) translateY(0); }
|
||||||
|
10% { transform: ~'scale(var(--@{fa-css-prefix}-bounce-start-scale-x, 1.1),var(--@{fa-css-prefix}-bounce-start-scale-y, 0.9))' translateY(0); }
|
||||||
|
30% { transform: ~'scale(var(--@{fa-css-prefix}-bounce-jump-scale-x, 0.9),var(--@{fa-css-prefix}-bounce-jump-scale-y, 1.1))' ~'translateY(var(--@{fa-css-prefix}-bounce-height, -0.5em))'; }
|
||||||
|
50% { transform: ~'scale(var(--@{fa-css-prefix}-bounce-land-scale-x, 1.05),var(--@{fa-css-prefix}-bounce-land-scale-y, 0.95))' translateY(0); }
|
||||||
|
57% { transform: ~'scale(1,1) translateY(var(--@{fa-css-prefix}-bounce-rebound, -0.125em))'; }
|
||||||
|
64% { transform: scale(1,1) translateY(0); }
|
||||||
|
100% { transform: scale(1,1) translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ~'@{fa-css-prefix}-fade' {
|
||||||
|
50% { opacity: ~'var(--@{fa-css-prefix}-fade-opacity, 0.4)'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ~'@{fa-css-prefix}-beat-fade' {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: ~'var(--@{fa-css-prefix}-beat-fade-opacity, 0.4)';
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: ~'scale(var(--@{fa-css-prefix}-beat-fade-scale, 1.125))';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ~'@{fa-css-prefix}-flip' {
|
||||||
|
50% {
|
||||||
|
transform: ~'rotate3d(var(--@{fa-css-prefix}-flip-x, 0), var(--@{fa-css-prefix}-flip-y, 1), var(--@{fa-css-prefix}-flip-z, 0), var(--@{fa-css-prefix}-flip-angle, -180deg))';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ~'@{fa-css-prefix}-shake' {
|
||||||
|
0% { transform: rotate(-15deg); }
|
||||||
|
4% { transform: rotate(15deg); }
|
||||||
|
8%, 24% { transform: rotate(-18deg); }
|
||||||
|
12%, 28% { transform: rotate(18deg); }
|
||||||
|
16% { transform: rotate(-22deg); }
|
||||||
|
20% { transform: rotate(22deg); }
|
||||||
|
32% { transform: rotate(-12deg); }
|
||||||
|
36% { transform: rotate(12deg); }
|
||||||
|
40%, 100% { transform: rotate(0deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ~'@{fa-css-prefix}-spin' {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// bordered + pulled icons
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-border {
|
||||||
|
border-color: ~'var(--@{fa-css-prefix}-border-color, @{fa-border-color})';
|
||||||
|
border-radius: ~'var(--@{fa-css-prefix}-border-radius, @{fa-border-radius})';
|
||||||
|
border-style: ~'var(--@{fa-css-prefix}-border-style, @{fa-border-style})';
|
||||||
|
border-width: ~'var(--@{fa-css-prefix}-border-width, @{fa-border-width})';
|
||||||
|
padding: ~'var(--@{fa-css-prefix}-border-padding, @{fa-border-padding})';
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-pull-left {
|
||||||
|
float: left;
|
||||||
|
margin-right: ~'var(--@{fa-css-prefix}-pull-margin, @{fa-pull-margin})';
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-pull-right {
|
||||||
|
float: right;
|
||||||
|
margin-left: ~'var(--@{fa-css-prefix}-pull-margin, @{fa-pull-margin})';
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// base icon class definition
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
.@{fa-css-prefix} {
|
||||||
|
font-family: ~"var(--@{fa-css-prefix}-style-family, '@{fa-style-family}')";
|
||||||
|
font-weight: ~'var(--@{fa-css-prefix}-style, @{fa-style})';
|
||||||
|
}
|
||||||
|
|
||||||
|
.fas,
|
||||||
|
.far,
|
||||||
|
.fab,
|
||||||
|
.@{fa-css-prefix}-solid,
|
||||||
|
.@{fa-css-prefix}-regular,
|
||||||
|
.@{fa-css-prefix}-brands,
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-sharp-solid,
|
||||||
|
.@{fa-css-prefix}-classic,
|
||||||
|
.@{fa-css-prefix} {
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
display: ~'var(--@{fa-css-prefix}-display, @{fa-display})';
|
||||||
|
font-style: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
line-height: 1;
|
||||||
|
text-rendering: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fas::before,
|
||||||
|
.far::before,
|
||||||
|
.fab::before,
|
||||||
|
.@{fa-css-prefix}-solid::before,
|
||||||
|
.@{fa-css-prefix}-regular::before,
|
||||||
|
.@{fa-css-prefix}-brands::before,
|
||||||
|
.@{fa-css-prefix}::before {
|
||||||
|
content: ~'var(@{fa-icon-property})';
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-classic,
|
||||||
|
.fas,
|
||||||
|
.@{fa-css-prefix}-solid,
|
||||||
|
.far,
|
||||||
|
.@{fa-css-prefix}-regular {
|
||||||
|
font-family: 'Font Awesome 6 Free';
|
||||||
|
}
|
||||||
|
.@{fa-css-prefix}-brands,
|
||||||
|
.fab {
|
||||||
|
font-family: 'Font Awesome 6 Brands';
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// fixed-width icons
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-fw {
|
||||||
|
text-align: center;
|
||||||
|
width: @fa-fw-width;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// specific icon class definition
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
|
||||||
|
readers do not read off random characters that represent icons */
|
||||||
|
|
||||||
|
each(.fa-icons(), {
|
||||||
|
.@{fa-css-prefix}-@{key} {
|
||||||
|
@{fa-icon-property}: @value;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// icons in a list
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-ul {
|
||||||
|
list-style-type: none;
|
||||||
|
margin-left: ~'var(--@{fa-css-prefix}-li-margin, @{fa-li-margin})';
|
||||||
|
padding-left: 0;
|
||||||
|
|
||||||
|
> li { position: relative; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-li {
|
||||||
|
left: calc(~'var(--@{fa-css-prefix}-li-width, @{fa-li-width})' * -1);
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
width: ~'var(--@{fa-css-prefix}-li-width, @{fa-li-width})';
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// mixins
|
||||||
|
// --------------------------
|
||||||
|
|
||||||
|
// base rendering for an icon
|
||||||
|
.fa-icon() {
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
display: inline-block;
|
||||||
|
font-style: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sets relative font-sizing and alignment (in _sizing)
|
||||||
|
.fa-size(@font-size) {
|
||||||
|
font-size: (@font-size / @fa-size-scale-base) * 1em; // converts step in sizing scale into an em-based value that's relative to the scale's base
|
||||||
|
line-height: (1 / @font-size) * 1em; // sets the line-height of the icon back to that of it's parent
|
||||||
|
vertical-align: ((6 / @font-size) - (3 / 8)) * 1em; // vertically centers the icon taking into account the surrounding text's descender
|
||||||
|
}
|
||||||
|
|
||||||
|
// only display content to screen readers
|
||||||
|
// see: https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/
|
||||||
|
// see: https://hugogiraudel.com/2016/10/13/css-hide-and-seek/
|
||||||
|
.fa-sr-only() {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// use in conjunction with .sr-only to only display content when it's focused
|
||||||
|
.fa-sr-only-focusable() {
|
||||||
|
&:not(:focus) {
|
||||||
|
.fa-sr-only();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sets a specific icon family to use alongside style + icon mixins
|
||||||
|
.fa-family-classic() {
|
||||||
|
&:extend(.fa-classic all);
|
||||||
|
}
|
||||||
|
|
||||||
|
// convenience mixins for declaring pseudo-elements by CSS variable,
|
||||||
|
// including all style-specific font properties
|
||||||
|
.fa-icon-solid(@fa-var) {
|
||||||
|
&:extend(.fa-solid all);
|
||||||
|
|
||||||
|
& { @{fa-icon-property}: @fa-var; @{fa-duotone-icon-property}: %("%s%s", @fa-var, @fa-var); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-icon-regular(@fa-var) {
|
||||||
|
&:extend(.fa-regular all);
|
||||||
|
|
||||||
|
& { @{fa-icon-property}: @fa-var; @{fa-duotone-icon-property}: %("%s%s", @fa-var, @fa-var); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-icon-brands(@fa-var) {
|
||||||
|
&:extend(.fa-brands all);
|
||||||
|
|
||||||
|
& { @{fa-icon-property}: @fa-var; @{fa-duotone-icon-property}: %("%s%s", @fa-var, @fa-var); }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// rotating + flipping icons
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-rotate-90 {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-rotate-180 {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-rotate-270 {
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-flip-horizontal {
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-flip-vertical {
|
||||||
|
transform: scale(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-flip-both,
|
||||||
|
.@{fa-css-prefix}-flip-horizontal.@{fa-css-prefix}-flip-vertical {
|
||||||
|
transform: scale(-1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{fa-css-prefix}-rotate-by {
|
||||||
|
transform: rotate(~'var(--@{fa-css-prefix}-rotate-angle, 0)');
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// screen-reader utilities
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
// only display content to screen readers
|
||||||
|
.sr-only,
|
||||||
|
.@{fa-css-prefix}-sr-only {
|
||||||
|
.fa-sr-only();
|
||||||
|
}
|
||||||
|
|
||||||
|
// use in conjunction with .sr-only to only display content when it's focused
|
||||||
|
.sr-only-focusable,
|
||||||
|
.@{fa-css-prefix}-sr-only-focusable {
|
||||||
|
.fa-sr-only-focusable();
|
||||||
|
}
|
||||||