From 40b7c5382b242299d0a804e98db8d227efa85393 Mon Sep 17 00:00:00 2001 From: mf1bzz-desktop Date: Thu, 26 Mar 2026 23:21:10 +0800 Subject: [PATCH 01/13] =?UTF-8?q?docs:=20=E9=87=8D=E6=9E=84=E4=BA=86?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + docs/00_Root_Context.md | 100 +++++++++++------- docs/01_Backend_Standards.md | 21 ++++ docs/01_Frontend_Standards.md | 25 +++++ docs/02_API_Contracts.md | 13 +++ docs/domain_instance_lifecycle.md | 44 +++----- ...devops.md => ops_engineering_standards.md} | 15 +-- 7 files changed, 138 insertions(+), 81 deletions(-) create mode 100644 docs/01_Backend_Standards.md create mode 100644 docs/01_Frontend_Standards.md create mode 100644 docs/02_API_Contracts.md rename docs/{ops_devops.md => ops_engineering_standards.md} (85%) diff --git a/.gitignore b/.gitignore index 2b3ce74..51506b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .github/agents/ +.github/prompts/ \ No newline at end of file diff --git a/docs/00_Root_Context.md b/docs/00_Root_Context.md index 10744fd..8f98311 100644 --- a/docs/00_Root_Context.md +++ b/docs/00_Root_Context.md @@ -1,45 +1,67 @@ # MineDock Root Context -## 1. 项目核心定义与最终目标 -MineDock 是一个用于管理本地 Docker 容器实例的最小可行系统。 -当前阶段目标是:跑通一个测试容器的“创建”与“销毁”完整流程,并提供最基本的列表查询能力。 +## 1. 说明 +MineDock 是一个用于管理 Docker 容器实例的系统 -## 2. 全局系统边界 -- 后端仅实现对本地 Docker 引擎的调用。 -- 当前阶段使用 SQLite 持久化实例基础状态(container_id、name、status)。 -- 前端提供实例生命周期四段交互动作: - - 获取列表 - - 创建(仅创建容器,不自动启动) - - 开启(启动已创建容器) - - 关闭(停止运行中容器) - - 删除(彻底删除已停止容器) -- 暂不处理 Cgroups 内存限制与跨环境一致性问题。 -- 直接使用现成公开镜像进行流程验证。 +## 2. 目标 +实现一个支持易配置,功能强的游戏服务器容器化管理平台 -## 3. 全局命名与契约约定 -- HTTP 接口统一挂载在 `/api` 前缀下。 -- 资源命名采用复数形式:`/api/instances`。 -- JSON 字段命名采用 snake_case(如 `container_id`)。 -- 实例状态字段 `status` 的业务值遵循: - - `Running` - - `Stopped` +## 3. 当前系统边界 +### 所有权边界 +#### 边界内 +- 本系统创建的容器 +- 本系统的数据库及其数据 +- 本系统的工作目录及容器挂载配置的数据卷目录 +#### 边界外 +- 非本系统创建的容器 +- 宿主机的其他进程 +- 除上述工作与挂载目录外的其他宿主机文件系统 +### 功能边界 +#### 边界内 +- 容器的生命周期管理与资源配额调度 +- 容器内游戏服务器的快速安装与运行 +- 在线终端交互与控制指令下发 +- 游戏数据的持久化与灾灾备 (快照、备份与回档) +#### 边界外 +- 游戏服务端程序本身的更新与热修复 +### 数据边界 +#### 边界内 +- Docker SDK返回的运行时信息 +- 容器的标准输出日志流 +- 容器的端口暴露配置 +#### 边界外 +- Docker Daemon的集群状态 +- 主机底层的网络状态(如防火墙配置) +### 用户边界 +#### 边界内 +- 单管理员环境结构下的全局容器管控 +#### 边界外 +- 复杂的多租户资源隔离与细粒度角色控制 (RBAC) -## 4. 顶层目录架构规范 +## 4. 目录规范 +```text MineDock/ -├── docs/ # 架构与文档 -├── backend/ # Go 后端代码 -│ ├── main.go # 程序入口 -│ └── internal/ # 内部包 -│ ├── api/ # 路由 -│ ├── model/ # 数据结构定义 -│ ├── service/ # 业务逻辑 -│ ├── store/ # 内存状态管理 -│ └── utils/ # 工具函数 -└── frontend/ # 前端代码 - └── src/ - ├── api/ # HTTP 请求封装 - ├── components/ # UI 组件 - ├── composables/ # 组合式函数 - ├── stores/ # 状态管理 - ├── locales/ # i18n 语言文件 - └── assets/ # 静态资源 +├── backend/ # 后端 Go 服务 +│ ├── data/ # 数据存储目录(如 SQLite 数据库文件) +│ ├── internal/ # 内部私有代码 +│ │ ├── api/ # 路由与 HTTP 处理层 (Handlers, Routers) +│ │ ├── model/ # 领域数据模型定义实体 (如 Instance) +│ │ ├── service/ # 核心业务逻辑服务 (如调用 Docker CLI/SDK) +│ │ └── store/ # 数据持久化/存储交互层 +│ ├── main.go # 后端程序入口 +│ └── go.mod # Go 依赖配置 +├── frontend/ # 前端 Vue 项目 +│ ├── src/ +│ │ ├── api/ # 统一管理后端接口定义与请求封装 +│ │ ├── components/ # 全局复用组件(非全局使用的应当就近存放业务内) +│ │ ├── composables/ # 复用的组合式 API (如 useInstances) +│ │ ├── locales/ # 多语言 i18n 配置文件 +│ │ ├── router/ # 路由配置与全局路由守卫 +│ │ ├── stores/ # 全局跨组件状态管理 (Pinia) +│ │ └── views/ # 路由级别的业务页面组件 +│ ├── package.json # Npm 依赖配置 +│ └── vite.config.js # Vite 构建配置 +├── docs/ # 相关架构规范与领域文档 +├── Taskfile.yml # 自动化任务构建配置 +└── Readme.md # 项目文档与入口 +``` diff --git a/docs/01_Backend_Standards.md b/docs/01_Backend_Standards.md new file mode 100644 index 0000000..fca6c3a --- /dev/null +++ b/docs/01_Backend_Standards.md @@ -0,0 +1,21 @@ +# 后端规范 + +后端基于以下核心选型构建: +- **开发语言**:Go +- **持久化存储**:SQLite +- **核心依赖**:Docker SDK + +## 架构依赖规则 +为保证代码的可测试性与可维护性,`internal` 目录下的四层架构必须遵循单向依赖规则,严禁循环引用: +- **`api` 层**:作为程序的入口与防腐层,负责依赖注入 `service`,并处理 HTTP 相关的解析与返回。 +- **`service` 层**:作为纯粹的业务中枢,内部调用 `store` 进行持久化获取或通过外部组件交互。 +- **`store` 层**:负责底层数据落地设计(如 SQLite 语句),对外暴露 Interface 或直接的数据操作方法。 +- **`model` 层**:最为底层,被上述三层共同引用,不应依赖 `api`/`service`/`store` 的任何代码。 + +## 编码质量与规范 +- **代码格式化与校验**:`go fmt` 与 `go vet` +- **错误处理**: + - 核心逻辑层要求显式返回 `error`,并在合适的网络层封装为统一规范的 JSON HTTP 响应体(如 `{"status": "error", "message": "..."}`)。 + - 透传错误时,应使用 `%w` (如 `fmt.Errorf("操作容器失败: %w", err)`)保留错误堆栈及上下文。 +- **Panic 处理**:业务处理流程中严禁主动触发 `panic`。仅系统引导阶段(如 SQLite 数据源加载失败、配置文件解析失败等无法提供服务的场景)被允许 `panic` +- **无状态设计**:作为管理平台,除了 `store` 与外部环境,`api` 层与 `service` 层应保持无状态,以防发生状态数据不一致的异常 diff --git a/docs/01_Frontend_Standards.md b/docs/01_Frontend_Standards.md new file mode 100644 index 0000000..d963cbc --- /dev/null +++ b/docs/01_Frontend_Standards.md @@ -0,0 +1,25 @@ +# 前端规范 + +前端基于以下核心选型构建: +- **构建工具**:Vite +- **核心框架**:Vue 3 (Composition API) +- **开发语言**: TypeScript +- **状态管理**:Pinia +- **路由控制**:Vue Router + +## 代码质量 +- **代码格式化与校验**: 使用 ESLint + Prettier + +## 模块化与主题化 +前端 UI 必须具备高度的模块化,以支持动态主题(如浅色/暗色模式切换)和未来可能的皮肤更换 +- **禁止硬编码样式**:所有颜色、间距、字体大小必须使用 CSS 变量引入 +- **变量分层**: + - 基础变量:如 `--blue-500`, `--gray-900` + - 语义变量:如 `--bg-primary`, `--text-danger`, `--border-color` +- **主题切换实现**:通过动态修改 `` 或 `` 的 `data-theme` 属性(如 `data-theme="dark"`),结合 CSS 变量覆盖实现低成本主题切换 + +## 国际化规范 +提供多语言支持 +- **文案分离**:所有的界面静态文本禁止在 `.vue` 或 `.js` 文件中硬编码,必须通过 Vue I18n 等插件引入 +- **文件组织**:语言包统一存放于 `src/locales/`,按语言划分(如 `zh-CN.json`, `en-US.json`) +- **动态变量插值**:涉及动态数据的文案使用插值符,例如:"成功启动容器 {containerName}" diff --git a/docs/02_API_Contracts.md b/docs/02_API_Contracts.md new file mode 100644 index 0000000..e9c73e9 --- /dev/null +++ b/docs/02_API_Contracts.md @@ -0,0 +1,13 @@ +# MineDock API Contracts + +前后端接口定义,后端与Docker Daemon的交互流等 + +## 1. 容器实例生命周期接口 (Instance Lifecycle) + +| 方法 | 路径 | 说明 | 请求参数 | 返回结果 | +| --- | --- | --- | --- | --- | +| GET | `/api/instances` | 获取当前所有容器的列表 | 无 | `[{"container_id":"xxx", "name":"xxx", "status":"xxx"}]` | +| POST | `/api/instances` | 创建一个新容器(初始为 Stopped) | `{"name": "测试服1号"}` | `{"status": "success", "container_id": "xxx"}` | +| POST | `/api/instances/:id/start` | 启动指定容器实例 | 无(ID 在路径中) | `{"status": "success"}` | +| POST | `/api/instances/:id/stop` | 停止指定容器实例 | 无(ID 在路径中) | `{"status": "success"}` | +| DELETE | `/api/instances/:id` | 彻底删除指定容器实例 | 无(ID 在路径中) | `{"status": "success"}` | diff --git a/docs/domain_instance_lifecycle.md b/docs/domain_instance_lifecycle.md index 74dcc04..e8de601 100644 --- a/docs/domain_instance_lifecycle.md +++ b/docs/domain_instance_lifecycle.md @@ -1,6 +1,6 @@ -# 领域切片:容器实例生命周期(Instance Lifecycle) +# 容器实例生命周期 -## 1. 领域说明 +## 1. 说明 该领域负责容器实例的完整生命周期管理: - 查询当前实例列表 - 创建新实例(不自动启动) @@ -10,44 +10,26 @@ 该领域是当前系统的核心业务能力。 -## 2. 领域数据结构 +## 2. 数据结构 -### 2.1 Instance(后端) +### Instance(后端) ```go -// 容器相关 -// Instances 表 type Instance struct { - ContainerID string `json:"container_id"` // 映射的 Docker 容器唯一标识 - Name string `json:"name"` // 服务端名称 (比如 "测试服1号") - Status string `json:"status"` // 当前运行态 (Running, Stopped) + // 映射的 Docker 容器唯一标识 + ContainerID string `json:"container_id"` + // 服务端名称 (比如 "测试服1号") + Name string `json:"name"` + // 当前运行态 (Running, Stopped) + Status string `json:"status"` } ``` -### 2.2 字段语义与约束 -- `container_id`:Docker 容器唯一标识。 -- `name`:实例名称(例如“测试服1号”)。 -- `status`:实例运行状态,当前约定值为 `Running` 或 `Stopped`。 +## 3. HTTP 接口 +具体的接口定义请参考 [02_API_Contracts.md](02_API_Contracts.md#1-容器实例生命周期接口-instance-lifecycle)。 -## 3. 领域 HTTP 接口契约 - -| 方法 | 路径 | 说明 | 请求参数 | 返回结果 | -| --- | --- | --- | --- | --- | -| GET | `/api/instances` | 获取当前所有容器的列表 | 无 | `[{"container_id":"xxx", "name":"xxx", "status":"xxx"}]` | -| POST | `/api/instances` | 创建一个新容器(初始为 Stopped) | `{"name": "测试服1号"}` | `{"status": "success", "container_id": "xxx"}` | -| POST | `/api/instances/:id/start` | 启动指定容器实例 | 无(ID 在路径中) | `{"status": "success"}` | -| POST | `/api/instances/:id/stop` | 停止指定容器实例 | 无(ID 在路径中) | `{"status": "success"}` | -| DELETE | `/api/instances/:id` | 彻底删除指定容器实例 | 无(ID 在路径中) | `{"status": "success"}` | - -## 4. 领域状态流转(当前实现语义) +## 4. 状态流转(当前实现语义) - 创建成功后:实例进入 `Stopped`,并写入 SQLite 持久化存储。 - 启动成功后:实例状态更新为 `Running`。 - 停止成功后:实例状态更新为 `Stopped`。 - 删除成功后:销毁 Docker 容器,同时从 SQLite 中删除该实例记录;若实例仍为 `Running`,删除请求会被拒绝(需先停止)。 - 列表查询:以后端从 Docker 引擎读取的受管容器为准,并同步更新 SQLite 中的实例状态信息。 - -## 5. 领域内外依赖关系 -- 领域内部依赖后端模块: - - API 路由层 - - 业务服务层 - - SQLite 持久化存储层 -- 领域对外通过 HTTP 提供统一契约,前端仅调用该契约,不直接触达 Docker 引擎或数据库。 diff --git a/docs/ops_devops.md b/docs/ops_engineering_standards.md similarity index 85% rename from docs/ops_devops.md rename to docs/ops_engineering_standards.md index 78adcef..e513a30 100644 --- a/docs/ops_devops.md +++ b/docs/ops_engineering_standards.md @@ -1,11 +1,8 @@ # Ops & Infrastructure -本文档定义了 MineDock 项目的本地开发工作流、自动化构建与 CI/CD 规范。 - -## 1. 核心构建工具 -项目根目录维护一个统一的 `Taskfile.yml`,屏蔽底层各语言组件的启动差异。 -**当前支持的指令:** +本文档定义了项目的本地开发工作流、自动化构建与 CI/CD 规范 +## 1. 构建命令 | 指令 | 作用 | 说明 | | --- | --- | --- | | `task --list-all` | 查看可用任务 | 输出当前支持的任务列表 | @@ -20,10 +17,7 @@ | `task vet` | 执行全局静态检查 | 当前依赖 `backend:vet` | | `task test` | 执行全局测试 | 当前依赖 `backend:test` | -## 2. 环境依赖约束 - -本地开发与构建至少需要以下工具: - +## 2. 环境依赖 | 组件 | 用途 | | --- | --- | | Go | 后端编译与运行 | @@ -32,8 +26,7 @@ | task | 统一任务入口 | ## 3. CI/CD 对接规范 - -项目已在仓库内新增 GitHub Actions 工作流: +GitHub Actions 工作流: - `.github/workflows/ci.yml` - `.github/workflows/release.yml` From fa92cdc039346ebd9787ff70f821170ee8a34d80 Mon Sep 17 00:00:00 2001 From: mf1bzz-desktop Date: Thu, 26 Mar 2026 23:40:36 +0800 Subject: [PATCH 02/13] =?UTF-8?q?style:=20=E4=BF=AE=E6=94=B9=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E8=83=8C=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/main.css | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index f125f57..4ae7548 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -1,10 +1,21 @@ :root { + /* Colors */ + --brown-500: #8F6051; + --brown-600: #8c5c4e; + + /* Semantic Variables */ + --bg-primary: var(--brown-500); + --bg-secondary: var(--brown-600); --bg: #f3f6f2; --card: #ffffff; --ink: #1d2a22; --accent: #2f7d4a; --accent-strong: #205a35; --border: #d5e2d8; + + /* Layout Variables */ + --bg-stripe-size: 6px; + --bg-stripe-cycle: 12px; } * { @@ -13,15 +24,23 @@ body { margin: 0; + padding: 0; + height: 100vh; + width: 100vw; font-family: "Segoe UI", "PingFang SC", sans-serif; color: var(--ink); - background: - radial-gradient(circle at 10% 10%, #d9ead8, transparent 35%), - radial-gradient(circle at 90% 90%, #d8e8ea, transparent 35%), - var(--bg); - min-height: 100vh; - display: grid; - place-items: center; + background-color: var(--bg-primary); + background-image: repeating-linear-gradient(180deg, + var(--bg-primary) 0px, + var(--bg-primary) var(--bg-stripe-size), + var(--bg-secondary) var(--bg-stripe-size), + var(--bg-secondary) var(--bg-stripe-cycle) + ); + background-repeat: repeat; + background-size: 100% var(--bg-stripe-cycle); + display: flex; + justify-content: center; + align-items: center; } #app { From 613b1a0f759ee0b03ae35f388542eead729eb142 Mon Sep 17 00:00:00 2001 From: mf1bzz-desktop Date: Sat, 28 Mar 2026 18:24:26 +0800 Subject: [PATCH 03/13] =?UTF-8?q?style:=20=E4=B8=BA=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BE=A7=E8=BE=B9=E6=A0=8F,=20=E5=B9=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A1=B6=E9=83=A8=E6=A0=87=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- frontend/src/App.vue | 51 +++++- frontend/src/assets/main.css | 18 +- frontend/src/components/Sidebar.vue | 270 ++++++++++++++++++++++++++++ 4 files changed, 332 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/Sidebar.vue diff --git a/.gitignore b/.gitignore index 51506b1..991a266 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .github/agents/ -.github/prompts/ \ No newline at end of file +.github/prompts/ +.agent/ \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6ee20ef..facd4fa 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,5 +1,6 @@ + + diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 4ae7548..cf055c4 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -3,6 +3,12 @@ --brown-500: #8F6051; --brown-600: #8c5c4e; + /* Create Theme Variables */ + --create-brass-primary: #fcf6bd; + --create-brass-secondary: #f5cb6e; + --create-bg: #2f3538; + --create-bg-stripe: #343a3d; + /* Semantic Variables */ --bg-primary: var(--brown-500); --bg-secondary: var(--brown-600); @@ -34,8 +40,7 @@ body { var(--bg-primary) 0px, var(--bg-primary) var(--bg-stripe-size), var(--bg-secondary) var(--bg-stripe-size), - var(--bg-secondary) var(--bg-stripe-cycle) - ); + var(--bg-secondary) var(--bg-stripe-cycle)); background-repeat: repeat; background-size: 100% var(--bg-stripe-cycle); display: flex; @@ -44,9 +49,9 @@ body { } #app { - width: 100%; - display: grid; - place-items: center; + width: 100vw; + height: 100vh; + display: flex; } .panel { @@ -56,6 +61,7 @@ body { border-radius: 16px; padding: 20px; box-shadow: 0 10px 26px rgba(32, 90, 53, 0.12); + margin: auto; } h1 { @@ -107,4 +113,4 @@ input { white-space: pre-wrap; word-break: break-all; font-family: Consolas, "Courier New", monospace; -} +} \ No newline at end of file diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue new file mode 100644 index 0000000..99710f7 --- /dev/null +++ b/frontend/src/components/Sidebar.vue @@ -0,0 +1,270 @@ + + + + + From 8b06ac7e6ee749570ee0b94fb7fbb0e3bb2649ea Mon Sep 17 00:00:00 2001 From: mf1bzz-desktop Date: Sat, 28 Mar 2026 22:22:45 +0800 Subject: [PATCH 04/13] =?UTF-8?q?feat(ui):=20=E5=B0=86=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=B8=BA=E5=8D=A1=E7=89=87=E5=BC=8F=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC=E5=B9=B6=E6=B7=BB=E5=8A=A0=E6=9C=BA=E6=A2=B0=E5=8A=A8?= =?UTF-8?q?=E5=8A=9B=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.vue | 433 ++++++++++++++++++++++++---- frontend/src/assets/main.css | 1 + frontend/src/components/Sidebar.vue | 11 +- 3 files changed, 376 insertions(+), 69 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index facd4fa..d49a7ed 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,5 +1,5 @@ @@ -132,27 +179,285 @@ async function handleStop() { } .page-header { - height: 60px; /* 严格对齐左侧固定侧边栏的 60px 图标容器 */ + height: var(--header-height); /* 使用 CSS 全局变量保证模块化同步 */ display: flex; align-items: center; justify-content: center; flex-shrink: 0; - /* 透明顶部栏无需背景色 */ + position: relative; /* 供右上角按钮绝对定位使用 */ } .page-title { - margin: 0; /* 之前用 margin 往下推,现在由 flex 完美垂直居中 */ + margin: 0; color: var(--create-brass-primary, #fde285); - font-size: 24px; + font-size: 16px; font-weight: bold; letter-spacing: 2px; font-family: 'Segoe UI', "PingFang SC", sans-serif; } -/* =========== 响应式标题隐藏 =========== */ +/* 内容区操作栏与新建按钮 */ +.content-actions { + display: flex; + justify-content: flex-end; + margin-bottom: 12px; /* 减少按钮与下方卡片的距离 */ +} + +.create-btn { + padding: 8px 16px; + background-color: var(--create-brass-dark, #d8c49f); + color: #130c0c; + border: none; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.create-btn:hover { + filter: brightness(1.1); +} + +.main-content { + padding: 8px 24px 24px 24px; /* 原为 24px,缩小顶端 padding 以拉近与标题的距离 */ + flex: 1; + display: flex; + flex-direction: column; + max-width: 1200px; + margin: 0 auto; + width: 100%; +} + +/* ========== 卡片列表 ========== */ +.card-list { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 24px; +} + +.empty-state { + text-align: center; + color: #888; + padding: 40px; + border: 1px dashed #555; + border-radius: 8px; +} + +.card { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 20px; /* 增加上下纵深的内含空间,避免过粗的(12px)内连线压入核心文字中 */ + background-color: #f8f8ec; + /* 左右与上下第一层统一外框: 同步加粗扩大至霸道的 6px 结构 */ + border: 3px solid #000000; + /* 内侧阶层也严丝合缝扩展至 6px 与叠加的 12px 递进厚度进行包裹堆叠渲染 */ + box-shadow: + inset 0 3px 0 0 #f8f8ec, + inset 0 -3px 0 0 #f8f8ec, + inset 0 6px 0 0 #cdbca8, + inset 0 -6px 0 0 #cdbca8; + border-radius: 0; + /* 裁掉四个角的 3px x 3px 方块,每个角用 3 个点走直角台阶 */ + clip-path: polygon( + /* 左上角 */ 0 3px, 3px 3px, 3px 0, + /* 右上角 */ calc(100% - 3px) 0, calc(100% - 3px) 3px, 100% 3px, + /* 右下角 */ 100% calc(100% - 3px), calc(100% - 3px) calc(100% - 3px), calc(100% - 3px) 100%, + /* 左下角 */ 3px 100%, 3px calc(100% - 3px), 0 calc(100% - 3px) + ); + transition: filter 0.2s; +} + +.card:hover { + filter: brightness(0.96); +} + +.card-left { + display: flex; + flex-direction: column; + justify-content: center; +} + +.card-name { + color: #1a1a1a; + font-size: 16px; + font-weight: bold; +} + +.card-right { + display: flex; + align-items: center; + gap: 20px; +} + +/* ======= 右侧按钮组件 ======= */ +.delete-btn { + padding: 6px 16px; + background-color: rgba(255, 77, 79, 0.1); + color: #ff4d4f; + border: 1px solid #ff4d4f; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; +} + +.delete-btn:hover { + background-color: #ff4d4f; + color: #fff; +} + +/* ======= 拉杆样式的开关 ======= */ +.switch { + position: relative; + display: inline-block; + width: 48px; + height: 24px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: #555; + transition: .4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: .4s; +} + +input:checked + .slider { + background-color: #10b981; /* 开启时显现绿色 */ +} + +input:focus + .slider { + box-shadow: 0 0 1px #10b981; +} + +input:checked + .slider:before { + transform: translateX(24px); +} + +.slider.round { + border-radius: 24px; +} +.slider.round:before { + border-radius: 50%; +} + +/* ========== 弹窗样式 ========== */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background-color: #2a2a2a; + padding: 20px; + border-radius: 8px; + width: 360px; /* 恢复回原本觉得合适的宽度 */ + border: 1px solid var(--create-brass-primary, #fde285); + display: flex; + flex-direction: column; + gap: 12px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); +} + +.modal-content h3 { + margin: 0; + color: var(--create-brass-primary, #fde285); + font-size: 16px; +} + +.modal-content input { + flex: none; /* 关键修复:阻止继承全局设置中 flex-basis 对高度造成的强制拉伸 */ + padding: 8px 10px; + background: #1a1a1a; + border: 1px solid #444; + color: #fff; + border-radius: 4px; + outline: none; +} + +.modal-content input:focus { + border-color: var(--create-brass-primary, #fde285); +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 8px; +} + +.btn-cancel { + padding: 6px 12px; + font-size: 13px; + background: transparent; + border: 1px solid #666; + color: #ccc; + border-radius: 4px; + cursor: pointer; +} +.btn-cancel:hover { + background: rgba(255, 255, 255, 0.1); +} + +.btn-confirm { + padding: 6px 12px; + font-size: 13px; + background: var(--create-brass-dark, #c5a059); + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; +} +.btn-confirm:hover { + filter: brightness(1.1); +} + +/* ========== 底部输出 ========== */ +.output { + margin-top: auto; + padding: 12px; + background: #1a1a1a; + color: #a9b7c6; + border: 1px solid #333; + border-radius: 4px; + min-height: 80px; + max-height: 160px; + overflow-y: auto; + word-wrap: break-word; + white-space: pre-wrap; + font-size: 13px; +} + +/* ========== 响应式自适应 ========== */ @media (max-width: 1023px) { - .page-header { - display: none; + .page-title { + display: none; /* 移动端只隐藏标题 */ } } diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index cf055c4..e2ee7c2 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -20,6 +20,7 @@ --border: #d5e2d8; /* Layout Variables */ + --header-height: 48px; --bg-stripe-size: 6px; --bg-stripe-cycle: 12px; } diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index 99710f7..7c6cbbf 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -60,8 +60,8 @@ const isOpen = ref(false); /* =========== 移动端悬浮按钮与遮罩 =========== */ .hamburger-btn { position: fixed; - top: 4px; - left: 4px; + top: 14px; /* 精确对应桌面端通过 padding-top 计算出来的垂直偏移像素 */ + left: 12px; /* 精确对应桌面端侧边宽和内居中共计 12px 的水平偏移 */ z-index: 40; background: transparent; border: none; @@ -119,7 +119,7 @@ const isOpen = ref(false); } .mobile-header { - height: 60px; + height: var(--header-height); flex-shrink: 0; width: 100%; z-index: 10; @@ -161,7 +161,8 @@ const isOpen = ref(false); .desktop-icon-container { width: 56px; - height: 60px; + height: var(--header-height); + padding-top: 12px; /* 把汉堡挪到底侧,为顶部让出刚好 4px 的纯净视觉间隙 */ display: flex; align-items: center; justify-content: center; @@ -190,7 +191,7 @@ const isOpen = ref(false); flex: 1; display: flex; flex-direction: column; - padding: 10px 0; + padding: 4px 0 10px 0; /* 严格配合上方给底侧留出对等的 4px 间隙,实现完美对称 */ overflow-y: auto; overflow-x: hidden; z-index: 10; From e73588832e23ad2422d103121444807dc01d2eab Mon Sep 17 00:00:00 2001 From: mf1bzz-desktop Date: Sat, 28 Mar 2026 22:38:58 +0800 Subject: [PATCH 05/13] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E6=A0=B7=E5=BC=8F=E7=B3=BB=E7=BB=9F=E5=B9=B6=E5=BC=95?= =?UTF-8?q?=E5=85=A5=E8=AF=AD=E4=B9=89=E5=8C=96=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.vue | 79 +++++++++---------- frontend/src/assets/main.css | 116 +++++++++++----------------- frontend/src/components/Sidebar.vue | 42 +++++----- 3 files changed, 105 insertions(+), 132 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d49a7ed..e499d80 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -189,7 +189,7 @@ function isInstanceRunning(status) { .page-title { margin: 0; - color: var(--create-brass-primary, #fde285); + color: var(--create-brass-primary); font-size: 16px; font-weight: bold; letter-spacing: 2px; @@ -205,8 +205,8 @@ function isInstanceRunning(status) { .create-btn { padding: 8px 16px; - background-color: var(--create-brass-dark, #d8c49f); - color: #130c0c; + background-color: var(--create-brass-dark); + color: var(--card-text); border: none; border-radius: 4px; font-size: 14px; @@ -238,9 +238,9 @@ function isInstanceRunning(status) { .empty-state { text-align: center; - color: #888; + color: var(--text-muted); padding: 40px; - border: 1px dashed #555; + border: 1px dashed var(--border-muted); border-radius: 8px; } @@ -249,15 +249,13 @@ function isInstanceRunning(status) { justify-content: space-between; align-items: center; padding: 10px 20px; /* 增加上下纵深的内含空间,避免过粗的(12px)内连线压入核心文字中 */ - background-color: #f8f8ec; - /* 左右与上下第一层统一外框: 同步加粗扩大至霸道的 6px 结构 */ - border: 3px solid #000000; - /* 内侧阶层也严丝合缝扩展至 6px 与叠加的 12px 递进厚度进行包裹堆叠渲染 */ + background-color: var(--card-bg); + border: 3px solid var(--card-border); box-shadow: - inset 0 3px 0 0 #f8f8ec, - inset 0 -3px 0 0 #f8f8ec, - inset 0 6px 0 0 #cdbca8, - inset 0 -6px 0 0 #cdbca8; + inset 0 3px 0 0 var(--card-bg), + inset 0 -3px 0 0 var(--card-bg), + inset 0 6px 0 0 var(--card-border-inner), + inset 0 -6px 0 0 var(--card-border-inner); border-radius: 0; /* 裁掉四个角的 3px x 3px 方块,每个角用 3 个点走直角台阶 */ clip-path: polygon( @@ -280,7 +278,7 @@ function isInstanceRunning(status) { } .card-name { - color: #1a1a1a; + color: var(--card-text); font-size: 16px; font-weight: bold; } @@ -294,17 +292,17 @@ function isInstanceRunning(status) { /* ======= 右侧按钮组件 ======= */ .delete-btn { padding: 6px 16px; - background-color: rgba(255, 77, 79, 0.1); - color: #ff4d4f; - border: 1px solid #ff4d4f; + background-color: var(--danger-light); + color: var(--danger); + border: 1px solid var(--danger); border-radius: 4px; cursor: pointer; transition: all 0.2s; } .delete-btn:hover { - background-color: #ff4d4f; - color: #fff; + background-color: var(--danger); + color: var(--text-on-dark); } /* ======= 拉杆样式的开关 ======= */ @@ -325,7 +323,7 @@ function isInstanceRunning(status) { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; - background-color: #555; + background-color: var(--toggle-off); transition: .4s; } @@ -341,11 +339,11 @@ function isInstanceRunning(status) { } input:checked + .slider { - background-color: #10b981; /* 开启时显现绿色 */ + background-color: var(--toggle-on); } input:focus + .slider { - box-shadow: 0 0 1px #10b981; + box-shadow: 0 0 1px var(--toggle-on); } input:checked + .slider:before { @@ -366,7 +364,7 @@ input:checked + .slider:before { left: 0; width: 100vw; height: 100vh; - background: rgba(0, 0, 0, 0.6); + background: var(--modal-overlay); display: flex; justify-content: center; align-items: center; @@ -374,35 +372,34 @@ input:checked + .slider:before { } .modal-content { - background-color: #2a2a2a; + background-color: var(--modal-bg); padding: 20px; border-radius: 8px; - width: 360px; /* 恢复回原本觉得合适的宽度 */ - border: 1px solid var(--create-brass-primary, #fde285); + width: 360px; + border: 1px solid var(--create-brass-primary); display: flex; flex-direction: column; gap: 12px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + box-shadow: 0 8px 24px var(--shadow-medium); } .modal-content h3 { margin: 0; - color: var(--create-brass-primary, #fde285); + color: var(--create-brass-primary); font-size: 16px; } .modal-content input { - flex: none; /* 关键修复:阻止继承全局设置中 flex-basis 对高度造成的强制拉伸 */ padding: 8px 10px; - background: #1a1a1a; - border: 1px solid #444; - color: #fff; + background: var(--input-bg); + border: 1px solid var(--input-border); + color: var(--text-on-dark); border-radius: 4px; outline: none; } .modal-content input:focus { - border-color: var(--create-brass-primary, #fde285); + border-color: var(--create-brass-primary); } .modal-actions { @@ -416,20 +413,20 @@ input:checked + .slider:before { padding: 6px 12px; font-size: 13px; background: transparent; - border: 1px solid #666; - color: #ccc; + border: 1px solid var(--btn-secondary-border); + color: var(--btn-secondary-text); border-radius: 4px; cursor: pointer; } .btn-cancel:hover { - background: rgba(255, 255, 255, 0.1); + background: var(--hover-lighten); } .btn-confirm { padding: 6px 12px; font-size: 13px; - background: var(--create-brass-dark, #c5a059); - color: #fff; + background: var(--create-brass-dark); + color: var(--text-on-dark); border: none; border-radius: 4px; cursor: pointer; @@ -442,9 +439,9 @@ input:checked + .slider:before { .output { margin-top: auto; padding: 12px; - background: #1a1a1a; - color: #a9b7c6; - border: 1px solid #333; + background: var(--output-bg); + color: var(--output-text); + border: 1px solid var(--output-border); border-radius: 4px; min-height: 80px; max-height: 160px; diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index e2ee7c2..7b883fa 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -1,25 +1,62 @@ :root { - /* Colors */ + /* ===== 基础色板 ===== */ --brown-500: #8F6051; --brown-600: #8c5c4e; - /* Create Theme Variables */ + /* ===== Create 主题变量 ===== */ --create-brass-primary: #fcf6bd; --create-brass-secondary: #f5cb6e; + --create-brass-dark: #d8c49f; --create-bg: #2f3538; --create-bg-stripe: #343a3d; + --create-border-outer: #b58550; + --create-border-inner: #e0d68c; + --create-border-dark: #32302b; - /* Semantic Variables */ + /* ===== 语义变量 ===== */ --bg-primary: var(--brown-500); --bg-secondary: var(--brown-600); - --bg: #f3f6f2; - --card: #ffffff; --ink: #1d2a22; - --accent: #2f7d4a; - --accent-strong: #205a35; - --border: #d5e2d8; + --text-on-dark: #ffffff; + --text-muted: #888888; + --border-muted: #555555; - /* Layout Variables */ + /* ===== 功能色 ===== */ + --danger: #ff4d4f; + --danger-light: rgba(255, 77, 79, 0.1); + --success: #10b981; + + /* ===== 卡片变量 ===== */ + --card-bg: #f8f8ec; + --card-border: #000000; + --card-border-inner: #cdbca8; + --card-text: #1a1a1a; + + /* ===== 输入框与弹窗 ===== */ + --modal-bg: #2a2a2a; + --modal-overlay: rgba(0, 0, 0, 0.6); + --input-bg: #1a1a1a; + --input-border: #444444; + --btn-secondary-border: #666666; + --btn-secondary-text: #cccccc; + + /* ===== 开关 ===== */ + --toggle-off: #555555; + --toggle-on: var(--success); + + /* ===== 输出区 ===== */ + --output-bg: #1a1a1a; + --output-text: #a9b7c6; + --output-border: #333333; + + /* ===== 交互反馈 ===== */ + --hover-darken: rgba(0, 0, 0, 0.2); + --hover-lighten: rgba(255, 255, 255, 0.1); + --active-darken: rgba(0, 0, 0, 0.4); + --shadow-medium: rgba(0, 0, 0, 0.5); + --shadow-light: rgba(0, 0, 0, 0.3); + + /* ===== 布局变量 ===== */ --header-height: 48px; --bg-stripe-size: 6px; --bg-stripe-cycle: 12px; @@ -53,65 +90,4 @@ body { width: 100vw; height: 100vh; display: flex; -} - -.panel { - width: min(820px, 92vw); - background: var(--card); - border: 1px solid var(--border); - border-radius: 16px; - padding: 20px; - box-shadow: 0 10px 26px rgba(32, 90, 53, 0.12); - margin: auto; -} - -h1 { - margin: 0 0 14px; - font-size: 24px; -} - -.actions { - display: flex; - flex-wrap: wrap; - gap: 10px; -} - -button { - border: 0; - background: var(--accent); - color: #fff; - padding: 10px 14px; - border-radius: 10px; - cursor: pointer; - font-weight: 600; -} - -button:hover { - background: var(--accent-strong); -} - -.field { - margin-top: 14px; - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -input { - flex: 1 1 280px; - padding: 10px; - border: 1px solid var(--border); - border-radius: 10px; -} - -.output { - margin-top: 14px; - padding: 12px; - border-radius: 10px; - border: 1px solid var(--border); - background: #f9fcf9; - min-height: 190px; - white-space: pre-wrap; - word-break: break-all; - font-family: Consolas, "Courier New", monospace; } \ No newline at end of file diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index 7c6cbbf..561a033 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -65,7 +65,7 @@ const isOpen = ref(false); z-index: 40; background: transparent; border: none; - color: var(--create-brass-primary, #fde285); + color: var(--create-brass-primary); padding: 8px; cursor: pointer; display: flex; @@ -75,8 +75,8 @@ const isOpen = ref(false); transition: all 0.2s ease; } .hamburger-btn:hover { - background: rgba(0, 0, 0, 0.2); - color: var(--create-brass-secondary, #f5cb6e); + background: var(--hover-darken); + color: var(--create-brass-secondary); } .icon { width: 24px; @@ -86,7 +86,7 @@ const isOpen = ref(false); .sidebar-overlay { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.5); + background: var(--shadow-medium); z-index: 45; } @@ -101,20 +101,20 @@ const isOpen = ref(false); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23343a3d' d='M0,0h4v1H0z M6,0h2v1H6z M15,1h1v1H15z M0,1h3v1H0z M5,1h2v1H5z M14,2h2v1H14z M0,2h2v1H0z M4,2h2v1H4z M13,3h3v1H13z M0,3h1v1H0z M3,3h2v1H3z M12,4h4v1H12z M2,4h2v1H2z M11,5h4v1H11z M1,5h2v1H1z M10,6h4v1H10z M0,6h2v1H0z M9,7h4v1H9z M15,7h1v1H15z M0,7h1v1H0z M8,8h4v1H8z M14,8h2v1H14z M7,9h4v1H7z M13,9h2v1H13z M6,10h4v1H6z M12,10h2v1H12z M5,11h4v1H5z M11,11h2v1H11z M4,12h4v1H4z M10,12h2v1H10z M3,13h4v1H3z M9,13h2v1H9z M2,14h4v1H2z M8,14h2v1H8z M1,15h4v1H1z M7,15h2v1H7z'/%3E%3C/svg%3E"); background-size: 128px 128px; image-rendering: pixelated; - border: 4px solid #b58550; + border: 4px solid var(--create-border-outer); z-index: 50; display: flex; flex-direction: column; - box-shadow: 4px 0 16px rgba(0,0,0,0.5); + box-shadow: 4px 0 16px var(--shadow-medium); } .sidebar-mobile::after { content: ''; position: absolute; inset: 0; - border: 4px solid #e0d68c; + border: 4px solid var(--create-border-inner); pointer-events: none; - box-shadow: inset 0 0 0 4px #32302b, inset -2px 0 4px rgba(0,0,0,0.3); + box-shadow: inset 0 0 0 4px var(--create-border-dark), inset -2px 0 4px var(--shadow-light); z-index: 20; } @@ -136,10 +136,10 @@ const isOpen = ref(false); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23343a3d' d='M0,0h4v1H0z M6,0h2v1H6z M15,1h1v1H15z M0,1h3v1H0z M5,1h2v1H5z M14,2h2v1H14z M0,2h2v1H0z M4,2h2v1H4z M13,3h3v1H13z M0,3h1v1H0z M3,3h2v1H3z M12,4h4v1H12z M2,4h2v1H2z M11,5h4v1H11z M1,5h2v1H1z M10,6h4v1H10z M0,6h2v1H0z M9,7h4v1H9z M15,7h1v1H15z M0,7h1v1H0z M8,8h4v1H8z M14,8h2v1H14z M7,9h4v1H7z M13,9h2v1H13z M6,10h4v1H6z M12,10h2v1H12z M5,11h4v1H5z M11,11h2v1H11z M4,12h4v1H4z M10,12h2v1H10z M3,13h4v1H3z M9,13h2v1H9z M2,14h4v1H2z M8,14h2v1H8z M1,15h4v1H1z M7,15h2v1H7z'/%3E%3C/svg%3E"); background-size: 128px 128px; image-rendering: pixelated; - border: 4px solid #b58550; + border: 4px solid var(--create-border-outer); transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), min-width 0.3s cubic-bezier(0.4, 0, 0.2, 1); overflow: hidden; - box-shadow: 2px 0 8px rgba(0,0,0,0.3); + box-shadow: 2px 0 8px var(--shadow-light); display: none; flex-direction: column; } @@ -153,9 +153,9 @@ const isOpen = ref(false); content: ''; position: absolute; inset: 0; - border: 4px solid #e0d68c; + border: 4px solid var(--create-border-inner); pointer-events: none; - box-shadow: inset 0 0 0 4px #32302b, inset -2px 0 4px rgba(0,0,0,0.3); + box-shadow: inset 0 0 0 4px var(--create-border-dark), inset -2px 0 4px var(--shadow-light); z-index: 20; } @@ -172,7 +172,7 @@ const isOpen = ref(false); .hamburger-btn-narrow { background: transparent; border: none; - color: var(--create-brass-primary, #fde285); + color: var(--create-brass-primary); cursor: pointer; padding: 8px; border-radius: 6px; @@ -182,8 +182,8 @@ const isOpen = ref(false); transition: all 0.2s ease; } .hamburger-btn-narrow:hover { - background: rgba(0, 0, 0, 0.2); - color: var(--create-brass-secondary, #f5cb6e); + background: var(--hover-darken); + color: var(--create-brass-secondary); } /* =========== 菜单列表样式 =========== */ @@ -204,7 +204,7 @@ const isOpen = ref(false); height: 52px; padding-left: 16px; padding-right: 16px; - color: var(--create-brass-primary, #fde285); + color: var(--create-brass-primary); text-decoration: none; cursor: pointer; background: transparent; @@ -213,15 +213,15 @@ const isOpen = ref(false); } .menu-item:hover { - background: rgba(0, 0, 0, 0.2); - color: #fff; + background: var(--hover-darken); + color: var(--text-on-dark); } .menu-item.active { - background: rgba(0, 0, 0, 0.4); - border-left: 4px solid var(--create-brass-secondary, #f5cb6e); + background: var(--active-darken); + border-left: 4px solid var(--create-brass-secondary); padding-left: 12px; - color: var(--create-brass-primary, #fcf6bd); + color: var(--create-brass-primary); } .menu-icon { From 0a00c159cb6d1f1f4a5d033fa055eda667c588b5 Mon Sep 17 00:00:00 2001 From: mf1bzz-desktop Date: Sat, 28 Mar 2026 22:42:53 +0800 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20=E5=BC=95=E5=85=A5=20i18n=20?= =?UTF-8?q?=E5=9B=BD=E9=99=85=E5=8C=96=E6=94=AF=E6=8C=81=E5=B9=B6=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E5=9F=BA=E7=A1=80=E6=96=87=E6=A1=88=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 74 ++++++++++++++++++++++++++++- frontend/package.json | 3 +- frontend/src/App.vue | 43 +++++++++-------- frontend/src/components/Sidebar.vue | 4 +- frontend/src/i18n.js | 15 ++++++ frontend/src/locales/en-US.json | 31 ++++++++++++ frontend/src/locales/zh-CN.json | 31 ++++++++++++ frontend/src/main.js | 3 +- 8 files changed, 179 insertions(+), 25 deletions(-) create mode 100644 frontend/src/i18n.js create mode 100644 frontend/src/locales/en-US.json create mode 100644 frontend/src/locales/zh-CN.json diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f85441f..525deda 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,8 @@ "name": "minedock-frontend", "version": "0.1.0", "dependencies": { - "vue": "^3.5.13" + "vue": "^3.5.13", + "vue-i18n": "^9.14.5" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", @@ -503,6 +504,50 @@ "node": ">=18" } }, + "node_modules/@intlify/core-base": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz", + "integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==", + "license": "MIT", + "dependencies": { + "@intlify/message-compiler": "9.14.5", + "@intlify/shared": "9.14.5" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.5.tgz", + "integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "9.14.5", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz", + "integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -930,6 +975,12 @@ "@vue/shared": "3.5.30" } }, + "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.30", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", @@ -1319,6 +1370,27 @@ "optional": true } } + }, + "node_modules/vue-i18n": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz", + "integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==", + "deprecated": "v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "9.14.5", + "@intlify/shared": "9.14.5", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 4707343..2abb8be 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "vue": "^3.5.13" + "vue": "^3.5.13", + "vue-i18n": "^9.14.5" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index e499d80..cc7679d 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,5 +1,6 @@ + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 525deda..5a70bba 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,7 +13,9 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", - "vite": "^6.2.0" + "typescript": "^6.0.2", + "vite": "^6.2.0", + "vue-tsc": "^3.2.6" } }, "node_modules/@babel/helper-string-parser": { @@ -925,6 +927,35 @@ "vue": "^3.2.25" } }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", @@ -981,6 +1012,22 @@ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "license": "MIT" }, + "node_modules/@vue/language-core": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz", + "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + } + }, "node_modules/@vue/reactivity": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", @@ -1031,6 +1078,13 @@ "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", "license": "MIT" }, + "node_modules/alien-signals": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", + "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1139,6 +1193,13 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1157,6 +1218,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1275,6 +1343,20 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -1350,6 +1432,13 @@ } } }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, "node_modules/vue": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", @@ -1391,6 +1480,23 @@ "peerDependencies": { "vue": "^3.0.0" } + }, + "node_modules/vue-tsc": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz", + "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.2.6" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 2abb8be..cba725b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,8 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", - "vite": "^6.2.0" + "typescript": "^6.0.2", + "vite": "^6.2.0", + "vue-tsc": "^3.2.6" } } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index cc7679d..7d45c28 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,28 +1,29 @@ -