From 5eecbecd7f8dc978b041484829fa5b4dda2e425d Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 22 Jul 2026 11:12:19 +0800 Subject: [PATCH] feat: align portal center panel with side cards + redesign login page - Portal: wrap .panel-head in matching card (border, radius, shadow) with cyan->purple left accent bar; h2 28px -> 20px; description right-aligned with vertical divider; top edge now aligns with side-menu & links-panel - Login: switch from absolute-positioned icons to el-input prefix slot; layout brand row (logo + title + uppercase subtitle); add form-title section with gradient bar; consistent cyan focus; gradient button; dashed top divider for back-link; card width 420 -> 460 - Add .gitignore (node_modules, dist, __pycache__, .env, etc.) --- .gitignore | 46 + README.md | 155 ++ backend/app.py | 57 + backend/auth.py | 45 + backend/config.py | 17 + backend/extensions.py | 3 + backend/models.py | 153 ++ backend/requirements.txt | 6 + backend/routes/__init__.py | 1 + backend/routes/admin.py | 427 ++++++ backend/routes/auth.py | 44 + backend/routes/portal.py | 94 ++ backend/seed.py | 207 +++ frontend/index.html | 12 + frontend/package-lock.json | 1870 +++++++++++++++++++++++ frontend/package.json | 22 + frontend/src/App.vue | 3 + frontend/src/api/index.js | 105 ++ frontend/src/main.js | 16 + frontend/src/router/index.js | 42 + frontend/src/styles/portal.css | 1262 +++++++++++++++ frontend/src/utils/auth.js | 13 + frontend/src/views/Portal.vue | 332 ++++ frontend/src/views/admin/Categories.vue | 233 +++ frontend/src/views/admin/Dashboard.vue | 373 +++++ frontend/src/views/admin/Layout.vue | 391 +++++ frontend/src/views/admin/Links.vue | 107 ++ frontend/src/views/admin/Login.vue | 504 ++++++ frontend/src/views/admin/Resources.vue | 635 ++++++++ frontend/src/views/admin/Settings.vue | 59 + frontend/src/views/admin/Users.vue | 119 ++ frontend/vite.config.js | 18 + 32 files changed, 7371 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/app.py create mode 100644 backend/auth.py create mode 100644 backend/config.py create mode 100644 backend/extensions.py create mode 100644 backend/models.py create mode 100644 backend/requirements.txt create mode 100644 backend/routes/__init__.py create mode 100644 backend/routes/admin.py create mode 100644 backend/routes/auth.py create mode 100644 backend/routes/portal.py create mode 100644 backend/seed.py create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api/index.js create mode 100644 frontend/src/main.js create mode 100644 frontend/src/router/index.js create mode 100644 frontend/src/styles/portal.css create mode 100644 frontend/src/utils/auth.js create mode 100644 frontend/src/views/Portal.vue create mode 100644 frontend/src/views/admin/Categories.vue create mode 100644 frontend/src/views/admin/Dashboard.vue create mode 100644 frontend/src/views/admin/Layout.vue create mode 100644 frontend/src/views/admin/Links.vue create mode 100644 frontend/src/views/admin/Login.vue create mode 100644 frontend/src/views/admin/Resources.vue create mode 100644 frontend/src/views/admin/Settings.vue create mode 100644 frontend/src/views/admin/Users.vue create mode 100644 frontend/vite.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61b93dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.egg-info/ +.venv/ +venv/ +env/ +.eggs/ +*.egg +.pytest_cache/ +.mypy_cache/ + +# Flask instance +instance/ +*.db +*.sqlite +*.sqlite3 + +# Logs +*.log +logs/ + +# Node / Vite +node_modules/ +dist/ +.vite/ +.cache/ + +# Env / secrets +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store +Thumbs.db + +# Misc +.coverage +htmlcov/ +.tox/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a87575 --- /dev/null +++ b/README.md @@ -0,0 +1,155 @@ +# OPS 资源中心 + +一个现代化的内部资源管理门户,用于集中管理运维命令、文档、链接和文件。 + +## 功能特性 + +### 门户展示 +- 🎨 现代化响应式设计 +- 🔍 全局搜索功能,支持按 `/` 快捷键搜索 +- 🏷️ 多维度分类筛选 +- 📋 资源卡片展示,支持一键复制命令 +- 🔗 右侧快捷链接栏 + +### 管理后台 +- 📊 数据仪表盘 +- 📁 分类管理(支持排序、分面选项) +- 📄 资源管理(支持 4 种类型:命令/链接/文档/文件) +- 📎 文件上传功能 +- 🔗 侧栏链接管理 +- ⚙️ 站点设置(标题、副标题、公告等) +- 👥 用户管理(创建、重置密码、删除) + +## 技术栈 + +### 后端 +- **Flask** - Web 框架 +- **Flask-SQLAlchemy** - ORM +- **SQLite** - 数据库 +- **JWT** - 身份认证 +- **bcrypt** - 密码加密 + +### 前端 +- **Vue 3** - 前端框架 +- **Element Plus** - UI 组件库 +- **Vite** - 构建工具 +- **Axios** - HTTP 客户端 + +## 快速开始 + +### 环境要求 +- Python 3.8+ +- Node.js 16+ + +### 后端启动 + +```bash +cd backend + +# 安装依赖 +pip install -r requirements.txt + +# 启动服务(开发模式) +python app.py +``` + +服务将在 `http://localhost:8090` 启动 + +### 前端启动 + +```bash +cd frontend + +# 安装依赖 +npm install + +# 开发模式 +npm run dev + +# 构建生产版本 +npm run build +``` + +## 默认账号 + +- 用户名: `admin` +- 密码: `admin123` + +## 项目结构 + +``` +ops-manager/ +├── backend/ # 后端服务 +│ ├── app.py # 应用入口 +│ ├── config.py # 配置文件 +│ ├── models.py # 数据模型 +│ ├── auth.py # 认证相关 +│ ├── routes/ # API 路由 +│ │ ├── portal.py # 门户公开 API +│ │ ├── admin.py # 管理后台 API +│ │ └── auth.py # 认证 API +│ ├── instance/ # 数据库文件(自动创建) +│ ├── uploads/ # 上传文件目录(自动创建) +│ └── requirements.txt # Python 依赖 +└── frontend/ # 前端应用 + ├── src/ + │ ├── views/ # 页面组件 + │ │ ├── Portal.vue # 门户首页 + │ │ └── admin/ # 管理后台页面 + │ ├── api/index.js # API 封装 + │ ├── router/ # 路由配置 + │ └── styles/ # 样式文件 + ├── dist/ # 构建产物(自动创建) + └── package.json +``` + +## 资源类型 + +1. **命令 (command)** - 可一键复制的 Shell 命令 +2. **链接 (link)** - 内部/外部链接跳转 +3. **文档 (text)** - 富文本说明文档 +4. **文件 (file)** - 可下载的文件资源 + +## 环境变量 + +| 变量名 | 说明 | 默认值 | +|--------|------|--------| +| `OPS_SECRET_KEY` | JWT 加密密钥 | `ops-portal-dev-secret-change-me-32b!` | +| `OPS_DATABASE_URL` | 数据库连接字符串 | `sqlite:///instance/ops_portal.db` | +| `OPS_JWT_EXPIRE_HOURS` | JWT 过期时间(小时) | `12` | +| `OPS_UPLOAD_DIR` | 文件上传目录 | `backend/uploads` | +| `OPS_UPLOAD_URL_PREFIX` | 文件访问 URL 前缀 | `/uploads` | + +## 部署建议 + +1. **生产环境**使用 Gunicorn 或 uWSGI 部署后端 +2. 使用 Nginx 反向代理前端静态文件和 API +3. 配置环境变量 `OPS_SECRET_KEY` 为强随机字符串 +4. 定期备份 SQLite 数据库文件 + +## 开发 + +### 添加新的资源类型 + +1. 在 `backend/models.py` 中更新 `Resource` 模型(如果需要) +2. 在 `backend/routes/admin.py` 中更新 `RESOURCE_TYPES` 和验证逻辑 +3. 在前端 `Resources.vue` 中添加对应展示样式 +4. 在门户 `Portal.vue` 中添加 badge 样式 + +### 自定义主题 + +主要样式变量定义在 `frontend/src/styles/portal.css` 的 `:root` 块中: + +```css +:root { + --ink: #0a1424; /* 主深色 */ + --cyan: #2dd9f0; /* 主青色 */ + --red: #e5484d; /* 主红色 */ + --paper: #f2f5f9; /* 背景色 */ + /* ... */ +} +``` + +## License + +MIT diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..0f83749 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,57 @@ +"""App factory. Serves API + (in production) the built Vue frontend.""" +import os + +from flask import Flask, send_from_directory + +from config import BASE_DIR, Config, UPLOAD_DIR, UPLOAD_URL_PREFIX +from extensions import db + +DIST_DIR = os.path.join(BASE_DIR, "..", "frontend", "dist") + + +def create_app(): + app = Flask(__name__, static_folder=None) + app.config.from_object(Config) + + os.makedirs(os.path.join(BASE_DIR, "instance"), exist_ok=True) + db.init_app(app) + db.app = app + + from routes import auth as auth_routes + from routes import portal as portal_routes + from routes import admin as admin_routes + app.register_blueprint(auth_routes.bp) + app.register_blueprint(portal_routes.bp) + app.register_blueprint(admin_routes.bp) + + @app.route("/api/health") + def health(): + return {"status": "ok"} + + # Serve uploaded files + os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True) + @app.route(UPLOAD_URL_PREFIX + "/") + def uploaded_file(filename): + return send_from_directory(Config.UPLOAD_FOLDER, filename) + + # ---- Serve built Vue SPA in production (after `npm run build`) ---- + if os.path.isdir(DIST_DIR): + @app.route("/", defaults={"path": ""}) + @app.route("/") + def spa(path): + full = os.path.join(DIST_DIR, path) + if path and os.path.isfile(full): + return send_from_directory(DIST_DIR, path) + return send_from_directory(DIST_DIR, "index.html") + + return app + + +app = create_app() + +if __name__ == "__main__": + with app.app_context(): + db.create_all() + from seed import seed_all + seed_all() + app.run(host="0.0.0.0", port=8090, debug=False) diff --git a/backend/auth.py b/backend/auth.py new file mode 100644 index 0000000..e8c3060 --- /dev/null +++ b/backend/auth.py @@ -0,0 +1,45 @@ +"""Auth helpers: JWT issue/verify + admin guard.""" +import functools +from datetime import datetime, timedelta, timezone + +import jwt +from flask import current_app, jsonify, request + +from models import User + + +def issue_token(user): + payload = { + "user_id": user.id, + "username": user.username, + "exp": datetime.now(timezone.utc) + timedelta(hours=current_app.config["JWT_EXPIRE_HOURS"]), + "iat": datetime.now(timezone.utc), + } + return jwt.encode(payload, current_app.config["SECRET_KEY"], algorithm="HS256") + + +def decode_token(token): + try: + return jwt.decode(token, current_app.config["SECRET_KEY"], algorithms=["HS256"]) + except jwt.PyJWTError: + return None + + +def get_current_user(): + auth = request.headers.get("Authorization", "") + if not auth.startswith("Bearer "): + return None + payload = decode_token(auth[7:]) + if not payload: + return None + return User.query.get(payload["user_id"]) + + +def admin_required(fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + user = get_current_user() + if not user: + return jsonify({"error": "未登录或登录已过期"}), 401 + return fn(*args, **kwargs) + return wrapper diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..f8970c6 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,17 @@ +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +UPLOAD_DIR = os.environ.get("OPS_UPLOAD_DIR", os.path.join(BASE_DIR, "uploads")) +UPLOAD_URL_PREFIX = os.environ.get("OPS_UPLOAD_URL_PREFIX", "/uploads") + + +class Config: + SECRET_KEY = os.environ.get("OPS_SECRET_KEY", "ops-portal-dev-secret-change-me-32b!") + SQLALCHEMY_DATABASE_URI = os.environ.get( + "OPS_DATABASE_URL", "sqlite:///" + os.path.join(BASE_DIR, "instance", "ops_portal.db") + ) + SQLALCHEMY_TRACK_MODIFICATIONS = False + JWT_EXPIRE_HOURS = int(os.environ.get("OPS_JWT_EXPIRE_HOURS", "12")) + MAX_CONTENT_LENGTH = 32 * 1024 * 1024 + JSON_AS_ASCII = False + UPLOAD_FOLDER = UPLOAD_DIR diff --git a/backend/extensions.py b/backend/extensions.py new file mode 100644 index 0000000..f0b13d6 --- /dev/null +++ b/backend/extensions.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..5b3a3e7 --- /dev/null +++ b/backend/models.py @@ -0,0 +1,153 @@ +"""SQLAlchemy models for OPS Resource Portal.""" +from datetime import datetime, timezone + +from extensions import db + + +def utcnow(): + return datetime.now(timezone.utc) + + +# --------------------------------------------------------------------------- +# Association table: resource <-> option (faceted tagging) +# --------------------------------------------------------------------------- +resource_options = db.Table( + "resource_options", + db.Column("resource_id", db.Integer, db.ForeignKey("resources.id", ondelete="CASCADE"), primary_key=True), + db.Column("option_id", db.Integer, db.ForeignKey("options.id", ondelete="CASCADE"), primary_key=True), +) + + +class User(db.Model): + __tablename__ = "users" + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + password = db.Column(db.String(128), nullable=False) # bcrypt hash + created_at = db.Column(db.DateTime, default=utcnow) + + def set_password(self, raw): + import bcrypt + self.password = bcrypt.hashpw(raw.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") + + def check_password(self, raw): + import bcrypt + try: + return bcrypt.checkpw(raw.encode("utf-8"), self.password.encode("utf-8")) + except Exception: + return False + + +class SiteSetting(db.Model): + """Key-value site configuration editable from the admin panel.""" + __tablename__ = "site_settings" + key = db.Column(db.String(64), primary_key=True) + value = db.Column(db.Text, default="") + updated_at = db.Column(db.DateTime, default=utcnow, onupdate=utcnow) + + +class Category(db.Model): + """Left-nav menu entry, e.g. 基础设备 / pypi源.""" + __tablename__ = "categories" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80), nullable=False) + icon = db.Column(db.String(16), default="📁") # emoji icon + description = db.Column(db.String(255), default="") + sort_order = db.Column(db.Integer, default=0) + is_active = db.Column(db.Boolean, default=True) + created_at = db.Column(db.DateTime, default=utcnow) + + rows = db.relationship("OptionRow", backref="category", lazy=True, + cascade="all, delete-orphan", order_by="OptionRow.sort_order") + resources = db.relationship("Resource", backref="category", lazy=True, + cascade="all, delete-orphan") + + def to_dict(self, with_rows=True): + d = { + "id": self.id, "name": self.name, "icon": self.icon, + "description": self.description, "sort_order": self.sort_order, + "is_active": self.is_active, + "resource_count": len(self.resources), + } + if with_rows: + d["rows"] = [r.to_dict() for r in self.rows] + return d + + +class OptionRow(db.Model): + """A selection row inside a category, e.g. 运行环境 / 操作系统.""" + __tablename__ = "option_rows" + id = db.Column(db.Integer, primary_key=True) + category_id = db.Column(db.Integer, db.ForeignKey("categories.id", ondelete="CASCADE"), nullable=False) + title = db.Column(db.String(80), nullable=False) # e.g. 运行环境 + sort_order = db.Column(db.Integer, default=0) + + options = db.relationship("Option", backref="row", lazy=True, + cascade="all, delete-orphan", order_by="Option.sort_order") + + def to_dict(self): + return { + "id": self.id, "title": self.title, "sort_order": self.sort_order, + "options": [o.to_dict() for o in self.options], + } + + +class Option(db.Model): + """A selectable tile inside a row, e.g. 物理机 / ubuntu.""" + __tablename__ = "options" + id = db.Column(db.Integer, primary_key=True) + row_id = db.Column(db.Integer, db.ForeignKey("option_rows.id", ondelete="CASCADE"), nullable=False) + label = db.Column(db.String(80), nullable=False) + icon = db.Column(db.String(16), default="") + sort_order = db.Column(db.Integer, default=0) + + def to_dict(self): + return {"id": self.id, "label": self.label, "icon": self.icon, "sort_order": self.sort_order} + + +class Resource(db.Model): + """A published internal resource: command / link / text / file.""" + __tablename__ = "resources" + id = db.Column(db.Integer, primary_key=True) + category_id = db.Column(db.Integer, db.ForeignKey("categories.id", ondelete="CASCADE"), nullable=False) + title = db.Column(db.String(160), nullable=False) + type = db.Column(db.String(16), nullable=False, default="command") # command|link|text|file + content = db.Column(db.Text, default="") # command text / rich text / note + url = db.Column(db.String(500), default="") # link target / file URL + description = db.Column(db.String(255), default="") + sort_order = db.Column(db.Integer, default=0) + is_active = db.Column(db.Boolean, default=True) + created_at = db.Column(db.DateTime, default=utcnow) + updated_at = db.Column(db.DateTime, default=utcnow, onupdate=utcnow) + + options = db.relationship("Option", secondary=resource_options, lazy="subquery", + backref=db.backref("resources", lazy=True)) + + def to_dict(self): + return { + "id": self.id, "category_id": self.category_id, "title": self.title, + "type": self.type, "content": self.content, "url": self.url, + "description": self.description, "sort_order": self.sort_order, + "is_active": self.is_active, + "option_ids": sorted(o.id for o in self.options), + "created_at": self.created_at.strftime("%Y-%m-%d %H:%M") if self.created_at else "", + "updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M") if self.updated_at else "", + } + + +class SideLink(db.Model): + """Right-panel 其他链接 entry.""" + __tablename__ = "side_links" + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(160), nullable=False) + url = db.Column(db.String(500), nullable=False) + group_name = db.Column(db.String(80), default="其他链接") + sort_order = db.Column(db.Integer, default=0) + is_active = db.Column(db.Boolean, default=True) + created_at = db.Column(db.DateTime, default=utcnow) + + def to_dict(self): + return { + "id": self.id, "title": self.title, "url": self.url, + "group_name": self.group_name, "sort_order": self.sort_order, + "is_active": self.is_active, + } diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..80877e1 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,6 @@ +Flask>=3.0 +Flask-SQLAlchemy>=3.1 +Flask-Cors>=4.0 +PyJWT>=2.8 +bcrypt>=4.0 +gunicorn>=21.2 diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py new file mode 100644 index 0000000..1eb1a2b --- /dev/null +++ b/backend/routes/__init__.py @@ -0,0 +1 @@ +# intentionally empty — prevents circular imports (see app factory) diff --git a/backend/routes/admin.py b/backend/routes/admin.py new file mode 100644 index 0000000..f9f1a8a --- /dev/null +++ b/backend/routes/admin.py @@ -0,0 +1,427 @@ +"""Admin CRUD API — all endpoints require a valid JWT.""" +import os +import uuid +from werkzeug.utils import secure_filename +from flask import Blueprint, jsonify, request, current_app + +from auth import admin_required +from config import UPLOAD_URL_PREFIX +from extensions import db +from models import Category, Option, OptionRow, Resource, SideLink, SiteSetting, User + +bp = Blueprint("admin", __name__, url_prefix="/api/admin") + +RESOURCE_TYPES = {"command", "link", "text", "file"} +ALLOWED_EXTENSIONS = {"txt", "pdf", "png", "jpg", "jpeg", "gif", "zip", "tar", "gz", "sh", "py", "json", "yaml", "yml", "deb", "rpm", "iso"} + + +def allowed_file(filename): + return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS + + +# --------------------------------------------------------------------------- +# Dashboard stats +# --------------------------------------------------------------------------- +@bp.route("/stats") +@admin_required +def stats(): + return jsonify({ + "categories": Category.query.count(), + "resources": Resource.query.count(), + "active_resources": Resource.query.filter_by(is_active=True).count(), + "links": SideLink.query.count(), + "options": Option.query.count(), + }) + + +# --------------------------------------------------------------------------- +# Categories +# --------------------------------------------------------------------------- +@bp.route("/categories") +@admin_required +def list_categories(): + cats = Category.query.order_by(Category.sort_order, Category.id).all() + return jsonify([c.to_dict() for c in cats]) + + +@bp.route("/categories", methods=["POST"]) +@admin_required +def create_category(): + data = request.get_json(silent=True) or {} + if not (data.get("name") or "").strip(): + return jsonify({"error": "分类名称不能为空"}), 400 + max_order = db.session.query(db.func.max(Category.sort_order)).scalar() or 0 + cat = Category( + name=data["name"].strip(), + icon=data.get("icon") or "📁", + description=data.get("description") or "", + sort_order=data.get("sort_order", max_order + 1), + is_active=data.get("is_active", True), + ) + db.session.add(cat) + db.session.commit() + return jsonify(cat.to_dict()), 201 + + +@bp.route("/categories/", methods=["PUT"]) +@admin_required +def update_category(cid): + cat = Category.query.get_or_404(cid) + data = request.get_json(silent=True) or {} + if "name" in data: + if not (data["name"] or "").strip(): + return jsonify({"error": "分类名称不能为空"}), 400 + cat.name = data["name"].strip() + for f in ("icon", "description", "sort_order", "is_active"): + if f in data: + setattr(cat, f, data[f]) + db.session.commit() + return jsonify(cat.to_dict()) + + +@bp.route("/categories/", methods=["DELETE"]) +@admin_required +def delete_category(cid): + cat = Category.query.get_or_404(cid) + db.session.delete(cat) + db.session.commit() + return jsonify({"message": "已删除"}) + + +@bp.route("/categories//move", methods=["POST"]) +@admin_required +def move_category(cid): + cat = Category.query.get_or_404(cid) + direction = (request.get_json(silent=True) or {}).get("direction", "up") + ordered = Category.query.order_by(Category.sort_order, Category.id).all() + idx = next(i for i, c in enumerate(ordered) if c.id == cid) + target = idx - 1 if direction == "up" else idx + 1 + if 0 <= target < len(ordered): + ordered[idx].sort_order, ordered[target].sort_order = ( + ordered[target].sort_order, ordered[idx].sort_order) + # break ties if both had the same sort_order + if ordered[idx].sort_order == ordered[target].sort_order: + ordered[target].sort_order += 1 if direction == "up" else -1 + db.session.commit() + return jsonify({"message": "ok"}) + + +# --------------------------------------------------------------------------- +# Option rows & options +# --------------------------------------------------------------------------- +@bp.route("/categories//rows", methods=["POST"]) +@admin_required +def create_row(cid): + Category.query.get_or_404(cid) + data = request.get_json(silent=True) or {} + if not (data.get("title") or "").strip(): + return jsonify({"error": "行标题不能为空"}), 400 + max_order = db.session.query(db.func.max(OptionRow.sort_order)).filter_by(category_id=cid).scalar() or 0 + row = OptionRow(category_id=cid, title=data["title"].strip(), + sort_order=data.get("sort_order", max_order + 1)) + db.session.add(row) + db.session.commit() + return jsonify(row.to_dict()), 201 + + +@bp.route("/rows/", methods=["PUT"]) +@admin_required +def update_row(rid): + row = OptionRow.query.get_or_404(rid) + data = request.get_json(silent=True) or {} + if "title" in data: + row.title = data["title"].strip() + if "sort_order" in data: + row.sort_order = data["sort_order"] + db.session.commit() + return jsonify(row.to_dict()) + + +@bp.route("/rows/", methods=["DELETE"]) +@admin_required +def delete_row(rid): + row = OptionRow.query.get_or_404(rid) + db.session.delete(row) + db.session.commit() + return jsonify({"message": "已删除"}) + + +@bp.route("/rows//options", methods=["POST"]) +@admin_required +def create_option(rid): + OptionRow.query.get_or_404(rid) + data = request.get_json(silent=True) or {} + if not (data.get("label") or "").strip(): + return jsonify({"error": "选项名称不能为空"}), 400 + max_order = db.session.query(db.func.max(Option.sort_order)).filter_by(row_id=rid).scalar() or 0 + opt = Option(row_id=rid, label=data["label"].strip(), + icon=data.get("icon") or "", + sort_order=data.get("sort_order", max_order + 1)) + db.session.add(opt) + db.session.commit() + return jsonify(opt.to_dict()), 201 + + +@bp.route("/options/", methods=["PUT"]) +@admin_required +def update_option(oid): + opt = Option.query.get_or_404(oid) + data = request.get_json(silent=True) or {} + for f in ("label", "icon", "sort_order"): + if f in data: + setattr(opt, f, data[f]) + db.session.commit() + return jsonify(opt.to_dict()) + + +@bp.route("/options/", methods=["DELETE"]) +@admin_required +def delete_option(oid): + opt = Option.query.get_or_404(oid) + db.session.delete(opt) + db.session.commit() + return jsonify({"message": "已删除"}) + + +# --------------------------------------------------------------------------- +# Resources +# --------------------------------------------------------------------------- +@bp.route("/resources") +@admin_required +def list_resources(): + q = Resource.query + cid = request.args.get("category_id", type=int) + if cid: + q = q.filter_by(category_id=cid) + kw = (request.args.get("keyword") or "").strip() + if kw: + q = q.filter(Resource.title.like(f"%{kw}%")) + items = q.order_by(Resource.category_id, Resource.sort_order, Resource.id).all() + return jsonify([r.to_dict() for r in items]) + + +@bp.route("/resources", methods=["POST"]) +@admin_required +def create_resource(): + data = request.get_json(silent=True) or {} + err = _validate_resource(data, require_category=True) + if err: + return jsonify({"error": err}), 400 + max_order = db.session.query(db.func.max(Resource.sort_order)).filter_by( + category_id=data["category_id"]).scalar() or 0 + res = Resource( + category_id=data["category_id"], + title=data["title"].strip(), + type=data.get("type", "command"), + content=data.get("content") or "", + url=data.get("url") or "", + description=data.get("description") or "", + sort_order=data.get("sort_order", max_order + 1), + is_active=data.get("is_active", True), + ) + _apply_option_tags(res, data.get("option_ids") or []) + db.session.add(res) + db.session.commit() + return jsonify(res.to_dict()), 201 + + +@bp.route("/resources/", methods=["PUT"]) +@admin_required +def update_resource(rid): + res = Resource.query.get_or_404(rid) + data = request.get_json(silent=True) or {} + err = _validate_resource(data, require_category=False) + if err: + return jsonify({"error": err}), 400 + for f in ("category_id", "title", "type", "content", "url", + "description", "sort_order", "is_active"): + if f in data: + value = data[f] + if f == "title": + value = value.strip() + setattr(res, f, value) + if "option_ids" in data: + _apply_option_tags(res, data["option_ids"] or []) + db.session.commit() + return jsonify(res.to_dict()) + + +@bp.route("/resources/", methods=["DELETE"]) +@admin_required +def delete_resource(rid): + res = Resource.query.get_or_404(rid) + db.session.delete(res) + db.session.commit() + return jsonify({"message": "已删除"}) + + +def _validate_resource(data, require_category): + if require_category and not data.get("category_id"): + return "必须选择所属分类" + if require_category and not (data.get("title") or "").strip(): + return "标题不能为空" + if "title" in data and not (data["title"] or "").strip(): + return "标题不能为空" + rtype = data.get("type") + if rtype is not None and rtype not in RESOURCE_TYPES: + return f"未知资源类型: {rtype}" + return None + + +def _apply_option_tags(res, option_ids): + opts = Option.query.filter(Option.id.in_(option_ids)).all() if option_ids else [] + res.options = opts + + +# --------------------------------------------------------------------------- +# Side links +# --------------------------------------------------------------------------- +@bp.route("/links") +@admin_required +def list_links(): + return jsonify([l.to_dict() for l in + SideLink.query.order_by(SideLink.sort_order, SideLink.id).all()]) + + +@bp.route("/links", methods=["POST"]) +@admin_required +def create_link(): + data = request.get_json(silent=True) or {} + if not (data.get("title") or "").strip() or not (data.get("url") or "").strip(): + return jsonify({"error": "标题和链接地址不能为空"}), 400 + max_order = db.session.query(db.func.max(SideLink.sort_order)).scalar() or 0 + link = SideLink( + title=data["title"].strip(), url=data["url"].strip(), + group_name=data.get("group_name") or "其他链接", + sort_order=data.get("sort_order", max_order + 1), + is_active=data.get("is_active", True), + ) + db.session.add(link) + db.session.commit() + return jsonify(link.to_dict()), 201 + + +@bp.route("/links/", methods=["PUT"]) +@admin_required +def update_link(lid): + link = SideLink.query.get_or_404(lid) + data = request.get_json(silent=True) or {} + for f in ("title", "url", "group_name", "sort_order", "is_active"): + if f in data: + setattr(link, f, data[f]) + db.session.commit() + return jsonify(link.to_dict()) + + +@bp.route("/links/", methods=["DELETE"]) +@admin_required +def delete_link(lid): + link = SideLink.query.get_or_404(lid) + db.session.delete(link) + db.session.commit() + return jsonify({"message": "已删除"}) + + +# --------------------------------------------------------------------------- +# Site settings +# --------------------------------------------------------------------------- +@bp.route("/settings", methods=["GET"]) +@admin_required +def get_settings(): + return jsonify({s.key: s.value for s in SiteSetting.query.all()}) + + +@bp.route("/settings", methods=["PUT"]) +@admin_required +def update_settings(): + data = request.get_json(silent=True) or {} + for key, value in data.items(): + s = SiteSetting.query.get(key) + if s: + s.value = value if value is not None else "" + else: + db.session.add(SiteSetting(key=key, value=value or "")) + db.session.commit() + return jsonify({"message": "已保存"}) + + +# --------------------------------------------------------------------------- +# Admin users (minimal: list + reset password) +# --------------------------------------------------------------------------- +@bp.route("/users") +@admin_required +def list_users(): + return jsonify([{"id": u.id, "username": u.username} for u in User.query.all()]) + + +@bp.route("/users", methods=["POST"]) +@admin_required +def create_user(): + data = request.get_json(silent=True) or {} + username = (data.get("username") or "").strip() + password = (data.get("password") or "").strip() + if not username or len(username) < 3: + return jsonify({"error": "用户名至少3个字符"}), 400 + if not password or len(password) < 6: + return jsonify({"error": "密码至少6个字符"}), 400 + if User.query.filter_by(username=username).first(): + return jsonify({"error": "用户名已存在"}), 400 + u = User(username=username) + u.set_password(password) + db.session.add(u) + db.session.commit() + return jsonify({"id": u.id, "username": u.username}), 201 + + +@bp.route("/users//password", methods=["PUT"]) +@admin_required +def reset_user_password(uid): + u = User.query.get_or_404(uid) + data = request.get_json(silent=True) or {} + password = (data.get("password") or "").strip() + if not password or len(password) < 6: + return jsonify({"error": "密码至少6个字符"}), 400 + u.set_password(password) + db.session.commit() + return jsonify({"message": "密码已重置"}) + + +@bp.route("/users/", methods=["DELETE"]) +@admin_required +def delete_user(uid): + u = User.query.get_or_404(uid) + if u.username == "admin": + return jsonify({"error": "不能删除内置 admin 用户"}), 400 + db.session.delete(u) + db.session.commit() + return jsonify({"message": "用户已删除"}) + + +# --------------------------------------------------------------------------- +# File upload +# --------------------------------------------------------------------------- +@bp.route("/upload", methods=["POST"]) +@admin_required +def upload_file(): + if "file" not in request.files: + return jsonify({"error": "未找到文件"}), 400 + f = request.files["file"] + if not f or not f.filename: + return jsonify({"error": "未选择文件"}), 400 + if not allowed_file(f.filename): + return jsonify({"error": "不支持的文件类型"}), 400 + + filename = secure_filename(f.filename) + # Add unique prefix + name, ext = os.path.splitext(filename) + unique_name = f"{name}_{uuid.uuid4().hex[:8]}{ext}" + os.makedirs(current_app.config["UPLOAD_FOLDER"], exist_ok=True) + filepath = os.path.join(current_app.config["UPLOAD_FOLDER"], unique_name) + f.save(filepath) + + file_url = f"{UPLOAD_URL_PREFIX}/{unique_name}" + return jsonify({ + "url": file_url, + "filename": filename, + "size": os.path.getsize(filepath) + }), 201 diff --git a/backend/routes/auth.py b/backend/routes/auth.py new file mode 100644 index 0000000..713fd73 --- /dev/null +++ b/backend/routes/auth.py @@ -0,0 +1,44 @@ +"""Auth API.""" +from flask import Blueprint, jsonify, request + +from auth import get_current_user, issue_token +from models import User + +bp = Blueprint("auth", __name__, url_prefix="/api/auth") + + +@bp.route("/login", methods=["POST"]) +def login(): + data = request.get_json(silent=True) or {} + username = (data.get("username") or "").strip() + password = data.get("password") or "" + user = User.query.filter_by(username=username).first() + if not user or not user.check_password(password): + return jsonify({"error": "用户名或密码错误"}), 401 + return jsonify({"token": issue_token(user), "username": user.username}) + + +@bp.route("/me") +def me(): + user = get_current_user() + if not user: + return jsonify({"error": "未登录"}), 401 + return jsonify({"username": user.username}) + + +@bp.route("/password", methods=["POST"]) +def change_password(): + user = get_current_user() + if not user: + return jsonify({"error": "未登录"}), 401 + data = request.get_json(silent=True) or {} + old = data.get("old_password") or "" + new = data.get("new_password") or "" + if not user.check_password(old): + return jsonify({"error": "原密码错误"}), 400 + if len(new) < 6: + return jsonify({"error": "新密码至少 6 位"}), 400 + from extensions import db + user.set_password(new) + db.session.commit() + return jsonify({"message": "密码已修改"}) diff --git a/backend/routes/portal.py b/backend/routes/portal.py new file mode 100644 index 0000000..d4cb899 --- /dev/null +++ b/backend/routes/portal.py @@ -0,0 +1,94 @@ +"""Public portal API — no auth required.""" +from flask import Blueprint, jsonify, request + +from models import Category, Option, Resource, SideLink, SiteSetting + +bp = Blueprint("portal", __name__, url_prefix="/api/portal") + + +@bp.route("/bootstrap") +def bootstrap(): + """Everything the portal page needs in one request.""" + settings = {s.key: s.value for s in SiteSetting.query.all()} + categories = ( + Category.query.filter_by(is_active=True) + .order_by(Category.sort_order, Category.id) + .all() + ) + links = ( + SideLink.query.filter_by(is_active=True) + .order_by(SideLink.sort_order, SideLink.id) + .all() + ) + return jsonify({ + "settings": settings, + "categories": [c.to_dict() for c in categories], + "links": [l.to_dict() for l in links], + }) + + +@bp.route("/categories//resources") +def category_resources(cid): + """Resources for a category, faceted by selected option ids.""" + category = Category.query.get_or_404(cid) + selected = [] + raw = request.args.get("option_ids", "") + keyword = request.args.get("keyword", "").strip() + if raw.strip(): + try: + selected = [int(x) for x in raw.split(",") if x.strip()] + except ValueError: + selected = [] + + query = Resource.query.filter_by(category_id=cid, is_active=True) + if keyword: + query = query.filter( + Resource.title.like(f"%{keyword}%") | + Resource.description.like(f"%{keyword}%") | + Resource.content.like(f"%{keyword}%") + ) + resources = query.order_by(Resource.sort_order, Resource.id).all() + + if selected: + chosen = Option.query.filter(Option.id.in_(selected)).all() + by_row = {} + for o in chosen: + by_row.setdefault(o.row_id, []).append(o.id) + + def matches(res): + tagged_ids = {o.id for o in res.options} + tagged_row_ids = {o.row_id for o in res.options} + for row_id, opt_ids in by_row.items(): + if row_id not in tagged_row_ids: + continue # untagged in this row -> applies to all + if not tagged_ids.intersection(opt_ids): + return False + return True + + resources = [r for r in resources if matches(r)] + + return jsonify({ + "category": category.to_dict(), + "resources": [r.to_dict() for r in resources], + "selected_option_ids": selected, + }) + + +@bp.route("/search") +def global_search(): + """Global search across all resources.""" + keyword = request.args.get("keyword", "").strip() + if not keyword or len(keyword) < 2: + return jsonify({"results": [], "keyword": keyword}) + + results = Resource.query.filter_by(is_active=True).filter( + Resource.title.like(f"%{keyword}%") | + Resource.description.like(f"%{keyword}%") | + Resource.content.like(f"%{keyword}%") + ).order_by(Resource.category_id, Resource.sort_order).limit(50).all() + + return jsonify({ + "keyword": keyword, + "count": len(results), + "results": [r.to_dict() for r in results] + }) diff --git a/backend/seed.py b/backend/seed.py new file mode 100644 index 0000000..99c394d --- /dev/null +++ b/backend/seed.py @@ -0,0 +1,207 @@ +"""Seed default admin + demo content mirroring the reference design.""" +from extensions import db +from models import Category, Option, OptionRow, Resource, SideLink, SiteSetting, User + +DEFAULT_SETTINGS = { + "site_name": "OPS 资源中心", + "hero_tag": "INTERNAL RESOURCES", + "hero_title": "内部资源 快速上手", + "hero_subtitle": "执行一条命令完成初始化,发现更多面向开发者的内部镜像与工具", + "hero_command": "curl -sSL http://ops.internal/init.sh | bash", + "hero_command_label": "RUN THE COMMAND TO INSTALL", + "announcement": "欢迎使用内部资源中心 · 新增资源请联系运维组", + "footer_text": "OPS Resource Portal · 内部使用", +} + + +def seed_admin(): + if not User.query.filter_by(username="admin").first(): + u = User(username="admin") + u.set_password("admin123") + db.session.add(u) + db.session.commit() + print("[seed] admin user created (admin / admin123)") + + +def seed_settings(): + changed = False + for k, v in DEFAULT_SETTINGS.items(): + if not SiteSetting.query.get(k): + db.session.add(SiteSetting(key=k, value=v)) + changed = True + if changed: + db.session.commit() + + +def seed_demo(): + """Populate demo content only on a completely empty database.""" + if Category.query.first(): + return + + # ---------------- Categories ---------------- + cats = {} + for i, (name, icon, desc) in enumerate([ + ("基础设备", "🖥️", "物理机 / KVM / Docker 等基础环境入口"), + ("基础系统", "💿", "各操作系统的初始化与配置"), + ("初始化执行命令", "⚡", "新机器上架后执行的标准命令"), + ("软件列表", "📦", "内部常用软件与工具清单"), + ("pypi源", "🐍", "内部 PyPI 镜像源配置"), + ("镜像源", "🪞", "apt / yum / npm 等镜像源汇总"), + ]): + c = Category(name=name, icon=icon, description=desc, sort_order=i) + db.session.add(c) + cats[name] = c + db.session.flush() + + # ---------------- Option rows ---------------- + def add_rows(cat, rows): + out = {} + for i, (title, labels) in enumerate(rows): + row = OptionRow(category_id=cat.id, title=title, sort_order=i) + db.session.add(row) + db.session.flush() + opts = [] + for j, (label, icon) in enumerate(labels): + o = Option(row_id=row.id, label=label, icon=icon, sort_order=j) + db.session.add(o) + opts.append(o) + db.session.flush() + out[title] = opts + return out + + env_labels = [("物理机", "🖥️"), ("KVM虚拟机", "🧩"), ("Docker容器", "🐳")] + os_labels = [("ubuntu", ""), ("debian", ""), ("centos", ""), ("kylin", "")] + + opts_base = add_rows(cats["基础设备"], [("运行环境", env_labels)]) + opts_sys = add_rows(cats["基础系统"], [("运行环境", env_labels), ("操作系统", os_labels)]) + opts_pypi = add_rows(cats["pypi源"], [("运行环境", env_labels), ("操作系统", os_labels)]) + + # ---------------- Resources ---------------- + r = Resource( + category_id=cats["pypi源"].id, + title="配置内部 pip 源", + type="command", + content="mkdir -p ~/.pip && wget http://ops.streamcomputing.com/repo_list/pip.conf -O ~/.pip/pip.conf", + description="一键写入 pip.conf,立即生效", + sort_order=0, + ) + db.session.add(r) + + db.session.add(Resource( + category_id=cats["pypi源"].id, + title="Docker 构建时注入 pip 源", + type="command", + content="RUN mkdir -p /root/.pip && wget -q http://ops.streamcomputing.com/repo_list/pip.conf -O /root/.pip/pip.conf", + description="适用于 Dockerfile 构建场景", + sort_order=1, + options=[opts_pypi["运行环境"][2]], # Docker容器 + )) + + db.session.add(Resource( + category_id=cats["pypi源"].id, + title="PyPI 包索引浏览", + type="link", + url="http://ops.streamcomputing.com/pypi/", + description="Index of / — 内部镜像的全量包列表", + sort_order=2, + )) + + db.session.add(Resource( + category_id=cats["基础系统"].id, + title="Ubuntu / Debian 初始化", + type="command", + content="wget http://ops.streamcomputing.com/repo_list/ubuntu.sources -O /etc/apt/sources.list.d/internal.sources && apt-get update", + description="替换为内部 apt 源并刷新索引", + sort_order=0, + options=[opts_sys["操作系统"][0], opts_sys["操作系统"][1]], # ubuntu, debian + )) + db.session.add(Resource( + category_id=cats["基础系统"].id, + title="CentOS / Kylin 初始化", + type="command", + content="wget http://ops.streamcomputing.com/repo_list/CentOS-Base.repo -O /etc/yum.repos.d/CentOS-Base.repo && yum makecache", + description="替换为内部 yum 源并建立缓存", + sort_order=1, + options=[opts_sys["操作系统"][2], opts_sys["操作系统"][3]], # centos, kylin + )) + db.session.add(Resource( + category_id=cats["基础系统"].id, + title="KVM 虚拟机 cloud-init 说明", + type="text", + content="KVM 模板镜像已内置 cloud-init,首次启动会自动扩容磁盘并注入 SSH 公钥。\n如需自定义主机名,请在创建时传入 metadata。", + description="适用于 KVM 模板镜像", + sort_order=2, + options=[opts_sys["运行环境"][1]], # KVM虚拟机 + )) + + db.session.add(Resource( + category_id=cats["初始化执行命令"].id, + title="标准初始化脚本", + type="command", + content="curl -sSL http://ops.streamcomputing.com/init/bootstrap.sh | bash", + description="包含时区、内核参数、监控 agent 安装", + sort_order=0, + )) + db.session.add(Resource( + category_id=cats["初始化执行命令"].id, + title="初始化自检", + type="command", + content="ops-check --all --report", + description="初始化完成后执行,输出自检报告", + sort_order=1, + )) + + db.session.add(Resource( + category_id=cats["软件列表"].id, + title="内部软件清单", + type="link", + url="http://ops.streamcomputing.com/software/", + description="常用软件版本与下载入口", + sort_order=0, + )) + db.session.add(Resource( + category_id=cats["软件列表"].id, + title="CLI 工具合集", + type="text", + content="ops-cli / dbtool / netprobe / logtail\n安装方式见各软件详情页。", + sort_order=1, + )) + + db.session.add(Resource( + category_id=cats["基础设备"].id, + title="设备申领入口", + type="link", + url="http://ops.streamcomputing.com/devices/", + description="物理机 / 虚拟机 / 容器资源申领", + sort_order=0, + )) + + db.session.add(Resource( + category_id=cats["镜像源"].id, + title="镜像源总览", + type="link", + url="http://ops.streamcomputing.com/mirrors/", + description="apt / yum / pypi / npm / docker registry", + sort_order=0, + )) + + # ---------------- Side links ---------------- + for i, (title, url) in enumerate([ + ("Docker镜像仓库", "http://ops.streamcomputing.com/docker/"), + ("Docker基础镜像使用方法以及镜像列表", "http://ops.streamcomputing.com/docker/guide"), + ("KVM模板镜像下载", "http://ops.streamcomputing.com/kvm/images"), + ("KVM模板镜像信息一览表", "http://ops.streamcomputing.com/kvm/list"), + ("iso镜像下载", "http://ops.streamcomputing.com/iso/"), + ("grafana网址", "http://grafana.streamcomputing.com/"), + ("ops文档中心", "http://docs.streamcomputing.com/"), + ]): + db.session.add(SideLink(title=title, url=url, sort_order=i)) + + db.session.commit() + print("[seed] demo content created") + + +def seed_all(): + seed_admin() + seed_settings() + seed_demo() diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..485cea5 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + OPS 资源中心 + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..5d73a7b --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1870 @@ +{ + "name": "ops-portal-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ops-portal-frontend", + "version": "1.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.7.9", + "element-plus": "^2.9.3", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.0.7" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.40", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.14.3", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.3.tgz", + "integrity": "sha512-pJcvxcpZjYruNzuJhAeVwnbYjfNgzBKnWHwSVEhwzM2/kcLI3brzmtIBxtPqd4hQWJfD1PRnjoc1WipLw2eBGg==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.7.6", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.3.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.5" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.7.tgz", + "integrity": "sha512-Skkhw9agYSgsWqv7bxSOGJZa9SaiJbZVGdXuFWnrzKaQYHnw9qbjD630rw6RyMqDbp54nfLCLw5SZA55if7JLg==", + "license": "MIT" + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1b6a2ee --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "ops-portal-frontend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.9", + "element-plus": "^2.9.3", + "@element-plus/icons-vue": "^2.3.1", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.0.7" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..98240ae --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,3 @@ + diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js new file mode 100644 index 0000000..029851c --- /dev/null +++ b/frontend/src/api/index.js @@ -0,0 +1,105 @@ +import axios from 'axios' +import { ElMessage } from 'element-plus' +import { getToken, removeToken } from '../utils/auth' +import router from '../router' + +const api = axios.create({ + baseURL: '/api', + timeout: 30000 +}) + +api.interceptors.request.use((config) => { + const token = getToken() + if (token) config.headers.Authorization = `Bearer ${token}` + return config +}) + +api.interceptors.response.use( + (res) => res, + (err) => { + const status = err.response?.status + const msg = err.response?.data?.error || err.message || '请求失败' + if (status === 401 && router.currentRoute.value.path.startsWith('/admin')) { + removeToken() + router.push({ name: 'admin-login' }) + } + ElMessage.error(msg) + return Promise.reject(err) + } +) + +// ---------- Portal (public) ---------- +export const portalApi = { + bootstrap: () => api.get('/portal/bootstrap'), + resources: (categoryId, optionIds = [], keyword = '') => + api.get(`/portal/categories/${categoryId}/resources`, { + params: { + ...(optionIds.length ? { option_ids: optionIds.join(',') } : {}), + ...(keyword ? { keyword } : {}) + } + }), + search: (keyword) => api.get('/portal/search', { params: { keyword } }) +} + +// ---------- Auth ---------- +export const authApi = { + login: (username, password) => api.post('/auth/login', { username, password }), + me: () => api.get('/auth/me'), + changePassword: (old_password, new_password) => api.post('/auth/password', { old_password, new_password }) +} + +// ---------- Admin ---------- +export const adminApi = { + stats: () => api.get('/admin/stats'), + + // categories + listCategories: () => api.get('/admin/categories'), + createCategory: (data) => api.post('/admin/categories', data), + updateCategory: (id, data) => api.put(`/admin/categories/${id}`, data), + deleteCategory: (id) => api.delete(`/admin/categories/${id}`), + moveCategory: (id, direction) => api.post(`/admin/categories/${id}/move`, { direction }), + + // option rows + createRow: (categoryId, data) => api.post(`/admin/categories/${categoryId}/rows`, data), + updateRow: (id, data) => api.put(`/admin/rows/${id}`, data), + deleteRow: (id) => api.delete(`/admin/rows/${id}`), + + // options + createOption: (rowId, data) => api.post(`/admin/rows/${rowId}/options`, data), + updateOption: (id, data) => api.put(`/admin/options/${id}`, data), + deleteOption: (id) => api.delete(`/admin/options/${id}`), + + // resources + listResources: (params) => api.get('/admin/resources', { params }), + createResource: (data) => api.post('/admin/resources', data), + updateResource: (id, data) => api.put(`/admin/resources/${id}`, data), + deleteResource: (id) => api.delete(`/admin/resources/${id}`), + + // side links + listLinks: () => api.get('/admin/links'), + createLink: (data) => api.post('/admin/links', data), + updateLink: (id, data) => api.put(`/admin/links/${id}`, data), + deleteLink: (id) => api.delete(`/admin/links/${id}`), + + // settings + getSettings: () => api.get('/admin/settings'), + updateSettings: (data) => api.put('/admin/settings', data), + + // users + listUsers: () => api.get('/admin/users'), + createUser: (data) => api.post('/admin/users', data), + resetUserPassword: (id, password) => api.put(`/admin/users/${id}/password`, { password }), + deleteUser: (id) => api.delete(`/admin/users/${id}`), + + // upload + uploadFile: (file, onProgress) => { + const formData = new FormData() + formData.append('file', file) + return api.post('/admin/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + onUploadProgress: onProgress + }) + } +} + +export default api diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..5072341 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,16 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' + +import App from './App.vue' +import router from './router' + +const app = createApp(App) +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} +app.use(ElementPlus, { locale: zhCn }) +app.use(router) +app.mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..f31eceb --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,42 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { getToken } from '../utils/auth' + +const routes = [ + { + path: '/', + name: 'portal', + component: () => import('../views/Portal.vue') + }, + { + path: '/admin/login', + name: 'admin-login', + component: () => import('../views/admin/Login.vue') + }, + { + path: '/admin', + component: () => import('../views/admin/Layout.vue'), + meta: { requiresAuth: true }, + children: [ + { path: '', redirect: '/admin/dashboard' }, + { path: 'dashboard', name: 'admin-dashboard', component: () => import('../views/admin/Dashboard.vue') }, + { path: 'categories', name: 'admin-categories', component: () => import('../views/admin/Categories.vue') }, + { path: 'resources', name: 'admin-resources', component: () => import('../views/admin/Resources.vue') }, + { path: 'links', name: 'admin-links', component: () => import('../views/admin/Links.vue') }, + { path: 'settings', name: 'admin-settings', component: () => import('../views/admin/Settings.vue') }, + { path: 'users', name: 'admin-users', component: () => import('../views/admin/Users.vue') } + ] + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +router.beforeEach((to) => { + if (to.meta.requiresAuth && !getToken()) { + return { name: 'admin-login', query: { redirect: to.fullPath } } + } +}) + +export default router diff --git a/frontend/src/styles/portal.css b/frontend/src/styles/portal.css new file mode 100644 index 0000000..b9560cd --- /dev/null +++ b/frontend/src/styles/portal.css @@ -0,0 +1,1262 @@ +/* ============================================================ + OPS Portal - Modern Design System v2.0 + Palette: Deep space gradient + Aurora cyan + Sunset orange + Features: Glassmorphism, Neomorphic shadows, Smooth animations + ============================================================ */ +:root { + /* Primary colors */ + --ink: #0a1424; + --ink-2: #0e1c33; + --ink-3: #13263f; + --cyan: #2dd9f0; + --cyan-light: #5ce4f7; + --cyan-glow: rgba(45, 217, 240, 0.4); + --cyan-dim: rgba(45, 217, 240, 0.12); + + /* Accent colors */ + --orange: #f97316; + --orange-glow: rgba(249, 115, 22, 0.35); + --orange-dim: rgba(249, 115, 22, 0.1); + + /* Purple accent */ + --purple: #8b5cf6; + --purple-glow: rgba(139, 92, 246, 0.35); + --purple-dim: rgba(139, 92, 246, 0.1); + + /* UI colors */ + --paper: #f8fafc; + --card: #ffffff; + --line: #e2e8f0; + --line-light: #f1f5f9; + + /* Text colors */ + --text: #0f172a; + --text-2: #475569; + --text-3: #94a3b8; + --text-light: #cbd5e1; + + /* Typography */ + --font-display: 'Inter', 'SF Pro Display', -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif; + --font-body: 'Inter', -apple-system, 'PingFang SC', 'Microsoft YaHei', 'Noto Sans SC', sans-serif; + --font-mono: 'JetBrains Mono', 'SF Mono', ui-monospace, Consolas, monospace; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + --shadow-glow: 0 0 40px rgba(45, 217, 240, 0.15); + + /* Border radius */ + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + --radius-xl: 20px; + + /* Transitions */ + --transition-fast: 0.15s ease; + --transition-normal: 0.25s ease; + --transition-slow: 0.4s ease; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: var(--font-body); + background: var(--paper); + color: var(--text); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* ---------- announcement ticker ---------- */ +.ticker { + background: linear-gradient(135deg, var(--ink) 0%, var(--ink-2) 100%); + color: var(--cyan-light); + font-size: 12.5px; + letter-spacing: 0.06em; + overflow: hidden; + white-space: nowrap; + border-bottom: 1px solid rgba(45, 217, 240, 0.2); + box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.03); +} + +.ticker-inner { + display: inline-block; + padding: 8px 0; + animation: ticker-scroll 28s linear infinite; +} + +.ticker-inner span { + margin-right: 80px; +} + +.ticker-inner b { + color: #fff; + font-weight: 600; + margin-right: 10px; +} + +@keyframes ticker-scroll { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} + +/* ---------- hero ---------- */ +.hero { + position: relative; + background: + radial-gradient(1200px 600px at 80% -10%, rgba(45, 217, 240, 0.15), transparent 60%), + radial-gradient(900px 500px at 10% 110%, rgba(249, 115, 22, 0.1), transparent 55%), + radial-gradient(600px 400px at 50% 120%, rgba(139, 92, 246, 0.08), transparent 50%), + linear-gradient(160deg, var(--ink) 0%, var(--ink-2) 55%, #0b1a30 100%); + color: #eaf2fb; + overflow: hidden; +} + +.hero-grid-bg { + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(45, 217, 240, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(45, 217, 240, 0.04) 1px, transparent 1px); + background-size: 50px 50px; + mask-image: radial-gradient(ellipse 90% 80% at 60% 30%, #000 30%, transparent 75%); +} + +.hero-inner { + position: relative; + max-width: 1280px; + margin: 0 auto; + padding: 80px 40px 46px; + display: grid; + grid-template-columns: 1.05fr 0.95fr; + gap: 60px; + align-items: center; + min-height: 480px; +} + +.hero-tag { + display: inline-flex; + align-items: center; + gap: 12px; + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.25em; + color: var(--cyan); + border: 1px solid rgba(45, 217, 240, 0.3); + padding: 8px 16px; + border-radius: var(--radius-sm); + margin-bottom: 28px; + background: rgba(45, 217, 240, 0.08); + backdrop-filter: blur(8px); +} + +.hero-tag::before { + content: ''; + width: 8px; + height: 8px; + background: var(--cyan); + border-radius: 50%; + box-shadow: 0 0 12px var(--cyan-glow); + animation: pulse-dot 2s ease-in-out infinite; +} + +@keyframes pulse-dot { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(0.7); } +} + +.hero h1 { + font-family: var(--font-display); + font-size: clamp(40px, 5vw, 68px); + line-height: 1.1; + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 20px; +} + +.hero h1 em { + font-style: normal; + background: linear-gradient(135deg, var(--cyan) 0%, var(--purple) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: none; + filter: drop-shadow(0 0 30px rgba(45, 217, 240, 0.4)); +} + +.hero-sub { + color: #9db1c7; + font-size: 16px; + line-height: 1.8; + max-width: 520px; + margin-bottom: 36px; +} + +/* Search button in hero */ +.hero-search-btn { + display: flex; + align-items: center; + gap: 14px; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: var(--radius-md); + padding: 16px 20px; + margin-bottom: 28px; + cursor: pointer; + transition: all var(--transition-normal); + max-width: 520px; + backdrop-filter: blur(10px); +} + +.hero-search-btn:hover { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(45, 217, 240, 0.5); + transform: translateY(-2px); + box-shadow: 0 8px 30px rgba(45, 217, 240, 0.2); +} + +.hs-icon { + font-size: 20px; +} + +.hs-hint { + margin-left: auto; + font-family: var(--font-mono); + font-size: 11px; + color: #7d92aa; + background: rgba(255, 255, 255, 0.1); + padding: 5px 12px; + border-radius: var(--radius-sm); +} + +/* terminal card */ +.terminal { + background: linear-gradient(135deg, rgba(10, 20, 36, 0.95) 0%, rgba(14, 28, 51, 0.95) 100%); + border: 1px solid rgba(45, 217, 240, 0.25); + border-radius: var(--radius-lg); + max-width: 560px; + box-shadow: + 0 24px 60px rgba(0, 0, 0, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.06), + 0 0 60px rgba(45, 217, 240, 0.1); + backdrop-filter: blur(12px); + overflow: hidden; +} + +.terminal-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 18px; + border-bottom: 1px solid rgba(45, 217, 240, 0.15); + background: rgba(0, 0, 0, 0.2); +} + +.terminal-bar i { + width: 12px; + height: 12px; + border-radius: 50%; + background: #2a3a50; +} + +.terminal-bar i:nth-child(1) { background: #ef4444; } +.terminal-bar i:nth-child(2) { background: #f59e0b; } +.terminal-bar i:nth-child(3) { background: #10b981; } + +.terminal-bar span { + margin-left: 12px; + font-family: var(--font-mono); + font-size: 11px; + color: #64748b; + letter-spacing: 0.12em; +} + +.terminal-body { + display: flex; + align-items: center; + gap: 14px; + padding: 20px 20px; +} + +.terminal-body code { + flex: 1; + font-family: var(--font-mono); + font-size: 13.5px; + line-height: 1.7; + color: #e2e8f0; + word-break: break-all; +} + +.terminal-body code::before { + content: '$ '; + color: var(--cyan); + font-weight: 700; +} + +.copy-btn { + flex-shrink: 0; + font-family: var(--font-mono); + font-size: 12px; + letter-spacing: 0.08em; + color: var(--cyan); + background: rgba(45, 217, 240, 0.12); + border: 1px solid rgba(45, 217, 240, 0.35); + padding: 10px 18px; + border-radius: var(--radius-sm); + cursor: pointer; + transition: all var(--transition-fast); +} + +.copy-btn:hover { + background: rgba(45, 217, 240, 0.22); + box-shadow: 0 0 20px rgba(45, 217, 240, 0.25); + transform: translateY(-1px); +} + +.copy-btn.copied { + color: #10b981; + border-color: rgba(16, 185, 129, 0.5); + background: rgba(16, 185, 129, 0.15); +} + +.hero-stats { + display: flex; + gap: 48px; + margin-top: 40px; +} + +.hero-stat { + position: relative; +} + +.hero-stat::after { + content: ''; + position: absolute; + right: -24px; + top: 50%; + transform: translateY(-50%); + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.2), transparent); +} + +.hero-stat:last-child::after { + display: none; +} + +.hero-stat b { + display: block; + font-family: var(--font-display); + font-size: 36px; + color: #fff; + line-height: 1; + font-weight: 700; +} + +.hero-stat span { + font-size: 12px; + color: #94a3b8; + letter-spacing: 0.1em; + margin-top: 8px; + display: block; + text-transform: uppercase; +} + +/* ---------- hero right: circuit svg + cubes ---------- */ +.hero-art { + position: relative; + height: 420px; +} + +.hero-art svg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.trace { + stroke: rgba(45, 217, 240, 0.3); + stroke-width: 1.5; + fill: none; +} + +.trace-flow { + stroke: var(--cyan); + stroke-width: 2.5; + fill: none; + stroke-dasharray: 30 360; + filter: drop-shadow(0 0 8px var(--cyan-glow)); + animation: trace-flow 4s linear infinite; +} + +@keyframes trace-flow { + to { stroke-dashoffset: -390; } +} + +.node { + fill: var(--ink-2); + stroke: rgba(45, 217, 240, 0.6); + stroke-width: 2; +} + +.node-core { + fill: var(--cyan); + animation: pulse-dot 2.2s ease-in-out infinite; +} + +.cube { + position: absolute; + border: 2px solid rgba(45, 217, 240, 0.35); + background: linear-gradient(135deg, rgba(45, 217, 240, 0.1), rgba(45, 217, 240, 0.02)); + box-shadow: + 0 12px 40px rgba(0, 0, 0, 0.3), + inset 0 0 30px rgba(45, 217, 240, 0.06); + animation: cube-float 7s ease-in-out infinite; + backdrop-filter: blur(4px); +} + +.cube::after { + content: ''; + position: absolute; + inset: 12px; + border: 1px solid rgba(45, 217, 240, 0.2); +} + +.cube.c1 { + top: 10%; + left: 12%; + width: 72px; + height: 72px; + animation-delay: 0s; + transform: rotate(15deg); + border-radius: 8px; +} + +.cube.c2 { + top: 45%; + left: 55%; + width: 100px; + height: 100px; + animation-delay: -2.2s; + transform: rotate(-10deg); + border-radius: 12px; + border-color: rgba(249, 115, 22, 0.35); + background: linear-gradient(135deg, rgba(249, 115, 22, 0.08), rgba(249, 115, 22, 0.02)); +} + +.cube.c3 { + top: 5%; + left: 65%; + width: 48px; + height: 48px; + animation-delay: -4s; + transform: rotate(28deg); + border-radius: 6px; + border-color: rgba(139, 92, 246, 0.4); + background: linear-gradient(135deg, rgba(139, 92, 246, 0.1), rgba(139, 92, 246, 0.02)); +} + +.cube.c4 { + top: 65%; + left: 18%; + width: 56px; + height: 56px; + animation-delay: -1.2s; + transform: rotate(-18deg); + border-radius: 6px; +} + +@keyframes cube-float { + 0%, 100% { transform: translateY(0) rotate(var(--rotation, 15deg)); } + 50% { transform: translateY(-22px) rotate(var(--rotation, 15deg)); } +} + +/* ---------- content layout ---------- */ +.content-wrap { + max-width: 1280px; + margin: 0 auto; + padding: 0 40px 80px; + position: relative; + display: grid; + grid-template-columns: 240px 1fr 280px; + gap: 28px; + align-items: start; +} + +/* left menu */ +.side-menu { + background: var(--card); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + padding: 12px 0; + position: sticky; + top: 24px; + box-shadow: var(--shadow-lg), var(--shadow-glow); +} + +.side-menu-title { + font-size: 11px; + letter-spacing: 0.25em; + color: var(--text-3); + padding: 10px 24px 14px; + font-family: var(--font-mono); + text-transform: uppercase; +} + +.menu-item { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 24px; + font-size: 14.5px; + color: var(--text-2); + cursor: pointer; + border-left: 3px solid transparent; + transition: all var(--transition-fast); + user-select: none; + margin: 0 4px; + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; +} + +.menu-item .mi-icon { + font-size: 18px; + width: 24px; + text-align: center; +} + +.menu-item .mi-count { + margin-left: auto; + font-size: 11px; + font-family: var(--font-mono); + color: var(--text-3); + background: var(--paper); + padding: 3px 10px; + border-radius: 20px; + font-weight: 600; +} + +.menu-item:hover { + color: var(--text); + background: var(--paper); +} + +.menu-item.active { + color: var(--orange); + font-weight: 600; + border-left-color: var(--orange); + background: linear-gradient(90deg, var(--orange-dim), transparent 70%); +} + +.menu-item.active .mi-count { + background: var(--orange-dim); + color: var(--orange); +} + +/* center panel */ +.center-panel { + min-width: 0; +} + +.panel-head { + display: flex; + align-items: center; + gap: 14px; + margin: 0 0 24px; + background: var(--card); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + padding: 18px 24px; + box-shadow: var(--shadow-lg); + position: relative; + overflow: hidden; +} + +.panel-head::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + background: linear-gradient(180deg, var(--cyan) 0%, var(--purple) 100%); +} + +.panel-head h2 { + font-family: var(--font-display); + font-size: 20px; + letter-spacing: -0.01em; + font-weight: 700; + color: var(--text); + display: flex; + align-items: center; + gap: 10px; + line-height: 1.2; + flex-shrink: 0; +} + +.panel-head p { + font-size: 13px; + color: var(--text-3); + margin-left: auto; + padding-left: 16px; + border-left: 1px solid var(--line); + line-height: 1.5; + max-width: 60%; + text-align: right; +} + +@media (max-width: 720px) { + .panel-head { + flex-direction: column; + align-items: flex-start; + padding: 16px 18px; + } + .panel-head p { + margin-left: 0; + padding-left: 0; + border-left: none; + text-align: left; + max-width: 100%; + } +} + +/* option tiles */ +.option-row { + margin-bottom: 24px; +} + +.option-row-title { + font-size: 12px; + letter-spacing: 0.15em; + color: var(--text-3); + margin-bottom: 12px; + font-family: var(--font-mono); + display: flex; + align-items: center; + gap: 12px; + text-transform: uppercase; +} + +.option-row-title::after { + content: ''; + flex: 1; + height: 1px; + background: linear-gradient(90deg, var(--line), transparent); +} + +.tiles { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 14px; +} + +.tile { + position: relative; + background: var(--card); + border: 2px solid var(--line); + border-radius: var(--radius-md); + padding: 20px 18px; + text-align: center; + cursor: pointer; + transition: all var(--transition-normal); + font-size: 14.5px; + color: var(--text-2); + user-select: none; + box-shadow: var(--shadow-sm); +} + +.tile .tile-icon { + display: block; + font-size: 28px; + margin-bottom: 10px; +} + +.tile:hover { + border-color: var(--cyan); + color: var(--text); + transform: translateY(-4px); + box-shadow: + 0 12px 32px rgba(45, 217, 240, 0.18), + 0 4px 12px rgba(0, 0, 0, 0.08); +} + +.tile.selected { + border-color: var(--cyan); + background: linear-gradient(160deg, var(--cyan-dim), #fff 70%); + color: #0e7490; + font-weight: 600; + box-shadow: + 0 8px 28px rgba(45, 217, 240, 0.25), + inset 0 1px 0 rgba(255, 255, 255, 0.8); +} + +.tile.selected::after { + content: '✓'; + position: absolute; + top: 10px; + right: 12px; + color: var(--cyan); + font-size: 16px; + font-weight: 700; +} + +/* resource cards */ +.res-list { + display: flex; + flex-direction: column; + gap: 18px; + margin-top: 8px; +} + +.res-card { + background: var(--card); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + overflow: hidden; + animation: rise-in 0.45s ease both; + box-shadow: var(--shadow-md); + transition: all var(--transition-normal); +} + +.res-card:hover { + box-shadow: var(--shadow-xl), 0 0 40px rgba(0, 0, 0, 0.05); + transform: translateY(-3px); + border-color: rgba(45, 217, 240, 0.3); +} + +@keyframes rise-in { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.res-head { + display: flex; + align-items: center; + gap: 14px; + padding: 18px 22px 14px; +} + +.res-badge { + font-size: 11px; + font-family: var(--font-mono); + letter-spacing: 0.08em; + padding: 4px 12px; + border-radius: 20px; + flex-shrink: 0; + font-weight: 600; +} + +.res-badge.command { + background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%); + color: #0369a1; + border: 1px solid #7dd3fc; +} + +.res-badge.link { + background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%); + color: #4338ca; + border: 1px solid #a5b4fc; +} + +.res-badge.text { + background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%); + color: #166534; + border: 1px solid #86efac; +} + +.res-badge.file { + background: linear-gradient(135deg, #ffedd5 0%, #fed7aa 100%); + color: #9a3412; + border: 1px solid #fdba74; +} + +.res-title { + font-size: 16px; + font-weight: 600; +} + +.res-desc { + font-size: 13px; + color: var(--text-3); + margin-left: auto; +} + +/* command block */ +.cmd-block { + margin: 6px 22px 20px; + background: linear-gradient(135deg, var(--ink) 0%, var(--ink-2) 100%); + border-radius: var(--radius-md); + display: flex; + align-items: center; + gap: 14px; + padding: 16px 18px; + border: 1px solid #1e3a5f; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.cmd-block code { + flex: 1; + font-family: var(--font-mono); + font-size: 13.5px; + color: #e2e8f0; + word-break: break-all; + line-height: 1.7; +} + +.cmd-block code::before { + content: '$ '; + color: var(--cyan); + font-weight: 700; +} + +.cmd-copy { + flex-shrink: 0; + font-size: 12px; + font-family: var(--font-mono); + color: var(--cyan); + background: rgba(45, 217, 240, 0.12); + border: 1px solid rgba(45, 217, 240, 0.35); + border-radius: var(--radius-sm); + padding: 8px 16px; + cursor: pointer; + transition: all var(--transition-fast); +} + +.cmd-copy:hover { + background: rgba(45, 217, 240, 0.22); + transform: translateY(-1px); +} + +.cmd-copy.copied { + color: #10b981; + border-color: rgba(16, 185, 129, 0.5); + background: rgba(16, 185, 129, 0.15); +} + +/* link / text / file body */ +.res-body { + padding: 4px 22px 20px; + font-size: 14.5px; + color: var(--text-2); + line-height: 1.8; + white-space: pre-wrap; +} + +.res-jump { + display: inline-flex; + align-items: center; + gap: 8px; + margin: 4px 22px 20px; + font-size: 14px; + color: var(--purple); + text-decoration: none; + font-weight: 500; + padding: 8px 16px; + background: var(--purple-dim); + border-radius: var(--radius-sm); + border: 1px solid rgba(139, 92, 246, 0.2); + transition: all var(--transition-fast); +} + +.res-jump:hover { + background: rgba(139, 92, 246, 0.15); + transform: translateX(4px); +} + +.empty-hint { + text-align: center; + padding: 80px 30px; + color: var(--text-3); + font-size: 15px; + background: var(--card); + border: 2px dashed var(--line); + border-radius: var(--radius-lg); +} + +.empty-hint .eh-icon { + font-size: 48px; + display: block; + margin-bottom: 16px; + opacity: 0.5; +} + +/* right links panel */ +.links-panel { + background: var(--card); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + padding: 22px 24px; + position: sticky; + top: 24px; + box-shadow: var(--shadow-lg); +} + +.links-panel h3 { + font-size: 16px; + padding-bottom: 16px; + border-bottom: 2px solid var(--line-light); + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; +} + +.links-panel h3::before { + content: ''; + width: 4px; + height: 18px; + background: linear-gradient(135deg, var(--cyan) 0%, var(--purple) 100%); + border-radius: 2px; +} + +.link-item { + display: block; + padding: 14px 8px; + font-size: 14px; + color: var(--text-2); + text-decoration: none; + border-bottom: 1px solid var(--line-light); + transition: all var(--transition-fast); + line-height: 1.5; + border-radius: var(--radius-sm); +} + +.link-item:last-child { + border-bottom: none; +} + +.link-item:hover { + color: var(--orange); + padding-left: 16px; + background: var(--orange-dim); +} + +.link-item::before { + content: '› '; + color: var(--text-3); + font-weight: 700; +} + +/* footer */ +.portal-footer { + text-align: center; + padding: 32px 20px 44px; + color: var(--text-3); + font-size: 13px; + letter-spacing: 0.06em; + border-top: 1px solid var(--line); + background: var(--card); +} + +.portal-footer a { + color: var(--text-2); + text-decoration: none; + margin-left: 16px; + transition: color var(--transition-fast); +} + +.portal-footer a:hover { + color: var(--orange); +} + +/* admin entry floating button */ +.admin-fab { + position: fixed; + right: 32px; + bottom: 32px; + width: 56px; + height: 56px; + border-radius: 50%; + background: linear-gradient(135deg, var(--ink) 0%, var(--ink-2) 100%); + color: var(--cyan); + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + text-decoration: none; + box-shadow: + 0 12px 40px rgba(10, 20, 36, 0.4), + 0 0 30px rgba(45, 217, 240, 0.2); + border: 2px solid rgba(45, 217, 240, 0.35); + transition: all var(--transition-normal); + z-index: 50; + backdrop-filter: blur(8px); +} + +.admin-fab:hover { + transform: translateY(-4px) scale(1.08); + box-shadow: + 0 16px 50px rgba(10, 20, 36, 0.5), + 0 0 50px rgba(45, 217, 240, 0.35); + border-color: var(--cyan); +} + +/* loading skeleton */ +.loading-wrap { + padding: 100px 0; + text-align: center; + color: var(--text-3); + font-size: 15px; +} + +/* ---------- search ---------- */ +.search-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(6px); + z-index: 1000; + display: flex; + justify-content: center; + padding-top: 14vh; + animation: fade-in 0.2s ease; +} + +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +.search-modal { + width: 90%; + max-width: 680px; + background: var(--card); + border-radius: var(--radius-lg); + box-shadow: + 0 32px 80px rgba(0, 0, 0, 0.4), + 0 0 100px rgba(0, 0, 0, 0.1); + overflow: hidden; + animation: modal-rise 0.25s ease; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +@keyframes modal-rise { + from { + opacity: 0; + transform: translateY(-30px) scale(0.96); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.search-input-wrap { + display: flex; + align-items: center; + gap: 14px; + padding: 20px 24px; + border-bottom: 1px solid var(--line); + background: linear-gradient(to bottom, var(--paper), transparent); +} + +.search-icon { + font-size: 22px; + color: var(--text-3); +} + +.search-input { + flex: 1; + border: none; + outline: none; + font-size: 17px; + font-family: var(--font-body); + background: transparent; + color: var(--text); +} + +.search-input::placeholder { + color: var(--text-3); +} + +.search-close { + border: none; + background: var(--paper); + font-size: 20px; + color: var(--text-3); + cursor: pointer; + padding: 6px 12px; + border-radius: var(--radius-sm); + transition: all var(--transition-fast); +} + +.search-close:hover { + background: var(--line); + color: var(--text-2); +} + +.search-results { + max-height: 55vh; + overflow-y: auto; +} + +.search-hint, +.search-empty { + padding: 50px 30px; + text-align: center; + color: var(--text-3); + font-size: 15px; +} + +.search-result-item { + display: flex; + align-items: center; + gap: 14px; + padding: 16px 24px; + border-bottom: 1px solid var(--line-light); + cursor: pointer; + transition: all var(--transition-fast); +} + +.search-result-item:hover { + background: var(--paper); + padding-left: 32px; +} + +.res-badge-sm { + font-size: 10px; + font-family: var(--font-mono); + padding: 3px 10px; + border-radius: 20px; + flex-shrink: 0; + font-weight: 600; +} + +.res-badge-sm.command { + background: #e0f2fe; + color: #0369a1; +} + +.res-badge-sm.link { + background: #e0e7ff; + color: #4338ca; +} + +.res-badge-sm.text { + background: #dcfce7; + color: #166534; +} + +.res-badge-sm.file { + background: #ffedd5; + color: #9a3412; +} + +.sr-title { + font-weight: 500; + font-size: 15px; +} + +.sr-desc { + font-size: 13px; + color: var(--text-3); + margin-left: auto; +} + +.search-footer { + padding: 14px 24px; + background: var(--paper); + border-top: 1px solid var(--line); + font-size: 11px; + color: var(--text-3); + font-family: var(--font-mono); +} + +.search-footer kbd { + display: inline-block; + background: var(--card); + border: 1px solid var(--line); + border-radius: 4px; + padding: 3px 8px; + font-size: 10px; + box-shadow: var(--shadow-sm); +} + +/* responsive */ +@media (max-width: 1080px) { + .content-wrap { + grid-template-columns: 210px 1fr; + } + + .links-panel { + grid-column: 1 / -1; + position: static; + } + + .hero-inner { + grid-template-columns: 1fr; + } + + .hero-art { + display: none; + } +} + +@media (max-width: 720px) { + .content-wrap { + grid-template-columns: 1fr; + padding: 0 16px 50px; + margin-top: -30px; + } + + .side-menu { + position: static; + display: flex; + overflow-x: auto; + padding: 10px; + gap: 8px; + } + + .side-menu-title { + display: none; + } + + .menu-item { + border-left: none; + border-bottom: 3px solid transparent; + white-space: nowrap; + padding: 12px 16px; + border-radius: var(--radius-sm); + } + + .menu-item.active { + border-bottom-color: var(--orange); + } + + .hero-inner { + padding: 50px 24px 70px; + } + + .hero-stats { + gap: 32px; + } + + .hero-stat b { + font-size: 28px; + } + + .admin-fab { + right: 20px; + bottom: 20px; + width: 52px; + height: 52px; + } +} diff --git a/frontend/src/utils/auth.js b/frontend/src/utils/auth.js new file mode 100644 index 0000000..d4e0874 --- /dev/null +++ b/frontend/src/utils/auth.js @@ -0,0 +1,13 @@ +const STORAGE_KEY = 'ops_portal_auth_token' + +export function getToken() { + return localStorage.getItem(STORAGE_KEY) +} + +export function setToken(token) { + localStorage.setItem(STORAGE_KEY, token) +} + +export function removeToken() { + localStorage.removeItem(STORAGE_KEY) +} diff --git a/frontend/src/views/Portal.vue b/frontend/src/views/Portal.vue new file mode 100644 index 0000000..a60baec --- /dev/null +++ b/frontend/src/views/Portal.vue @@ -0,0 +1,332 @@ + + + diff --git a/frontend/src/views/admin/Categories.vue b/frontend/src/views/admin/Categories.vue new file mode 100644 index 0000000..b178887 --- /dev/null +++ b/frontend/src/views/admin/Categories.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/frontend/src/views/admin/Dashboard.vue b/frontend/src/views/admin/Dashboard.vue new file mode 100644 index 0000000..b878241 --- /dev/null +++ b/frontend/src/views/admin/Dashboard.vue @@ -0,0 +1,373 @@ + + + + + diff --git a/frontend/src/views/admin/Layout.vue b/frontend/src/views/admin/Layout.vue new file mode 100644 index 0000000..3a16915 --- /dev/null +++ b/frontend/src/views/admin/Layout.vue @@ -0,0 +1,391 @@ + + + + + diff --git a/frontend/src/views/admin/Links.vue b/frontend/src/views/admin/Links.vue new file mode 100644 index 0000000..85f3779 --- /dev/null +++ b/frontend/src/views/admin/Links.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/frontend/src/views/admin/Login.vue b/frontend/src/views/admin/Login.vue new file mode 100644 index 0000000..79d65fc --- /dev/null +++ b/frontend/src/views/admin/Login.vue @@ -0,0 +1,504 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/Resources.vue b/frontend/src/views/admin/Resources.vue new file mode 100644 index 0000000..0930843 --- /dev/null +++ b/frontend/src/views/admin/Resources.vue @@ -0,0 +1,635 @@ + + + + + diff --git a/frontend/src/views/admin/Settings.vue b/frontend/src/views/admin/Settings.vue new file mode 100644 index 0000000..1445bbd --- /dev/null +++ b/frontend/src/views/admin/Settings.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/frontend/src/views/admin/Users.vue b/frontend/src/views/admin/Users.vue new file mode 100644 index 0000000..655b669 --- /dev/null +++ b/frontend/src/views/admin/Users.vue @@ -0,0 +1,119 @@ + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..3f3e713 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + host: '0.0.0.0', + port: 3001, + allowedHosts: true, + proxy: { + '/api': { target: 'http://127.0.0.1:8090', changeOrigin: true } + } + }, + build: { + outDir: 'dist', + chunkSizeWarningLimit: 1500 + } +})