Repository: MartialBE/one-hub

Issue:
ali qvq和qwen2.5依旧无法识别图片
**例行检查**

[//]: # (方框内删除已有的空格，填 x 号)
+ [x] 我已确认目前没有类似 issue
+ [x] 我已确认我已升级到最新版本
+ [x] 我已完整查看过项目 README，尤其是常见问题部分
+ [x] 我理解并愿意跟进此 issue，协助测试和提供反馈 
+ [x] 我理解并认可上述内容，并理解项目维护者精力有限，**不遵循规则的 issue 可能会被无视或直接关闭**

**问题描述**

**复现步骤**

**预期结果**

**相关截图**
如果没有的话，请删除此节。

Hints from issue comments:

Solution patch:
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
index 77955c86..fa4fe262 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -8,6 +8,8 @@ on:
       - "v*"
     paths-ignore:
       - "README.md"
+      - "README.en.md"
+      - "docs/**"
 
 jobs:
   build-and-push:
diff --git a/README.en.md b/README.en.md
index 9e3b8a60..2cf6017c 100644
--- a/README.en.md
+++ b/README.en.md
@@ -39,7 +39,8 @@ _This project is based on [one-api](https://github.com/songquanpeng/one-api) and
 
 If you find that a new model is missing, please update the newly added model in `Backend-Model Price-Update Price`
 
-[Demo Site](https://one-api-martialbe.vercel.app/)
+[Demo Site](https://one-hub.xiao5.info/)
+[Documentation](https://one-hub-doc.vercel.app/)
 
 </div>
 
@@ -68,10 +69,6 @@ If you find that a new model is missing, please update the newly added model in
 - Support configuring user group RPM
 - Support `Prometheus` monitoring
 
-## Documentation
-
-Please refer to the [documentation](https://github.com/MartialBE/one-hub/wiki).
-
 ## Current Supported Providers
 
 | Provider                                                              | Chat                             | Embeddings | Audio  | Images   | Other                                                            |
diff --git a/README.md b/README.md
index 5ba57d44..1048bb3f 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,8 @@ _本项目是基于[one-api](https://github.com/songquanpeng/one-api)二次开
 
 **如果发现缺少新模型，请在`后台-模型价格-更新价格`中更新新增的模型**
 
-[演示网站](https://one-api-martialbe.vercel.app/)
+[演示网站](https://one-hub.xiao5.info/)
+[文档](https://one-hub-doc.vercel.app/)
 
 </div>
 
@@ -72,10 +73,6 @@ _本项目是基于[one-api](https://github.com/songquanpeng/one-api)二次开
 - 支持配置用户组 RPM
 - 支持`Prometheus`监控
 
-## 文档
-
-请查看[文档](https://github.com/MartialBE/one-hub/wiki)
-
 ## 当前支持的供应商
 
 | 供应商                                                                | Chat                     | Embeddings | Audio  | Images      | 其他                                                             |
diff --git a/common/requester/http_client.go b/common/requester/http_client.go
index 4a351595..8b9f7b7a 100644
--- a/common/requester/http_client.go
+++ b/common/requester/http_client.go
@@ -18,8 +18,8 @@ func InitHttpClient() {
 		Transport: trans,
 	}
 
-	relayTimeout := utils.GetOrDefault("relay_timeout", 600)
-	if relayTimeout != 0 {
+	relayTimeout := utils.GetOrDefault("relay_timeout", 0)
+	if relayTimeout > 0 {
 		HTTPClient.Timeout = time.Duration(relayTimeout) * time.Second
 	}
 }
diff --git a/common/utils/request_builder.go b/common/utils/request_builder.go
index 7a1933c5..ef0c5619 100644
--- a/common/utils/request_builder.go
+++ b/common/utils/request_builder.go
@@ -19,6 +19,8 @@ func RequestBuilder(
 	if body != nil {
 		if v, ok := body.(io.Reader); ok {
 			bodyReader = v
+		} else if v, ok := body.([]byte); ok {
+			bodyReader = bytes.NewBuffer(v)
 		} else {
 			var reqBytes []byte
 			reqBytes, err = json.Marshal(body)
diff --git a/controller/channel.go b/controller/channel.go
index fa5f783c..dab0f59c 100644
--- a/controller/channel.go
+++ b/controller/channel.go
@@ -233,3 +233,28 @@ func BatchDelModelChannels(c *gin.Context) {
 		"message": "更新成功",
 	})
 }
+
+func BatchDeleteChannel(c *gin.Context) {
+	var params model.BatchChannelsParams
+	err := c.ShouldBindJSON(&params)
+	if err != nil {
+		common.APIRespondWithError(c, http.StatusOK, err)
+		return
+	}
+
+	if params.Ids == nil || len(params.Ids) == 0 {
+		common.APIRespondWithError(c, http.StatusOK, errors.New("ids不能为空"))
+		return
+	}
+
+	count, err := model.BatchDeleteChannel(params.Ids)
+	if err != nil {
+		common.APIRespondWithError(c, http.StatusOK, err)
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data":    count,
+	})
+}
diff --git a/controller/channel_tag.go b/controller/channel_tag.go
index 6a9af98a..9e6a4ee8 100644
--- a/controller/channel_tag.go
+++ b/controller/channel_tag.go
@@ -1,21 +1,23 @@
 package controller
 
 import (
+	"errors"
 	"net/http"
 	"one-api/common"
+	"one-api/common/config"
 	"one-api/model"
 
 	"github.com/gin-gonic/gin"
 )
 
 func GetChannelsTagList(c *gin.Context) {
-	var params model.SearchChannelsTagParams
-	if err := c.ShouldBindQuery(&params); err != nil {
-		common.APIRespondWithError(c, http.StatusOK, err)
+	tag := c.Param("tag")
+	if tag == "" {
+		common.APIRespondWithError(c, http.StatusOK, errors.New("tag is required"))
 		return
 	}
 
-	channelsTag, err := model.GetChannelsTagList(&params)
+	channelsTag, err := model.GetChannelsTagList(tag)
 	if err != nil {
 		common.APIRespondWithError(c, http.StatusOK, err)
 		return
@@ -148,3 +150,35 @@ func UpdateChannelsTagPriority(c *gin.Context) {
 		"message": "",
 	})
 }
+
+func ChangeChannelsTagStatus(c *gin.Context) {
+	tag := c.Param("tag")
+	status := c.Param("status")
+	if tag == "" {
+		common.AbortWithMessage(c, http.StatusOK, "tag is required")
+		return
+	}
+
+	var statusInt int
+	switch status {
+	case "enable":
+		statusInt = config.TokenStatusEnabled
+	case "disable":
+		statusInt = config.TokenStatusDisabled
+	}
+
+	if statusInt == 0 {
+		common.AbortWithMessage(c, http.StatusOK, "invalid status")
+		return
+	}
+
+	err := model.ChangeChannelsTagStatus(tag, statusInt)
+	if err != nil {
+		common.APIRespondWithError(c, http.StatusOK, err)
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+	})
+}
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 00000000..0586dcd8
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,3 @@
+.yarn
+.pnp.*
+.vitepress/cache
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
new file mode 100644
index 00000000..abcf1d66
--- /dev/null
+++ b/docs/.vitepress/config.mts
@@ -0,0 +1,67 @@
+import { defineConfig } from 'vitepress'
+
+// https://vitepress.dev/reference/site-config
+export default defineConfig({
+  title: "One Hub 文档",
+  description: "One Hub 文档",
+  head: [['link', { rel: 'icon', href: 'https://raw.githubusercontent.com/MartialBE/one-api/main/web/src/assets/images/logo.svg' }]],
+  lastUpdated: true,
+  markdown: {
+    math: true,
+    image: {
+      lazyLoading: true
+    }
+  },
+  themeConfig: {
+    search: {
+      provider: 'local'
+    },
+    logo: { light: 'https://raw.githubusercontent.com/MartialBE/one-api/main/web/src/assets/images/logo.svg', dark: 'https://github.com/MartialBE/one-api/assets/42402987/c4125d1a-5577-446d-ba15-2a71c52140c1', alt: 'One Hub Logo' },
+    // https://vitepress.dev/reference/default-theme-config
+    nav: [
+      { text: '首页', link: '/' },
+      { text: '文档', link: '/deployment/index' }
+    ],
+
+    sidebar: [
+      {
+        text: '部署',
+        items: [
+          { text: '部署说明', link: '/deployment/index' },
+          { text: '环境变量', link: '/deployment/env' },
+          { text: '图床配置', link: '/deployment/storage' },
+          { text: '自动升级', link: '/deployment/update' },
+          { text: '消息通知', link: '/deployment/notify' },
+          { text: '命令行参数', link: '/deployment/cli' }
+        ]
+      },
+      {
+        text: '使用',
+        items: [
+          { text: '使用说明', link: '/use/index' },
+          { text: '添加 VertexAI', link: '/use/VertexAI' },
+          { text: 'Rerank 接口', link: '/use/Rerank' },
+          { text: '价格更新', link: '/use/prices_update' },
+          { text: '常见问题', link: '/use/FAQ' }
+        ]
+      },
+      {
+        text: '开发',
+        items: [
+          { text: '开发说明', link: '/dev/index' }
+        ]
+      },
+      {
+        text: '其他',
+        items: [
+          { text: '更新日志', link: 'https://github.com/MartialBE/one-api/releases' },
+          { text: '请我喝杯咖啡', link: 'https://github.com/MartialBE/one-hub/wiki/Coffee' }
+        ]
+      }
+    ],
+
+    socialLinks: [
+      { icon: 'github', link: 'https://github.com/MartialBE/one-hub' }
+    ]
+  }
+})
diff --git a/docs/deployment/cli.md b/docs/deployment/cli.md
new file mode 100644
index 00000000..f12d11bb
--- /dev/null
+++ b/docs/deployment/cli.md
@@ -0,0 +1,17 @@
+---
+title: "命令行参数"
+layout: doc
+outline: deep
+lastUpdated: true
+---
+
+# 命令行参数
+
+1. `--port <port_number>`: 指定服务器监听的端口号，默认为 `3000`。
+   - 例子：`--port 3000`
+2. `--log-dir <log_dir>`: 指定日志文件夹，如果没有设置，默认保存至工作目录的 `logs` 文件夹下。
+   - 例子：`--log-dir ./logs`
+3. `--version`: 打印系统版本号并退出。
+4. `--help`: 查看命令的使用帮助和参数说明。
+5. `--config <config_file>`: 指定配置文件路径，程序将会读取该文件中的配置。配置文件详见[config.example.yaml](https://raw.githubusercontent.com/MartialBE/one-api/main/config.example.yaml)
+   - 例子：`--config ./config.yaml`
diff --git a/docs/deployment/env.md b/docs/deployment/env.md
new file mode 100644
index 00000000..909480cc
--- /dev/null
+++ b/docs/deployment/env.md
@@ -0,0 +1,84 @@
+---
+title: "环境变量"
+layout: doc
+outline: deep
+lastUpdated: true
+---
+
+# 环境变量
+
+::: warning 注意
+环境变量优先级高于配置文件。
+:::
+
+本页面可能未及时更新，具体值可参考 [配置文件](https://raw.githubusercontent.com/MartialBE/one-hub/refs/heads/main/config.example.yaml)。
+
+## 配置文件转环境变量
+
+将配置文件中的变量名全部大写，遇到子集用下划线连接，例如：
+
+```yaml
+# 配置文件
+user_token_secret: "your-secret-key"
+logs:
+  filename: "one-hub.log" # 日志文件名
+```
+
+转换为环境变量：
+
+```bash
+USER_TOKEN_SECRET="your-secret-key"
+LOGS_FILENAME="one-hub.log"
+```
+
+## 环境变量说明
+
+1. `REDIS_CONN_STRING`：设置之后将使用 Redis 作为缓存使用。
+   - 例子：`REDIS_CONN_STRING=redis://default:redispw@localhost:49153`
+   - 如果数据库访问延迟很低，没有必要启用 Redis，启用后反而会出现数据滞后的问题。
+2. `SESSION_SECRET`：设置之后将使用固定的会话密钥，这样系统重新启动后已登录用户的 cookie 将依旧有效。
+   - 例子：`SESSION_SECRET=random_string`
+3. `SQL_DSN`：设置之后将使用指定数据库而非 SQLite，请使用 MySQL 或 PostgreSQL。
+   - 例子：
+     - MySQL：`SQL_DSN=root:123456@tcp(localhost:3306)/oneapi`
+     - PostgreSQL：`SQL_DSN=postgres://postgres:123456@localhost:5432/oneapi`（适配中，欢迎反馈）
+   - 注意需要提前建立数据库 `oneapi`，无需手动建表，程序将自动建表。
+   - 如果使用本地数据库：部署命令可添加 `--network="host"` 以使得容器内的程序可以访问到宿主机上的 MySQL。
+   - 如果使用云数据库：如果云服务器需要验证身份，需要在连接参数中添加 `?tls=skip-verify`。
+   - 请根据你的数据库配置修改下列参数（或者保持默认值）：
+     - `SQL_MAX_IDLE_CONNS`：最大空闲连接数，默认为 `100`。
+     - `SQL_MAX_OPEN_CONNS`：最大打开连接数，默认为 `1000`。
+       - 如果报错 `Error 1040: Too many connections`，请适当减小该值。
+     - `SQL_CONN_MAX_LIFETIME`：连接的最大生命周期，默认为 `60`，单位分钟。
+4. `FRONTEND_BASE_URL`：设置之后将重定向页面请求到指定的地址，仅限从服务器设置。
+   - 例子：`FRONTEND_BASE_URL=https://openai.justsong.cn`
+5. `MEMORY_CACHE_ENABLED`：启用内存缓存，会导致用户额度的更新存在一定的延迟，可选值为 `true` 和 `false`，未设置则默认为 `false`。
+   - 例子：`MEMORY_CACHE_ENABLED=true`
+6. `SYNC_FREQUENCY`：在启用缓存的情况下与数据库同步配置的频率，单位为秒，默认为 `600` 秒。
+   - 例子：`SYNC_FREQUENCY=60`
+7. `NODE_TYPE`：设置之后将指定节点类型，可选值为 `master` 和 `slave`，未设置则默认为 `master`。
+   - 例子：`NODE_TYPE=slave`
+8. `CHANNEL_UPDATE_FREQUENCY`：设置之后将定期更新渠道余额，单位为分钟，未设置则不进行更新。
+   - 例子：`CHANNEL_UPDATE_FREQUENCY=1440`
+9. `CHANNEL_TEST_FREQUENCY`：设置之后将定期检查渠道，单位为分钟，未设置则不进行检查。
+   - 例子：`CHANNEL_TEST_FREQUENCY=1440`
+10. `POLLING_INTERVAL`：批量更新渠道余额以及测试可用性时的请求间隔，单位为秒，默认无间隔。
+    - 例子：`POLLING_INTERVAL=5`
+11. `BATCH_UPDATE_ENABLED`：启用数据库批量更新聚合，会导致用户额度的更新存在一定的延迟可选值为 `true` 和 `false`，未设置则默认为 `false`。
+    - 例子：`BATCH_UPDATE_ENABLED=true`
+    - 如果你遇到了数据库连接数过多的问题，可以尝试启用该选项。
+12. `BATCH_UPDATE_INTERVAL=5`：批量更新聚合的时间间隔，单位为秒，默认为 `5`。
+    - 例子：`BATCH_UPDATE_INTERVAL=5`
+13. 请求频率限制：
+    - `GLOBAL_API_RATE_LIMIT`：全局 API 速率限制（除中继请求外），单 ip 三分钟内的最大请求数，默认为 `180`。
+    - `GLOBAL_WEB_RATE_LIMIT`：全局 Web 速率限制，单 ip 三分钟内的最大请求数，默认为 `60`。
+14. 编码器缓存设置：
+    - `TIKTOKEN_CACHE_DIR`：默认程序启动时会联网下载一些通用的词元的编码，如：`gpt-3.5-turbo`，在一些网络环境不稳定，或者离线情况，可能会导致启动有问题，可以配置此目录缓存数据，可迁移到离线环境。
+    - `DATA_GYM_CACHE_DIR`：目前该配置作用与 `TIKTOKEN_CACHE_DIR` 一致，但是优先级没有它高。
+15. `RELAY_TIMEOUT`：中继超时设置，单位为秒，默认不设置超时时间。
+16. `SQLITE_BUSY_TIMEOUT`：SQLite 锁等待超时设置，单位为毫秒，默认 `3000`。
+17. `TG_BOT_API_KEY`： 你的 Telegram bot 的 API 密钥。你可以在 [BotFather](https://t.me/BotFather) 获取这个密钥。
+18. `TG_WEBHOOK_SECRET`：（可选）你的 webhook 密钥。你可以自定义这个密钥。如果设置了这个密钥，将使用`webhook`的方式接收消息，否则使用轮询（Polling）的方式。
+19. `AUTO_PRICE_UPDATES`：自动更新价格，可选值为 `true` 和 `false`，未设置则默认为 `true`。开启后每次启动程序时，会检测数据库中的数据和程序中默认模型价格，如果数据库中的模型价格有缺失将会自动同步到数据库中。 开启带来的问题：你删不掉程序默认的模型价格，删除后，重启又回来了，这个选项适合跟官网一致价格的用户使用。
+20. `USER_TOKEN_SECRET` ： 设置用户令牌签名密钥，必填，大于 32 位以上， 设置后请勿修改，否则会导致用户令牌失效。
+21. `HASHIDS_SALT` ：Sqids 字母表，用于混淆用户令牌信息， 可空，如为空则使用默认字母表`abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`，如设置，则需要保证字母表中无重复字符。
diff --git a/docs/deployment/index.md b/docs/deployment/index.md
new file mode 100644
index 00000000..b5483898
--- /dev/null
+++ b/docs/deployment/index.md
@@ -0,0 +1,239 @@
+---
+title: "部署说明"
+layout: doc
+outline: deep
+lastUpdated: true
+---
+
+# 部署说明
+
+## 配置说明
+
+系统支持两种配置方式：
+
+1. 环境变量
+2. 配置文件 (config.yaml)
+
+::: tip 配置优先级
+环境变量 > 配置文件
+:::
+
+### 必要配置
+
+- `USER_TOKEN_SECRET`: 必填，用于生成用户令牌的密钥
+- `SESSION_SECRET`: 推荐填写，用于保持用户登录状态，如果不设置，每次重启后已登录用户需要重新登录
+
+## Docker 部署
+
+### 准备工作
+
+1. 创建数据目录：
+
+```bash
+# 创建主数据目录
+sudo mkdir -p /data/one-hub
+cd /data/one-hub
+```
+
+2. 确保 Docker 已正确安装并启动：
+
+```bash
+# 检查 Docker 状态
+sudo systemctl status docker
+# 如果未启动，则启动 Docker
+sudo systemctl start docker
+```
+
+::: warning 注意
+
+- `-p 3000:3000` 中的第一个 `3000` 是宿主机的端口，可以根据需要进行修改。
+- 数据和日志将会保存在宿主机的 `/data/one-hub` 目录，请确保该目录存在且具有写入权限，或者更改为合适的目录。
+- 如果启动失败，请添加 `--privileged=true`，具体参考 [issue #482](https://github.com/songquanpeng/one-api/issues/482)。
+- 如果你的并发量较大，**务必**设置 `SQL_DSN`。
+  :::
+
+### 使用环境变量部署
+
+更多环境变量说明请参考 [环境变量](./env.md)。
+
+#### 使用 SQLite
+
+```shell
+docker run -d -p 3000:3000 \
+  --name one-hub \
+  --restart always \
+  -e TZ=Asia/Shanghai \
+  -e USER_TOKEN_SECRET="user_token_secret" \
+  -e SESSION_SECRET="session_secret" \
+  -v /data/one-hub:/data \
+  ghcr.io/martialbe/one-api
+```
+
+#### 使用 MySQL
+
+在 SQLite 的基础上，添加 `-e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"`。请根据实际情况修改数据库连接参数。
+
+```shell
+docker run -d -p 3000:3000 \
+  --name one-hub \
+  --restart always \
+  -e TZ=Asia/Shanghai \
+  -e USER_TOKEN_SECRET="user_token_secret" \
+  -e SESSION_SECRET="session_secret" \
+  -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" \
+  -v /data/one-hub:/data \
+  ghcr.io/martialbe/one-api
+
+```
+
+#### 使用 PostgreSQL
+
+```shell
+docker run -d -p 3000:3000 \
+  --name one-hub \
+  --restart always \
+  -e TZ=Asia/Shanghai \
+  -e USER_TOKEN_SECRET="user_token_secret" \
+  -e SESSION_SECRET="session_secret" \
+  -e SQL_DSN="postgres://postgres:123456@localhost:5432/oneapi" \
+  -v /data/one-hub:/data \
+  ghcr.io/martialbe/one-api
+```
+
+部署完毕后，访问 `http://localhost:3000` 即可。
+
+### 使用配置文件部署
+
+1. 下载配置文件模板：
+
+```bash
+cd /data/one-hub
+wget https://raw.githubusercontent.com/MartialBE/one-hub/refs/heads/main/config.example.yaml -O config.yaml
+```
+
+2. 根据需要修改配置文件内容，常用配置项包括：
+
+```yaml
+# 必要配置
+user_token_secret: "your-secret-key" # 用户令牌密钥
+session_secret: "your-session-secret" # 会话密钥
+
+# 数据库配置
+sql_dsn: "root:123456@tcp(localhost:3306)/oneapi" # MySQL配置示例
+```
+
+3. 运行容器
+
+```shell
+docker run -d -p 3000:3000 \
+  --name one-hub \
+  --restart always \
+  -e TZ=Asia/Shanghai \
+  -v /data/one-hub:/data \
+  ghcr.io/martialbe/one-api
+```
+
+## Docker Compose 部署
+
+### 准备工作
+
+1. 创建必要的目录结构：
+
+```bash
+# 创建主目录
+sudo mkdir -p /data/one-hub
+cd /data/one-hub
+# 创建子目录
+mkdir data
+```
+
+2. 下载配置文件：
+
+```bash
+# 下载 docker-compose 配置文件
+wget https://raw.githubusercontent.com/MartialBE/one-api/main/docker-compose.yml
+
+
+```
+
+3. 编辑 `docker-compose.yml` 文件，修改环境变量值。
+
+如果使用配置文件，执行下面命令，并删除 `docker-compose.yml` 文件中的 `SQL_DSN`/`REDIS_CONN_STRING`/`SESSION_SECRET` / `USER_TOKEN_SECRET` 参数：
+
+```shell
+# 下载应用配置文件模板
+wget https://raw.githubusercontent.com/MartialBE/one-api/main/config.example.yaml -O ./data/config.yaml
+```
+
+### 启动服务
+
+```shell
+docker-compose up -d
+```
+
+启动服务后，你可以通过运行以下命令来查看部署状态：
+
+```shell
+docker-compose ps
+```
+
+请确保所有的服务都已经成功启动，并且状态为 'Up'。
+
+部署完毕后，访问 `http://localhost:3000` 即可。
+
+## 手动部署
+
+1. **获取源码**：从 [GitHub Releases](https://github.com/MartialBE/one-hub/releases/latest) 下载最新的可执行文件，或者直接从源码编译。如果你选择编译源码，可以使用以下命令：
+
+   ```shell
+   git clone https://github.com/MartialBE/one-hub.git
+   ```
+
+2. **构建**：进入代码目录，构建：
+
+   ```shell
+   cd one-hub
+   make
+   ```
+
+3. **运行应用**：为构建的应用添加执行权限，并运行：
+
+   ```shell
+   chmod u+x one-api
+   ./one-api --port 3000 --log-dir ./logs
+   ```
+
+4. **访问应用**：在浏览器中访问 `http://localhost:3000` 并登录。初始账号用户名为 `root`，密码为 `123456`。
+
+5. **重新编译**：如果需要重新编译，可以使用以下命令：
+
+   ```shell
+   make clean
+   make
+   ```
+
+请确保在执行以上步骤时，你的环境已经安装了必要的工具，如 Git、Node.js、yarn 和 Go。
+
+## 多机部署
+
+### 准备工作
+
+1. 确保所有服务器都安装了必要的组件：
+
+- Docker 或 手动部署所需的组件
+- Redis（如果需要使用缓存）
+- MySQL 客户端（如果使用远程 MySQL）
+
+2. 网络配置：
+
+- 确保所有服务器能够访问主数据库
+- 如果使用 Redis，确保可以访问 Redis 服务器
+- 检查服务器间的防火墙设置
+
+1. 所有服务器 `SESSION_SECRET` 设置一样的值。
+2. 必须设置 `SQL_DSN`，使用 MySQL 数据库而非 SQLite，所有服务器连接同一个数据库。
+3. 所有从服务器必须设置 `NODE_TYPE` 为 `slave`，不设置则默认为主服务器。
+4. 设置 `SYNC_FREQUENCY` 后服务器将定期从数据库同步配置，在使用远程数据库的情况下，推荐设置该项并启用 Redis，无论主从。
+5. 从服务器可以选择设置 `FRONTEND_BASE_URL`，以重定向页面请求到主服务器。
+6. 从服务器上**分别**装好 Redis，设置好 `REDIS_CONN_STRING`，这样可以做到在缓存未过期的情况下数据库零访问，可以减少延迟。
+7. 如果主服务器访问数据库延迟也比较高，则也需要启用 Redis，并设置 `SYNC_FREQUENCY`，以定期从数据库同步配置。
diff --git a/docs/deployment/notify.md b/docs/deployment/notify.md
new file mode 100644
index 00000000..11050d54
--- /dev/null
+++ b/docs/deployment/notify.md
@@ -0,0 +1,64 @@
+---
+title: "消息通知"
+layout: doc
+outline: deep
+lastUpdated: true
+---
+
+# 消息通知
+
+当渠道被禁用时，系统会发送通知。
+
+## 配置文件配置
+
+请在 config.yaml 中填写以下字段内容，不开启保持为空即可
+
+```yaml
+notify: # 通知设置, 配置了几个通知方式，就会同时发送几次通知 如果不需要通知，可以删除这个配置
+  email: # 邮件通知 (具体stmp配置在后台设置，默认开启状态，如需要关闭请在下面禁用)
+    disable: false # 是否禁用邮件通知
+    smtp_to: "" # 收件人地址 (可空，如果为空则使用超级管理员邮箱)
+  dingTalk: # 钉钉机器人通知
+    token: "" # webhook 地址最后一串字符
+    secret: "" # 密钥 (secret/keyWord 二选一)
+    keyWord: "" # 关键字 (secret/keyWord 二选一)
+  lark: # 飞书机器人通知
+    token: "" # webhook 地址最后一串字符
+    secret: "" # 密钥 (secret/keyWord 二选一)
+    keyWord: "" # 关键字 (secret/keyWord 二选一)
+  pushdeer: # pushdeer 通知
+    url: "https://api2.pushdeer.com" # pushdeer地址 (可空，如果自建需填写)
+    pushkey: "" # pushkey
+  telegram: # Telegram 通知
+    bot_api_key: "" # 你的 Telegram bot 的 API 密钥
+    chat_id: "" # 你的 Telegram chat_id
+```
+
+## 环境变量配置
+
+### email 通知
+
+- `NOTIFY_EMAIL_DISABLE` 是否禁用邮件通知, `true` 或者 `false`
+- `NOTIFY_EMAIL_SMTP_TO` 收件人地址 (可空，如果为空则使用超级管理员邮箱)
+
+### 钉钉通知
+
+- `NOTIFY_DINGTALK_TOKEN` webhook 地址最后一串字符
+- `NOTIFY_DINGTALK_SECRET` 密钥 (secret/keyWord 二选一)
+- `NOTIFY_DINGTALK_KEYWORD` 关键字 (secret/keyWord 二选一)
+
+### 飞书通知
+
+- `NOTIFY_LARK_TOKEN` webhook 地址最后一串字符
+- `NOTIFY_LARK_SECRET` 密钥 (secret/keyWord 二选一)
+- `NOTIFY_LARK_KEYWORD` 关键字 (secret/keyWord 二选一)
+
+### pushdeer 通知
+
+- `NOTIFY_PUSHDEER_URL` pushdeer 地址 (可空，如果自建需填写)
+- `NOTIFY_PUSHDEER_PUSHKEY` pushdeer 地址 (可空，如果自建需填写)
+
+### telegram 通知
+
+- `NOTIFY_TELEGRAM_BOT_API_KEY` 你的 Telegram bot 的 API 密钥
+- `NOTIFY_TELEGRAM_CHAT_ID` 你的 Telegram chat_id
diff --git a/docs/deployment/storage.md b/docs/deployment/storage.md
new file mode 100644
index 00000000..a9c452d4
--- /dev/null
+++ b/docs/deployment/storage.md
@@ -0,0 +1,34 @@
+---
+title: "图床配置"
+layout: doc
+outline: deep
+lastUpdated: true
+---
+
+# 图床配置
+
+因为图片生成有些供应商不提供 url，所以如果设置了图床，那么会上传到图床后返回链接。
+
+在使用`gemini`支持图像输出的模型时，如果图床设置不正确，会导致图片返回失败。（gemini 原生 API 接口不需要配置）
+
+可以设置多个图床， 上传失败后，会自动使用下一个图床上传。
+
+```yaml
+storage: # 存储设置 (可选,主要用于图片生成，有些供应商不提供url，只能返回base64图片，设置后可以正常返回url格式的图片生成)
+  smms: # sm.ms 图床设置
+    secret: "" # 你的 sm.ms API 密钥
+  imgur:
+    client_id: "" # 你的 imgur client_id
+  alioss: # 阿里云OSS对象存储
+    endpoint: "" # Endpoint（地域节点）,比如oss-cn-beijing.aliyuncs.com
+    bucketName: "" # Bucket名称，比如zerodeng-superai
+    accessKeyId: "" # 阿里授权KEY,在阿里云后台用户RAM控制部分获取
+    accessKeySecret: "" # 阿里授权SECRET,在阿里云后台用户RAM控制部分获取
+  s3: # AwsS3协议
+    endpoint: "" # Endpoint（地域节点）,比如https://xxxxxx.r2.cloudflarestorage.com
+    cdnurl: "" # 公共访问域名，比如https://pub-xxxxx.r2.dev，如果不配置则使用endpoint
+    bucketName: "" # Bucket名称，比如zerodeng-superai
+    accessKeyId: "" # accessKeyId
+    accessKeySecret: "" # accessKeySecret
+    expirationDays: 3
+```
diff --git a/docs/deployment/update.md b/docs/deployment/update.md
new file mode 100644
index 00000000..3470ecee
--- /dev/null
+++ b/docs/deployment/update.md
@@ -0,0 +1,108 @@
+---
+title: "升级"
+layout: doc
+outline: deep
+lastUpdated: true
+---
+
+# 自动升级
+
+### 宝塔用户
+
+如果你是宝塔面板部署，可以很方便添加计划任务
+
+添加计划任务-> Shell 脚本 -> 将以下内容粘贴进去即可。
+
+使用 Docker Compose 部署的，记得修改`docker-compose.yml` 所在的目录
+
+```
+cd /www/wwwroot/
+```
+
+### 一般玩家
+
+此脚本可以在 Crontab 中使用，但请确认你的 Crontab 可以找到正确的 Docker 命令。建议使用绝对路径。
+
+配置 Crontab，每 5 分钟执行一次脚本：
+
+```
+*/5 * * * * /path/to/auto-update-lobe-chat.sh >> /path/to/auto-update-lobe-chat.log 2>&1
+```
+
+### Docker Compose 脚本
+
+```sh
+# 拉取最新镜像，并将输出存储在变量中
+output=$(docker pull martialbe/one-api:latest 2>&1)
+
+# 检查拉取命令是否成功执行
+if [ $? -ne 0 ]; then
+exit 1
+fi
+
+# 检查输出中是否包含特定字符串
+echo "$output" | grep -q "Image is up to date for martialbe/one-api:latest"
+
+# 如果镜像已经是最新的，则不执行任何操作
+if [ $? -eq 0 ]; then
+exit 0
+fi
+
+echo "检测到 one-api 更新"
+
+# 移除旧的容器
+echo "已移除: $(docker rm -f one-api)"
+
+# 你需要首先导航到 `docker-compose.yml` 所在的目录
+cd /www/wwwroot/
+
+# 运行新的容器
+echo "已启动: $(docker-compose up)"
+
+# 打印更新时间和版本
+echo "更新时间: $(date)"
+echo "版本: $(docker inspect martialbe/one-api:latest | grep 'org.opencontainers.image.version' | awk -F'"' '{print $4}')"
+
+# 清理未使用的镜像
+docker images | grep 'martialbe/one-api' | grep -v 'latest' | awk '{print $3}' | xargs -r docker rmi > /dev/null 2>&1
+echo "已移除旧的镜像."
+```
+
+### Docker 命令方式 脚本
+
+```sh
+# 拉取最新镜像，并将输出存储在变量中
+output=$(docker pull martialbe/one-api:latest 2>&1)
+
+# 检查拉取命令是否成功执行
+if [ $? -ne 0 ]; then
+exit 1
+fi
+
+# 检查输出中是否包含特定字符串
+echo "$output" | grep -q "Image is up to date for martialbe/one-api:latest"
+
+# 如果镜像已经是最新的，则不执行任何操作
+if [ $? -eq 0 ]; then
+exit 0
+fi
+
+echo "检测到 one-api 更新"
+
+# 移除旧的容器
+echo "已移除: $(docker rm -f one-api)"
+
+# 你需要首先导航到 `docker-compose.yml` 所在的目录
+cd /www/wwwroot/
+
+# 运行新的容器 这里使用的是SQlite的docker命令，可以改成你自己想要的
+echo "已启动: $(docker run -d -p 3000:3000 --name one-api --restart always -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data ghcr.io/martialbe/one-api)"
+
+# 打印更新时间和版本
+echo "更新时间: $(date)"
+echo "版本: $(docker inspect martialbe/one-api:latest | grep 'org.opencontainers.image.version' | awk -F'"' '{print $4}')"
+
+# 清理未使用的镜像
+docker images | grep 'martialbe/one-api' | grep -v 'latest' | awk '{print $3}' | xargs -r docker rmi > /dev/null 2>&1
+echo "已移除旧的镜像."
+```
diff --git a/docs/dev/index.md b/docs/dev/index.md
new file mode 100644
index 00000000..f2116e3b
--- /dev/null
+++ b/docs/dev/index.md
@@ -0,0 +1,114 @@
+# 开发与贡献
+
+## 目录
+
+- [本地构建](#本地构建)
+  - [环境配置](#环境配置)
+  - [编译流程](#编译流程)
+  - [运行说明](#运行说明)
+- [Docker 构建](#docker-构建)
+  - [环境配置](#环境配置-1)
+  - [编译流程](#编译流程-1)
+  - [运行说明](#运行说明-1)
+
+## 本地构建
+
+### 环境配置
+
+你需要一个 golang 与 yarn 开发环境
+
+#### 直接安装
+
+golang 官方安装指南：https://go.dev/doc/install \
+yarn 官方安装指南：https://yarnpkg.com/getting-started/install
+
+#### 通过 conda/mamba 安装 （没错它不只能管理 python）
+
+如果你已有[conda](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html)或者[mamba](https://github.com/conda-forge/miniforge)的经验，也可将其用于 golang 环境管理：
+
+```bash
+conda create -n goenv go yarn
+# mamba create -n goenv go yarn # 如果你使用 mamba
+```
+
+### 编译流程
+
+项目根目录已经提供了本地构建的 makefile
+
+```bash
+# cd one-hub
+# 确保你已经启动了开发环境，比如conda activate goenv
+make all
+# 更多 make 命令，详见makefile
+```
+
+编译成功之后你应当能够在项目根目录找到 `dist` 与 `web/build` 两个文件夹。
+
+### 运行说明
+
+运行
+
+```bash
+$ ./dist/one-api -h
+Usage of ./dist/one-api:
+  -config string
+        specify the config.yaml path (default "config.yaml")
+  -export
+        Exports prices to a JSON file.
+  -help
+        print help and exit
+  -log-dir string
+        specify the log directory
+  -port int
+        the listening port
+  -version
+        print version and exit
+```
+
+根据[使用方法](/use/index)进行具体的项目配置。
+
+## Docker 构建
+
+### 环境配置
+
+你需要 docker 环境，列出下列文档作为安装参考，任选其一即可：
+
+- MirrorZ Help，此为校园网 cernet 镜像站：https://help.mirrors.cernet.edu.cn/docker-ce/
+- docker 官方安装文档：https://docs.docker.com/engine/install/
+
+### 编译流程
+
+项目根目录已经提供了 docker 构建的 dockerfile
+
+```bash
+# cd one-hub
+docker build -t one-hub:dev .
+```
+
+编译成功后，运行
+
+```bash
+docker images | grep one-hub:dev
+```
+
+你应当能找到刚刚编译的镜像，注意与项目官方镜像区分名称。
+
+当然你也可以选择修改 Dockerfile，使用 `docker compose build` 进行编译。
+
+### 运行说明
+
+项目根目录提供了一份 [`docker-compose.yaml`](https://github.com/MartialBE/one-hub/blob/main/docker-compose.yml) 文件。你应当根据上一步 `docker build` 时采用的镜像名称进行修改，比如将`martialbe/one-api:latest`替换`one-hub:dev`。当然你也可以直接利用 `docker compose` 进行 build：
+
+```yaml
+image: martialbe/one-api:latest
+```
+
+替换为
+
+```yaml
+build:
+  dockerfile: Dockerfile
+  context: .
+```
+
+然后进行 `docker compose build` 即可。
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..86acefd6
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,62 @@
+---
+# https://vitepress.dev/reference/default-theme-home-page
+layout: home
+
+hero:
+  name: "One Hub 文档"
+  actions:
+    - theme: brand
+      text: 开始使用
+      link: /deployment/index
+    - theme: alt
+      text: 项目地址
+      link: https://github.com/MartialBE/one-hub
+
+features:
+  - title: 部署
+    icon: 🚀
+    details: 部署One Hub
+    link: /deployment/index
+  - title: 使用方法
+    icon: 📖
+    details: 使用One Hub
+    link: /use/index
+  - title: 常见问题
+    icon: 💬
+    details: 常见问题
+    link: /use/FAQ
+---
+
+::: tip 说明
+本项目是基于[one-api](https://github.com/songquanpeng/one-api)二次开发而来的，主要将原项目中的模块代码分离，模块化，并修改了前端界面。本项目同样遵循 MIT 协议。
+:::
+
+::: warning 注意
+请不要和原版混用，因为新增功能，数据库与原版不兼容
+:::
+
+<div style="text-align: center">
+
+[演示网站](https://one-hub.xiao5.info)
+
+</div>
+
+## 功能变化
+
+- 全新的 UI 界面
+- 新增用户仪表盘
+- 新增管理员分析数据统计界面
+- 重构了中转`供应商`模块
+- 支持使用`Azure Speech`模拟`TTS`功能
+- 渠道可配置单独的 http/socks5 代理
+- 支持动态返回用户模型列表
+- 支持自定义测速模型
+- 日志增加请求耗时
+- 支持和优化非 OpenAI 模型的函数调用（支持的模型可以在 lobe-chat 直接使用）
+- 支持完成倍率自定义
+- 支持完整的分页和排序
+- 支持`Telegram bot`
+- 支持模型按次收费
+- 支持模型通配符
+- 支持使用配置文件启动程序
+- 支持消息通知
diff --git a/docs/package.json b/docs/package.json
new file mode 100644
index 00000000..bd755191
--- /dev/null
+++ b/docs/package.json
@@ -0,0 +1,16 @@
+{
+  "name": "docs",
+  "devDependencies": {
+    "vitepress": "^1.6.3",
+    "vue": "^3.5.13"
+  },
+  "scripts": {
+    "docs:dev": "vitepress dev",
+    "docs:build": "vitepress build",
+    "docs:preview": "vitepress preview"
+  },
+  "packageManager": "yarn@4.8.1",
+  "dependencies": {
+    "markdown-it-mathjax3": "^4.3.2"
+  }
+}
diff --git a/docs/use/FAQ.md b/docs/use/FAQ.md
new file mode 100644
index 00000000..f79d61c5
--- /dev/null
+++ b/docs/use/FAQ.md
@@ -0,0 +1,35 @@
+---
+title: "常见问题"
+layout: doc
+outline: deep
+lastUpdated: true
+---
+
+# 常见问题
+
+1. 额度是什么？怎么计算的？One API 的额度计算有问题？
+   - 额度 = 分组倍率 _ 模型倍率 _ （提示 token 数 + 补全 token 数 \* 补全倍率）
+   - 其中补全倍率对于 GPT3.5 固定为 1.33，GPT4 为 2，与官方保持一致。
+   - 如果是非流模式，官方接口会返回消耗的总 token，但是你要注意提示和补全的消耗倍率不一样。
+   - 注意，One API 的默认倍率就是官方倍率，是已经调整过的。
+2. 账户额度足够为什么提示额度不足？
+   - 请检查你的令牌额度是否足够，这个和账户额度是分开的。
+   - 令牌额度仅供用户设置最大使用量，用户可自由设置。
+3. 提示无可用渠道？
+   - 请检查的用户分组和渠道分组设置。
+   - 以及渠道的模型设置。
+4. 渠道测试报错：`invalid character '<' looking for beginning of value`
+   - 这是因为返回值不是合法的 JSON，而是一个 HTML 页面。
+   - 大概率是你的部署站的 IP 或代理的节点被 CloudFlare 封禁了。
+5. ChatGPT Next Web 报错：`Failed to fetch`
+   - 部署的时候不要设置 `BASE_URL`。
+   - 检查你的接口地址和 API Key 有没有填对。
+   - 检查是否启用了 HTTPS，浏览器会拦截 HTTPS 域名下的 HTTP 请求。
+6. 报错：`当前分组负载已饱和，请稍后再试`
+   - 上游通道 429 了。
+7. 升级之后我的数据会丢失吗？
+   - 如果使用 MySQL，不会。
+   - 如果使用 SQLite，需要按照我所给的部署命令挂载 volume 持久化 one-api.db 数据库文件，否则容器重启后数据会丢失。
+8. 升级之前数据库需要做变更吗？
+   - 一般情况下不需要，系统将在初始化的时候自动调整。
+   - 如果需要的话，我会在更新日志中说明，并给出脚本。
diff --git a/docs/use/Rerank.md b/docs/use/Rerank.md
new file mode 100644
index 00000000..efe32455
--- /dev/null
+++ b/docs/use/Rerank.md
@@ -0,0 +1,100 @@
+---
+title: "Rerank 接口"
+layout: doc
+outline: deep
+lastUpdated: true
+---
+
+# Rerank 接口
+
+由于 rerank 接口目前没有统一标准，每家都有各自的实现方案。目前本项目支持下述几种 rerank 返回格式，分别为 Jina、Cohere、Siliconflow。
+
+通常对于提供 rerank 模型的供应商而言，会选择三者之一的 JSON 结构作为其返回形式，差别主要在于 Usage 的返回方案。目前项目代码仅以下三种供应商渠道类型包含 rerank 的解析方案。
+
+请在添加 Rerank 模型时，创建一个新的渠道，按照供应商文档以及实测得到的 JSON 结构，确定以上三种其中之一为渠道类型。自定义渠道实为 OpenAI 的类型，其下不包含 rerank 接口。
+
+## Jina
+
+```json
+{
+  "model": "jina-reranker-v2-base-multilingual",
+  "usage": {
+    "total_tokens": 815
+  },
+  "results": [
+    {
+      "index": 0,
+      "document": {
+        "text": "Organic skincare for sensitive skin with aloe vera and chamomile: Imagine the soothing embrace of nature with our organic skincare range, crafted specifically for sensitive skin. Infused with the calming properties of aloe vera and chamomile, each product provides gentle nourishment and protection. Say goodbye to irritation and hello to a glowing, healthy complexion."
+      },
+      "relevance_score": 0.8783142566680908
+    },
+    {
+      "index": 6,
+      "document": {
+        "text": "针对敏感肌专门设计的天然有机护肤产品：体验由芦荟和洋甘菊提取物带来的自然呵护。我们的护肤产品特别为敏感肌设计，温和滋润，保护您的肌肤不受刺激。让您的肌肤告别不适，迎来健康光彩。"
+      },
+      "relevance_score": 0.8783142566680908
+    },
+    {
+      "index": 4,
+      "document": {
+        "text": "Cuidado de la piel orgánico para piel sensible con aloe vera y manzanilla: Descubre el poder de la naturaleza con nuestra línea de cuidado de la piel orgánico, diseñada especialmente para pieles sensibles. Enriquecidos con aloe vera y manzanilla, estos productos ofrecen una hidratación y protección suave. Despídete de las irritaciones y saluda a una piel radiante y saludable."
+      },
+      "relevance_score": 0.8624675869941711
+    }
+  ]
+}
+```
+
+## Cohere
+
+```json
+{
+  "id": "e6b994e7-1158-4216-bdec-3ea4246434d0",
+  "results": [
+    {
+      "index": 0,
+      "relevance_score": 0.9197076
+    },
+    {
+      "index": 4,
+      "relevance_score": 0.9008183
+    },
+    {
+      "index": 8,
+      "relevance_score": 0.8800674
+    }
+  ],
+  "meta": {
+    "api_version": {
+      "version": "2"
+    },
+    "billed_units": {
+      "search_units": 1
+    }
+  }
+}
+```
+
+## Siliconflow
+
+```json
+{
+  "id": "0195c4bc8788725da3006f0779171780",
+  "results": [
+    { "index": 8, "relevance_score": 0.9983834 },
+    { "index": 0, "relevance_score": 0.9983059 },
+    { "index": 6, "relevance_score": 0.9961606 }
+  ],
+  "meta": {
+    "billed_units": {
+      "input_tokens": 896,
+      "output_tokens": 0,
+      "search_units": 0,
+      "classifications": 0
+    },
+    "tokens": { "input_tokens": 896, "output_tokens": 0 }
+  }
+}
+```
diff --git a/docs/use/VertexAI.md b/docs/use/VertexAI.md
new file mode 100644
index 00000000..a277c7cd
--- /dev/null
+++ b/docs/use/VertexAI.md
@@ -0,0 +1,32 @@
+---
+title: "添加 VertexAI"
+layout: doc
+outline: deep
+lastUpdated: true
+---
+
+# 添加 VertexAI
+
+1. 创建服务凭证
+   [打开此页面](https://console.cloud.google.com/iam-admin/serviceaccounts)。选择你的项目，然后点击“创建服务帐号”。
+   <img width="1099" alt="iShot_2024-07-12_20 53 21" src="https://github.com/user-attachments/assets/6d0412df-a219-4133-9c0e-283f665a8ed9">
+
+2. 服务账号详情，自己填一个名称
+   <img width="604" alt="iShot_2024-07-12_20 54 53" src="https://github.com/user-attachments/assets/f6d2cbed-88f2-46a9-9d80-ec6268722e83">
+
+3. 选择角色， 请在上面的过滤器中输入`Vertex AI User` 和 `Service Account Token Creator`，然后选择这两个角色。
+
+<img width="582" alt="iShot_2024-07-12_20 55 22" src="https://github.com/user-attachments/assets/679a63a2-a6b4-4669-b583-a90d66029f97">
+<img width="593" alt="iShot_2024-07-12_20 56 25" src="https://github.com/user-attachments/assets/71910d72-e622-4f06-9b60-d9df70dc3d26">
+
+4. 点击“继续”，然后点击“完成”。
+
+5. 点击你刚才创建的服务账号，然后点击“密钥”，选择“创建新密钥”，然后选择“JSON”，然后点击“创建”。
+   <img width="598" alt="iShot_2024-07-12_20 57 14" src="https://github.com/user-attachments/assets/f75956e2-9d43-492a-a622-5145b6edc65b">
+   <img width="454" alt="iShot_2024-07-12_20 58 29" src="https://github.com/user-attachments/assets/6729b086-1674-4995-9d57-58f366db81b1">
+
+<img width="386" alt="iShot_2024-07-12_20 58 45" src="https://github.com/user-attachments/assets/ae0df568-dc93-4bf2-a570-829e86c3090a">
+
+6. 创建成功后，下载 JSON 文件，在本地打开 JSON 文件，将里面所有的内容复制到渠道的`Key`中。
+
+7. 最后在[这里](https://console.cloud.google.com/apis/library/iamcredentials.googleapis.com)，启用`IAM Service Account Credentials API`。
diff --git a/docs/use/index.md b/docs/use/index.md
new file mode 100644
index 00000000..6dea4f9a
--- /dev/null
+++ b/docs/use/index.md
@@ -0,0 +1,104 @@
+---
+title: "使用方法"
+layout: doc
+outline: deep
+lastUpdated: true
+---
+
+# 使用方法
+
+系统本身开箱即用。
+
+你可以通过设置环境变量/命令行参数/配置文件进行配置。
+如果你要使用配置文件，请下载[config.example.yaml](https://raw.githubusercontent.com/MartialBE/one-api/main/config.example.yaml)，重命名为`config.yaml`，放在程序同目录下。
+
+等到系统启动后，使用 `root`(默认密码：123456) 登录系统并做进一步的配置。
+
+## API 调用
+
+在`渠道`页面中添加你的 API Key，之后在`令牌`页面中新增访问令牌。
+
+之后就可以使用你的令牌访问 One Hub 了。
+
+### OpenAI API
+
+使用方式与 [OpenAI API](https://platform.openai.com/docs/api-reference/introduction) 一致。
+
+你需要在各种用到 OpenAI API 的地方设置 API Base 为你的 One Hub 的部署地址，例如：`https://api.onehub.cn`，API Key 则为你在 One API 中生成的令牌。
+
+::: warning 注意
+具体的 API Base 的格式取决于你所使用的客户端，请尝试如下地址：
+
+- `https://api.onehub.cn`
+- `https://api.onehub.cn/v1`
+- `https://api.onehub.cn/v1/chat/completions`
+  :::
+
+#### 使用示例
+
+```bash
+curl --request POST \
+    --url https://api.onehub.cn/v1/chat/completions \
+    --header 'Authorization: Bearer sk-替换为你的key' \
+    -H "Content-Type: application/json" \
+    --data '{
+      "model": "gpt-4o-mini",
+      "messages": [
+          {
+              "role": "user",
+              "content": "hi~"
+          }
+      ]
+  }'
+```
+
+### Claude API
+
+使用方式与 [Claude API](https://docs.anthropic.com/en/api/messages) 一致。
+
+你需要在各种用到 Claude API 的地方设置 API Base 为你的 One Hub 的部署地址，例如：`https://claude.xxxx.cn/claude`，API Key 则为你在 One API 中生成的令牌。
+
+#### 使用示例
+
+```bash
+curl --request POST \
+    --url https://api.onehub.cn/claude/v1/messages \
+    -H "Content-Type: application/json" \
+    -H "x-api-key: sk-替换为你的key" \
+    --data '{
+  "model": "claude-3-haiku-20240307",
+  "max_tokens": 1024,
+  "messages": [
+    {
+      "role": "user",
+      "content": "hi~"
+    }
+  ]
+}'
+```
+
+### Gemini API
+
+你需要在各种用到 Gemini API 的地方设置 API Base 为你的 One Hub 的部署地址，例如：`https://api.onehub.cn/gemini`，API Key 则为你在 One API 中生成的令牌。
+
+#### 使用示例
+
+```bash
+curl --request POST \
+  --url https://api.onehub.cn/gemini/v1alpha/models/gemini-2.0-pro-exp:generateContent \
+  --header 'Content-Type: application/json' \
+  --header 'x-goog-api-key: sk-替换为你的key' \
+  --data '{
+	"contents": [
+
+		{
+			"role": "user",
+			"parts": [
+				{
+					"text": "hi~"
+				}
+			]
+		}
+	]
+}'
+```
diff --git a/docs/use/prices_update.md b/docs/use/prices_update.md
new file mode 100644
index 00000000..a2e4839a
--- /dev/null
+++ b/docs/use/prices_update.md
@@ -0,0 +1,33 @@
+---
+title: "价格更新"
+layout: doc
+outline: deep
+lastUpdated: true
+---
+
+# 价格更新
+
+目前定价管理是通过管理员账户下的`运营 --> 模型价格 --> 更新价格`进行配置的。
+
+项目官方提供一张价格表：<https://raw.githubusercontent.com/MartialBE/one-api/prices/prices.json>
+
+另外社区维护的价格表项目如下：
+
+- [Oaklight/onehub_prices](https://github.com/Oaklight/onehub_prices)
+
+  - 每 2 小时自动同步 Openrouter 和 Siliconflow 更新，定期手动核验其他供应商。
+  - 供应商 id 映射：使用任何价格表前，请务必检查 [ownedby.json](https://raw.githubusercontent.com/Oaklight/onehub_prices/prices/ownedby.json) 以确保供应商 ID 与 价格表 channel id 对应，否则“可用模型”页面无法按供应商正确显示。
+  - 完整价格表: 适用于 one-hub 的完整价格表，合并 MartialBE/one-hub 价格表，另外提供了更多供应商：<https://raw.githubusercontent.com/Oaklight/onehub_prices/prices/oneapi_prices.json>
+  - 核心供应商价格表: 仅包含 MartialBE/one-hub 目前定义的供应商 id <= 1000 的核心供应商价格表：<https://raw.githubusercontent.com/Oaklight/onehub_prices/prices/onehub_only_prices.json>
+  - siliconflow 价格表：<https://raw.githubusercontent.com/Oaklight/onehub_prices/prices/siliconflow_prices.json>
+  - openrouter 价格表：<https://raw.githubusercontent.com/Oaklight/onehub_prices/prices/openrouter_prices.json>
+
+- [woodchen-ink 维护](https://github.com/MartialBE/one-hub/issues/562#issuecomment-2746243372)
+
+  - 价格连接：<https://aimodels-prices.q58.club/api/prices/rates>
+
+需要明确的是，除了项目官方默认提供的供应商列表（`运营 --> 模型归属`）外，即 `id > 1000`的供应商，需要和项目价格表的供应商 id 能够匹配的上才可以正确显示价格。所以你需要同时关注：
+
+- ownedby 列表，一般通过 onehub 网页`运营 --> 模型归属`维护。
+- prices 列表，通过`运营 --> 模型价格`手动维护，或 json 连接更新。
+- Oaklight/onehub_prices 项目提供了 [sync_price.py](https://raw.githubusercontent.com/Oaklight/onehub_prices/refs/heads/master/src/sync_pricing.py) 和 [sync_ownedby.py](https://raw.githubusercontent.com/Oaklight/onehub_prices/refs/heads/master/src/sync_ownedby.py) 两份自动同步脚本。用法见项目[README](https://github.com/Oaklight/onehub_prices?tab=readme-ov-file#%E4%BB%B7%E6%A0%BC%E5%90%8C%E6%AD%A5%E6%8C%87%E5%AF%BC)。
diff --git a/docs/yarn.lock b/docs/yarn.lock
new file mode 100644
index 00000000..6bb10c3f
--- /dev/null
+++ b/docs/yarn.lock
@@ -0,0 +1,2923 @@
+# This file is generated by running "yarn install" inside your project.
+# Manual changes might be lost - proceed with caution!
+
+__metadata:
+  version: 8
+  cacheKey: 10c0
+
+"@algolia/autocomplete-core@npm:1.17.7":
+  version: 1.17.7
+  resolution: "@algolia/autocomplete-core@npm:1.17.7"
+  dependencies:
+    "@algolia/autocomplete-plugin-algolia-insights": "npm:1.17.7"
+    "@algolia/autocomplete-shared": "npm:1.17.7"
+  checksum: 10c0/603e0f0157eed71a8fabfba2d14ca846e399dc4e10bc300eb2f018529f9ac68f689193f582b6e97828e01bb150c045bb7d251aa40950a058a191dc560895ed98
+  languageName: node
+  linkType: hard
+
+"@algolia/autocomplete-plugin-algolia-insights@npm:1.17.7":
+  version: 1.17.7
+  resolution: "@algolia/autocomplete-plugin-algolia-insights@npm:1.17.7"
+  dependencies:
+    "@algolia/autocomplete-shared": "npm:1.17.7"
+  peerDependencies:
+    search-insights: ">= 1 < 3"
+  checksum: 10c0/4f0f6b87ca76ea2fb45bfaa8a14c206d5bead60962b80bad10fd26928a37835d61a7420cbfd07cc2f1eb027b23b2e14f5796acfc35a74a9f51653367ee95e506
+  languageName: node
+  linkType: hard
+
+"@algolia/autocomplete-preset-algolia@npm:1.17.7":
+  version: 1.17.7
+  resolution: "@algolia/autocomplete-preset-algolia@npm:1.17.7"
+  dependencies:
+    "@algolia/autocomplete-shared": "npm:1.17.7"
+  peerDependencies:
+    "@algolia/client-search": ">= 4.9.1 < 6"
+    algoliasearch: ">= 4.9.1 < 6"
+  checksum: 10c0/eb20746cbba532f8ade62fb48b7d2b6e9b2e0b5acc33bc80071630d3da724d78242de9c06cf838bef402ce2a912e86ab018bd2f6728ecb0f981a22c65bbbb2cb
+  languageName: node
+  linkType: hard
+
+"@algolia/autocomplete-shared@npm:1.17.7":
+  version: 1.17.7
+  resolution: "@algolia/autocomplete-shared@npm:1.17.7"
+  peerDependencies:
+    "@algolia/client-search": ">= 4.9.1 < 6"
+    algoliasearch: ">= 4.9.1 < 6"
+  checksum: 10c0/9eb0c3ab57c7bae5b9c1d4c5c58dfdab56d1f4591f7488bd3d1dfd372eb8fa03416c97e247a3fcd581cda075eaea8b973dcfa306a8085c67d71f14513e3f5c5b
+  languageName: node
+  linkType: hard
+
+"@algolia/client-abtesting@npm:5.23.2":
+  version: 5.23.2
+  resolution: "@algolia/client-abtesting@npm:5.23.2"
+  dependencies:
+    "@algolia/client-common": "npm:5.23.2"
+    "@algolia/requester-browser-xhr": "npm:5.23.2"
+    "@algolia/requester-fetch": "npm:5.23.2"
+    "@algolia/requester-node-http": "npm:5.23.2"
+  checksum: 10c0/cd0f89408a3aacca6a9fa07f1c7303f7179fb8d16585015e8a1c40f63e637613fe1ed2249f5fe523db589dede1741da5b8a8e03cf081d0428bb63d1eb60721db
+  languageName: node
+  linkType: hard
+
+"@algolia/client-analytics@npm:5.23.2":
+  version: 5.23.2
+  resolution: "@algolia/client-analytics@npm:5.23.2"
+  dependencies:
+    "@algolia/client-common": "npm:5.23.2"
+    "@algolia/requester-browser-xhr": "npm:5.23.2"
+    "@algolia/requester-fetch": "npm:5.23.2"
+    "@algolia/requester-node-http": "npm:5.23.2"
+  checksum: 10c0/f377955b032d3213bc713915fd1b04abe145383c92591e2877cbc7959402ebfccb3c0a6bc070147c7684e9ea52268fdeccaad7d175ebbe67af9b942c4edcce47
+  languageName: node
+  linkType: hard
+
+"@algolia/client-common@npm:5.23.2":
+  version: 5.23.2
+  resolution: "@algolia/client-common@npm:5.23.2"
+  checksum: 10c0/0a1b6a7bbad69944d4d997c6c57ee533ea378eeb7ce44b40eb694a1a312943b8ce3cd79e5fce3b670497704d144808300c05b8e526e487f0a21b73088c72f350
+  languageName: node
+  linkType: hard
+
+"@algolia/client-insights@npm:5.23.2":
+  version: 5.23.2
+  resolution: "@algolia/client-insights@npm:5.23.2"
+  dependencies:
+    "@algolia/client-common": "npm:5.23.2"
+    "@algolia/requester-browser-xhr": "npm:5.23.2"
+    "@algolia/requester-fetch": "npm:5.23.2"
+    "@algolia/requester-node-http": "npm:5.23.2"
+  checksum: 10c0/c01a4acad95fec26d592fbab5ba243abed9f7feb08824d4a4e74c8b12333819e43327c9ca6d650199ff29e0046ed3ce374f840974417bec21b4dc562360b6527
+  languageName: node
+  linkType: hard
+
+"@algolia/client-personalization@npm:5.23.2":
+  version: 5.23.2
+  resolution: "@algolia/client-personalization@npm:5.23.2"
+  dependencies:
+    "@algolia/client-common": "npm:5.23.2"
+    "@algolia/requester-browser-xhr": "npm:5.23.2"
+    "@algolia/requester-fetch": "npm:5.23.2"
+    "@algolia/requester-node-http": "npm:5.23.2"
+  checksum: 10c0/9fb2efb566f014b22842e7fcdbed2b147b66cc632b793312cf9e0ec2c09a98a1e7d8b997c86d4f58eaaf7faa7dcdb1507ba56cbb078285049ec47527bf72367b
+  languageName: node
+  linkType: hard
+
+"@algolia/client-query-suggestions@npm:5.23.2":
+  version: 5.23.2
+  resolution: "@algolia/client-query-suggestions@npm:5.23.2"
+  dependencies:
+    "@algolia/client-common": "npm:5.23.2"
+    "@algolia/requester-browser-xhr": "npm:5.23.2"
+    "@algolia/requester-fetch": "npm:5.23.2"
+    "@algolia/requester-node-http": "npm:5.23.2"
+  checksum: 10c0/a49fee861b07c06bdb0b51ae5bac597b79414d0cda4c8d3c864b7dd3890100f6977a3dfc60b84f49bb7a672b70b55a07b77b198b51a4e5ec783eb4902e4525a2
+  languageName: node
+  linkType: hard
+
+"@algolia/client-search@npm:5.23.2":
+  version: 5.23.2
+  resolution: "@algolia/client-search@npm:5.23.2"
+  dependencies:
+    "@algolia/client-common": "npm:5.23.2"
+    "@algolia/requester-browser-xhr": "npm:5.23.2"
+    "@algolia/requester-fetch": "npm:5.23.2"
+    "@algolia/requester-node-http": "npm:5.23.2"
+  checksum: 10c0/72c9ababf6cbbcf1b7ecae54f213661db3ebfcfd366b6ef2e8190b15a89947fd3b00fa30ab2951c4ee8971f813485e83917febd2c000bab711b815ad4d61a1f6
+  languageName: node
+  linkType: hard
+
+"@algolia/ingestion@npm:1.23.2":
+  version: 1.23.2
+  resolution: "@algolia/ingestion@npm:1.23.2"
+  dependencies:
+    "@algolia/client-common": "npm:5.23.2"
+    "@algolia/requester-browser-xhr": "npm:5.23.2"
+    "@algolia/requester-fetch": "npm:5.23.2"
+    "@algolia/requester-node-http": "npm:5.23.2"
+  checksum: 10c0/62a20840e5d43bf32e1cbeaf6e21770c44a46a558b23b790eb58f196cbd3b94851e7e3d6bc069b737cb42e4ddf179a8f00bbf6615ad1a4ee060286b1aca9fb35
+  languageName: node
+  linkType: hard
+
+"@algolia/monitoring@npm:1.23.2":
+  version: 1.23.2
+  resolution: "@algolia/monitoring@npm:1.23.2"
+  dependencies:
+    "@algolia/client-common": "npm:5.23.2"
+    "@algolia/requester-browser-xhr": "npm:5.23.2"
+    "@algolia/requester-fetch": "npm:5.23.2"
+    "@algolia/requester-node-http": "npm:5.23.2"
+  checksum: 10c0/5dd770715273451e4653f3beed97a980671dd8816a35b2236495ab42923e86d53a04bdad2f469c7f978cb73a4cdad4148d43ef28502f842a0c810bf2ab264f1f
+  languageName: node
+  linkType: hard
+
+"@algolia/recommend@npm:5.23.2":
+  version: 5.23.2
+  resolution: "@algolia/recommend@npm:5.23.2"
+  dependencies:
+    "@algolia/client-common": "npm:5.23.2"
+    "@algolia/requester-browser-xhr": "npm:5.23.2"
+    "@algolia/requester-fetch": "npm:5.23.2"
+    "@algolia/requester-node-http": "npm:5.23.2"
+  checksum: 10c0/b503df19dcfd92efe4f5939f0eadbc7f8ab2f7c047ec7493603c35434c136ba9f2f9cc56f7d5931db464d7f569212dce674d92e253d7086f405b4bff7cddaf0e
+  languageName: node
+  linkType: hard
+
+"@algolia/requester-browser-xhr@npm:5.23.2":
+  version: 5.23.2
+  resolution: "@algolia/requester-browser-xhr@npm:5.23.2"
+  dependencies:
+    "@algolia/client-common": "npm:5.23.2"
+  checksum: 10c0/d2f0ea45335bb614ba606f9ba9daf707c8994941cd4cc5d3d09bcc8a230c418625aaa5b34efa2f7e59bc458cd59734b61fa500b0bf505e651b94f60341b3b19a
+  languageName: node
+  linkType: hard
+
+"@algolia/requester-fetch@npm:5.23.2":
+  version: 5.23.2
+  resolution: "@algolia/requester-fetch@npm:5.23.2"
+  dependencies:
+    "@algolia/client-common": "npm:5.23.2"
+  checksum: 10c0/17fc5a57465313ebd4100ad870d2d9e9ac78b0b39da02622e0821cbdcb5f66c2cb18a68ec530e238e817ce6d535b0a269b46366102bf0e9e539882b353e35765
+  languageName: node
+  linkType: hard
+
+"@algolia/requester-node-http@npm:5.23.2":
+  version: 5.23.2
+  resolution: "@algolia/requester-node-http@npm:5.23.2"
+  dependencies:
+    "@algolia/client-common": "npm:5.23.2"
+  checksum: 10c0/d029ceb4f2af952342054b5b983551827f2e38516312935ac5bf78593a946fcaa16b99b47e44817a60d4d001d60c8512687ba0c7e5210141ce09abff72c5e6de
+  languageName: node
+  linkType: hard
+
+"@babel/helper-string-parser@npm:^7.25.9":
+  version: 7.25.9
+  resolution: "@babel/helper-string-parser@npm:7.25.9"
+  checksum: 10c0/7244b45d8e65f6b4338a6a68a8556f2cb161b782343e97281a5f2b9b93e420cad0d9f5773a59d79f61d0c448913d06f6a2358a87f2e203cf112e3c5b53522ee6
+  languageName: node
+  linkType: hard
+
+"@babel/helper-validator-identifier@npm:^7.25.9":
+  version: 7.25.9
+  resolution: "@babel/helper-validator-identifier@npm:7.25.9"
+  checksum: 10c0/4fc6f830177b7b7e887ad3277ddb3b91d81e6c4a24151540d9d1023e8dc6b1c0505f0f0628ae653601eb4388a8db45c1c14b2c07a9173837aef7e4116456259d
+  languageName: node
+  linkType: hard
+
+"@babel/parser@npm:^7.25.3":
+  version: 7.27.0
+  resolution: "@babel/parser@npm:7.27.0"
+  dependencies:
+    "@babel/types": "npm:^7.27.0"
+  bin:
+    parser: ./bin/babel-parser.js
+  checksum: 10c0/ba2ed3f41735826546a3ef2a7634a8d10351df221891906e59b29b0a0cd748f9b0e7a6f07576858a9de8e77785aad925c8389ddef146de04ea2842047c9d2859
+  languageName: node
+  linkType: hard
+
+"@babel/types@npm:^7.27.0":
+  version: 7.27.0
+  resolution: "@babel/types@npm:7.27.0"
+  dependencies:
+    "@babel/helper-string-parser": "npm:^7.25.9"
+    "@babel/helper-validator-identifier": "npm:^7.25.9"
+  checksum: 10c0/6f1592eabe243c89a608717b07b72969be9d9d2fce1dee21426238757ea1fa60fdfc09b29de9e48d8104311afc6e6fb1702565a9cc1e09bc1e76f2b2ddb0f6e1
+  languageName: node
+  linkType: hard
+
+"@docsearch/css@npm:3.8.2":
+  version: 3.8.2
+  resolution: "@docsearch/css@npm:3.8.2"
+  checksum: 10c0/32f86b7b344834885a4a0b1a317d3fb568bafb2ceab5b4733c2d99ebd13d85899035fcb2680c940876c96d0d9f7b5db84b5be3a4d5ca41f0807775cc31991cff
+  languageName: node
+  linkType: hard
+
+"@docsearch/js@npm:3.8.2":
+  version: 3.8.2
+  resolution: "@docsearch/js@npm:3.8.2"
+  dependencies:
+    "@docsearch/react": "npm:3.8.2"
+    preact: "npm:^10.0.0"
+  checksum: 10c0/8e3f9c91287f7b7f258d41fbffc5c5c567e2554dcd8127566a771c05112efcf69b99bb6ad14e86ce4f8e506218e5ddb377f94d9a2d336e648b66a18a650c9df2
+  languageName: node
+  linkType: hard
+
+"@docsearch/react@npm:3.8.2":
+  version: 3.8.2
+  resolution: "@docsearch/react@npm:3.8.2"
+  dependencies:
+    "@algolia/autocomplete-core": "npm:1.17.7"
+    "@algolia/autocomplete-preset-algolia": "npm:1.17.7"
+    "@docsearch/css": "npm:3.8.2"
+    algoliasearch: "npm:^5.14.2"
+  peerDependencies:
+    "@types/react": ">= 16.8.0 < 19.0.0"
+    react: ">= 16.8.0 < 19.0.0"
+    react-dom: ">= 16.8.0 < 19.0.0"
+    search-insights: ">= 1 < 3"
+  peerDependenciesMeta:
+    "@types/react":
+      optional: true
+    react:
+      optional: true
+    react-dom:
+      optional: true
+    search-insights:
+      optional: true
+  checksum: 10c0/f54916d478abb2e8b797ad19b4c549c162aa04a9cdc8eca5e92d31722404ddafa64669922008bd1e723ea9d2cd8f3eee7f8ed22c224118ae961640503bd90be1
+  languageName: node
+  linkType: hard
+
+"@esbuild/aix-ppc64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/aix-ppc64@npm:0.21.5"
+  conditions: os=aix & cpu=ppc64
+  languageName: node
+  linkType: hard
+
+"@esbuild/android-arm64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/android-arm64@npm:0.21.5"
+  conditions: os=android & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"@esbuild/android-arm@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/android-arm@npm:0.21.5"
+  conditions: os=android & cpu=arm
+  languageName: node
+  linkType: hard
+
+"@esbuild/android-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/android-x64@npm:0.21.5"
+  conditions: os=android & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@esbuild/darwin-arm64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/darwin-arm64@npm:0.21.5"
+  conditions: os=darwin & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"@esbuild/darwin-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/darwin-x64@npm:0.21.5"
+  conditions: os=darwin & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@esbuild/freebsd-arm64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/freebsd-arm64@npm:0.21.5"
+  conditions: os=freebsd & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"@esbuild/freebsd-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/freebsd-x64@npm:0.21.5"
+  conditions: os=freebsd & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@esbuild/linux-arm64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-arm64@npm:0.21.5"
+  conditions: os=linux & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"@esbuild/linux-arm@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-arm@npm:0.21.5"
+  conditions: os=linux & cpu=arm
+  languageName: node
+  linkType: hard
+
+"@esbuild/linux-ia32@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-ia32@npm:0.21.5"
+  conditions: os=linux & cpu=ia32
+  languageName: node
+  linkType: hard
+
+"@esbuild/linux-loong64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-loong64@npm:0.21.5"
+  conditions: os=linux & cpu=loong64
+  languageName: node
+  linkType: hard
+
+"@esbuild/linux-mips64el@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-mips64el@npm:0.21.5"
+  conditions: os=linux & cpu=mips64el
+  languageName: node
+  linkType: hard
+
+"@esbuild/linux-ppc64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-ppc64@npm:0.21.5"
+  conditions: os=linux & cpu=ppc64
+  languageName: node
+  linkType: hard
+
+"@esbuild/linux-riscv64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-riscv64@npm:0.21.5"
+  conditions: os=linux & cpu=riscv64
+  languageName: node
+  linkType: hard
+
+"@esbuild/linux-s390x@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-s390x@npm:0.21.5"
+  conditions: os=linux & cpu=s390x
+  languageName: node
+  linkType: hard
+
+"@esbuild/linux-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-x64@npm:0.21.5"
+  conditions: os=linux & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@esbuild/netbsd-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/netbsd-x64@npm:0.21.5"
+  conditions: os=netbsd & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@esbuild/openbsd-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/openbsd-x64@npm:0.21.5"
+  conditions: os=openbsd & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@esbuild/sunos-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/sunos-x64@npm:0.21.5"
+  conditions: os=sunos & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@esbuild/win32-arm64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/win32-arm64@npm:0.21.5"
+  conditions: os=win32 & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"@esbuild/win32-ia32@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/win32-ia32@npm:0.21.5"
+  conditions: os=win32 & cpu=ia32
+  languageName: node
+  linkType: hard
+
+"@esbuild/win32-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/win32-x64@npm:0.21.5"
+  conditions: os=win32 & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@iconify-json/simple-icons@npm:^1.2.21":
+  version: 1.2.30
+  resolution: "@iconify-json/simple-icons@npm:1.2.30"
+  dependencies:
+    "@iconify/types": "npm:*"
+  checksum: 10c0/a43c4977dfe23403011e24393bc143bcab0bbba26c764f6353ec9d23a8afad08168f3e8994acdfb10a40b8e9ac62d3d3af0f7a03cc4aedaea93c525c3ed9718f
+  languageName: node
+  linkType: hard
+
+"@iconify/types@npm:*":
+  version: 2.0.0
+  resolution: "@iconify/types@npm:2.0.0"
+  checksum: 10c0/65a3be43500c7ccacf360e136d00e1717f050b7b91da644e94370256ac66f582d59212bdb30d00788aab4fc078262e91c95b805d1808d654b72f6d2072a7e4b2
+  languageName: node
+  linkType: hard
+
+"@isaacs/cliui@npm:^8.0.2":
+  version: 8.0.2
+  resolution: "@isaacs/cliui@npm:8.0.2"
+  dependencies:
+    string-width: "npm:^5.1.2"
+    string-width-cjs: "npm:string-width@^4.2.0"
+    strip-ansi: "npm:^7.0.1"
+    strip-ansi-cjs: "npm:strip-ansi@^6.0.1"
+    wrap-ansi: "npm:^8.1.0"
+    wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0"
+  checksum: 10c0/b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e
+  languageName: node
+  linkType: hard
+
+"@isaacs/fs-minipass@npm:^4.0.0":
+  version: 4.0.1
+  resolution: "@isaacs/fs-minipass@npm:4.0.1"
+  dependencies:
+    minipass: "npm:^7.0.4"
+  checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2
+  languageName: node
+  linkType: hard
+
+"@jridgewell/sourcemap-codec@npm:^1.5.0":
+  version: 1.5.0
+  resolution: "@jridgewell/sourcemap-codec@npm:1.5.0"
+  checksum: 10c0/2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18
+  languageName: node
+  linkType: hard
+
+"@npmcli/agent@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "@npmcli/agent@npm:3.0.0"
+  dependencies:
+    agent-base: "npm:^7.1.0"
+    http-proxy-agent: "npm:^7.0.0"
+    https-proxy-agent: "npm:^7.0.1"
+    lru-cache: "npm:^10.0.1"
+    socks-proxy-agent: "npm:^8.0.3"
+  checksum: 10c0/efe37b982f30740ee77696a80c196912c274ecd2cb243bc6ae7053a50c733ce0f6c09fda085145f33ecf453be19654acca74b69e81eaad4c90f00ccffe2f9271
+  languageName: node
+  linkType: hard
+
+"@npmcli/fs@npm:^4.0.0":
+  version: 4.0.0
+  resolution: "@npmcli/fs@npm:4.0.0"
+  dependencies:
+    semver: "npm:^7.3.5"
+  checksum: 10c0/c90935d5ce670c87b6b14fab04a965a3b8137e585f8b2a6257263bd7f97756dd736cb165bb470e5156a9e718ecd99413dccc54b1138c1a46d6ec7cf325982fe5
+  languageName: node
+  linkType: hard
+
+"@pkgjs/parseargs@npm:^0.11.0":
+  version: 0.11.0
+  resolution: "@pkgjs/parseargs@npm:0.11.0"
+  checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-android-arm-eabi@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-android-arm-eabi@npm:4.39.0"
+  conditions: os=android & cpu=arm
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-android-arm64@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-android-arm64@npm:4.39.0"
+  conditions: os=android & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-darwin-arm64@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-darwin-arm64@npm:4.39.0"
+  conditions: os=darwin & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-darwin-x64@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-darwin-x64@npm:4.39.0"
+  conditions: os=darwin & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-freebsd-arm64@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-freebsd-arm64@npm:4.39.0"
+  conditions: os=freebsd & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-freebsd-x64@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-freebsd-x64@npm:4.39.0"
+  conditions: os=freebsd & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-linux-arm-gnueabihf@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.39.0"
+  conditions: os=linux & cpu=arm & libc=glibc
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-linux-arm-musleabihf@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.39.0"
+  conditions: os=linux & cpu=arm & libc=musl
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-linux-arm64-gnu@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.39.0"
+  conditions: os=linux & cpu=arm64 & libc=glibc
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-linux-arm64-musl@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-linux-arm64-musl@npm:4.39.0"
+  conditions: os=linux & cpu=arm64 & libc=musl
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-linux-loongarch64-gnu@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.39.0"
+  conditions: os=linux & cpu=loong64 & libc=glibc
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-linux-powerpc64le-gnu@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.39.0"
+  conditions: os=linux & cpu=ppc64 & libc=glibc
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-linux-riscv64-gnu@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.39.0"
+  conditions: os=linux & cpu=riscv64 & libc=glibc
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-linux-riscv64-musl@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.39.0"
+  conditions: os=linux & cpu=riscv64 & libc=musl
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-linux-s390x-gnu@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.39.0"
+  conditions: os=linux & cpu=s390x & libc=glibc
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-linux-x64-gnu@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-linux-x64-gnu@npm:4.39.0"
+  conditions: os=linux & cpu=x64 & libc=glibc
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-linux-x64-musl@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-linux-x64-musl@npm:4.39.0"
+  conditions: os=linux & cpu=x64 & libc=musl
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-win32-arm64-msvc@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.39.0"
+  conditions: os=win32 & cpu=arm64
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-win32-ia32-msvc@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.39.0"
+  conditions: os=win32 & cpu=ia32
+  languageName: node
+  linkType: hard
+
+"@rollup/rollup-win32-x64-msvc@npm:4.39.0":
+  version: 4.39.0
+  resolution: "@rollup/rollup-win32-x64-msvc@npm:4.39.0"
+  conditions: os=win32 & cpu=x64
+  languageName: node
+  linkType: hard
+
+"@shikijs/core@npm:2.5.0, @shikijs/core@npm:^2.1.0":
+  version: 2.5.0
+  resolution: "@shikijs/core@npm:2.5.0"
+  dependencies:
+    "@shikijs/engine-javascript": "npm:2.5.0"
+    "@shikijs/engine-oniguruma": "npm:2.5.0"
+    "@shikijs/types": "npm:2.5.0"
+    "@shikijs/vscode-textmate": "npm:^10.0.2"
+    "@types/hast": "npm:^3.0.4"
+    hast-util-to-html: "npm:^9.0.4"
+  checksum: 10c0/8851e1db14cf09ec4c6a9ad656d67b807a53e27d0face69a234ad810b3e8198e3fa19a32e9f062a49fd000487c88d8096675bb8fbfaecc64dc99125694f2d7e1
+  languageName: node
+  linkType: hard
+
+"@shikijs/engine-javascript@npm:2.5.0":
+  version: 2.5.0
+  resolution: "@shikijs/engine-javascript@npm:2.5.0"
+  dependencies:
+    "@shikijs/types": "npm:2.5.0"
+    "@shikijs/vscode-textmate": "npm:^10.0.2"
+    oniguruma-to-es: "npm:^3.1.0"
+  checksum: 10c0/5cc43e7e0c25eb3c56e0ce48ea55e4c8f09e235e4148f5c852febcd0569f74b6e3c91887d2e5bb079821fb2f13412e551d20491b516e2ad7ae675b613a727093
+  languageName: node
+  linkType: hard
+
+"@shikijs/engine-oniguruma@npm:2.5.0":
+  version: 2.5.0
+  resolution: "@shikijs/engine-oniguruma@npm:2.5.0"
+  dependencies:
+    "@shikijs/types": "npm:2.5.0"
+    "@shikijs/vscode-textmate": "npm:^10.0.2"
+  checksum: 10c0/7c1ae4cf0196b51b8a1da9496ca2ba5fbd9d1765629e312922804507b07d794208ff6562ece555518c641c99f190ec2c22202edddd5d78c7983a163677883dfe
+  languageName: node
+  linkType: hard
+
+"@shikijs/langs@npm:2.5.0":
+  version: 2.5.0
+  resolution: "@shikijs/langs@npm:2.5.0"
+  dependencies:
+    "@shikijs/types": "npm:2.5.0"
+  checksum: 10c0/bf8007f33aa9e8d5009054e9ab00fa608605bd203090bd3a648316cb632a63b6b99009a674198f386bb0647e1ce8f0d91adac3324999a8697eb6eafc12a6f0ed
+  languageName: node
+  linkType: hard
+
+"@shikijs/themes@npm:2.5.0":
+  version: 2.5.0
+  resolution: "@shikijs/themes@npm:2.5.0"
+  dependencies:
+    "@shikijs/types": "npm:2.5.0"
+  checksum: 10c0/73e38847138a2cb4a034af15279f30f4637a4f95217cb1d4212ea708f00cb68cb6c9ed0067a41c4d67b1b6a4cec1f71034aae22c2626a24629f2e13f7363428d
+  languageName: node
+  linkType: hard
+
+"@shikijs/transformers@npm:^2.1.0":
+  version: 2.5.0
+  resolution: "@shikijs/transformers@npm:2.5.0"
+  dependencies:
+    "@shikijs/core": "npm:2.5.0"
+    "@shikijs/types": "npm:2.5.0"
+  checksum: 10c0/2a9c91215ed14182ef4cdc248ee490cef56bc220a0dd50e551be77a704c51360aea9104bdc4a25f6c54f999ebd4e1bc123075735e930daf3e2c82393373197a7
+  languageName: node
+  linkType: hard
+
+"@shikijs/types@npm:2.5.0, @shikijs/types@npm:^2.1.0":
+  version: 2.5.0
+  resolution: "@shikijs/types@npm:2.5.0"
+  dependencies:
+    "@shikijs/vscode-textmate": "npm:^10.0.2"
+    "@types/hast": "npm:^3.0.4"
+  checksum: 10c0/71e75351ac550d079d220235d40d4aff7f81c2ac8cd7d846cf16cd10db4d59bee982e097d87b4709a8ef5993add96a6841a0f6a42caebc300cfbe83191028f5d
+  languageName: node
+  linkType: hard
+
+"@shikijs/vscode-textmate@npm:^10.0.2":
+  version: 10.0.2
+  resolution: "@shikijs/vscode-textmate@npm:10.0.2"
+  checksum: 10c0/36b682d691088ec244de292dc8f91b808f95c89466af421cf84cbab92230f03c8348649c14b3251991b10ce632b0c715e416e992dd5f28ff3221dc2693fd9462
+  languageName: node
+  linkType: hard
+
+"@types/estree@npm:1.0.7":
+  version: 1.0.7
+  resolution: "@types/estree@npm:1.0.7"
+  checksum: 10c0/be815254316882f7c40847336cd484c3bc1c3e34f710d197160d455dc9d6d050ffbf4c3bc76585dba86f737f020ab20bdb137ebe0e9116b0c86c7c0342221b8c
+  languageName: node
+  linkType: hard
+
+"@types/hast@npm:^3.0.0, @types/hast@npm:^3.0.4":
+  version: 3.0.4
+  resolution: "@types/hast@npm:3.0.4"
+  dependencies:
+    "@types/unist": "npm:*"
+  checksum: 10c0/3249781a511b38f1d330fd1e3344eed3c4e7ea8eff82e835d35da78e637480d36fad37a78be5a7aed8465d237ad0446abc1150859d0fde395354ea634decf9f7
+  languageName: node
+  linkType: hard
+
+"@types/linkify-it@npm:^5":
+  version: 5.0.0
+  resolution: "@types/linkify-it@npm:5.0.0"
+  checksum: 10c0/7bbbf45b9dde17bf3f184fee585aef0e7342f6954f0377a24e4ff42ab5a85d5b806aaa5c8d16e2faf2a6b87b2d94467a196b7d2b85c9c7de2f0eaac5487aaab8
+  languageName: node
+  linkType: hard
+
+"@types/markdown-it@npm:^14.1.2":
+  version: 14.1.2
+  resolution: "@types/markdown-it@npm:14.1.2"
+  dependencies:
+    "@types/linkify-it": "npm:^5"
+    "@types/mdurl": "npm:^2"
+  checksum: 10c0/34f709f0476bd4e7b2ba7c3341072a6d532f1f4cb6f70aef371e403af8a08a7c372ba6907ac426bc618d356dab660c5b872791ff6c1ead80c483e0d639c6f127
+  languageName: node
+  linkType: hard
+
+"@types/mdast@npm:^4.0.0":
+  version: 4.0.4
+  resolution: "@types/mdast@npm:4.0.4"
+  dependencies:
+    "@types/unist": "npm:*"
+  checksum: 10c0/84f403dbe582ee508fd9c7643ac781ad8597fcbfc9ccb8d4715a2c92e4545e5772cbd0dbdf18eda65789386d81b009967fdef01b24faf6640f817287f54d9c82
+  languageName: node
+  linkType: hard
+
+"@types/mdurl@npm:^2":
+  version: 2.0.0
+  resolution: "@types/mdurl@npm:2.0.0"
+  checksum: 10c0/cde7bb571630ed1ceb3b92a28f7b59890bb38b8f34cd35326e2df43eebfc74985e6aa6fd4184e307393bad8a9e0783a519a3f9d13c8e03788c0f98e5ec869c5e
+  languageName: node
+  linkType: hard
+
+"@types/unist@npm:*, @types/unist@npm:^3.0.0":
+  version: 3.0.3
+  resolution: "@types/unist@npm:3.0.3"
+  checksum: 10c0/2b1e4adcab78388e088fcc3c0ae8700f76619dbcb4741d7d201f87e2cb346bfc29a89003cfea2d76c996e1061452e14fcd737e8b25aacf949c1f2d6b2bc3dd60
+  languageName: node
+  linkType: hard
+
+"@types/web-bluetooth@npm:^0.0.21":
+  version: 0.0.21
+  resolution: "@types/web-bluetooth@npm:0.0.21"
+  checksum: 10c0/5f47c5ec452ca31e6a9b44c8aaa54b66c4f57d4aca00166c45902a3883139580e14b254b774172ff982ba4458cc444b5aa59bb4573c2fd34562cfa599721f8e4
+  languageName: node
+  linkType: hard
+
+"@ungap/structured-clone@npm:^1.0.0":
+  version: 1.3.0
+  resolution: "@ungap/structured-clone@npm:1.3.0"
+  checksum: 10c0/0fc3097c2540ada1fc340ee56d58d96b5b536a2a0dab6e3ec17d4bfc8c4c86db345f61a375a8185f9da96f01c69678f836a2b57eeaa9e4b8eeafd26428e57b0a
+  languageName: node
+  linkType: hard
+
+"@vitejs/plugin-vue@npm:^5.2.1":
+  version: 5.2.3
+  resolution: "@vitejs/plugin-vue@npm:5.2.3"
+  peerDependencies:
+    vite: ^5.0.0 || ^6.0.0
+    vue: ^3.2.25
+  checksum: 10c0/34a719c70567582c8f345378e4f46a7edd9242e7f1595cdda408d97fccf210b8ee66b97c8555ec867f777be86ae530bd19166a542b1d7ecd6894bc544096f047
+  languageName: node
+  linkType: hard
+
+"@vue/compiler-core@npm:3.5.13":
+  version: 3.5.13
+  resolution: "@vue/compiler-core@npm:3.5.13"
+  dependencies:
+    "@babel/parser": "npm:^7.25.3"
+    "@vue/shared": "npm:3.5.13"
+    entities: "npm:^4.5.0"
+    estree-walker: "npm:^2.0.2"
+    source-map-js: "npm:^1.2.0"
+  checksum: 10c0/b89f3e3ca92c3177ae449ada1480df13d99b5b3b2cdcf3202fd37dc30f294a1db1f473209f8bae9233e2d338632219d39b2bfa6941d158cea55255e4b0b30f90
+  languageName: node
+  linkType: hard
+
+"@vue/compiler-dom@npm:3.5.13":
+  version: 3.5.13
+  resolution: "@vue/compiler-dom@npm:3.5.13"
+  dependencies:
+    "@vue/compiler-core": "npm:3.5.13"
+    "@vue/shared": "npm:3.5.13"
+  checksum: 10c0/8f424a71883c9ef4abdd125d2be8d12dd8cf94ba56089245c88734b1f87c65e10597816070ba2ea0a297a2f66dc579f39275a9a53ef5664c143a12409612cd72
+  languageName: node
+  linkType: hard
+
+"@vue/compiler-sfc@npm:3.5.13":
+  version: 3.5.13
+  resolution: "@vue/compiler-sfc@npm:3.5.13"
+  dependencies:
+    "@babel/parser": "npm:^7.25.3"
+    "@vue/compiler-core": "npm:3.5.13"
+    "@vue/compiler-dom": "npm:3.5.13"
+    "@vue/compiler-ssr": "npm:3.5.13"
+    "@vue/shared": "npm:3.5.13"
+    estree-walker: "npm:^2.0.2"
+    magic-string: "npm:^0.30.11"
+    postcss: "npm:^8.4.48"
+    source-map-js: "npm:^1.2.0"
+  checksum: 10c0/5fd57895ce2801e480c08f31f91f0d1746ed08a9c1973895fd7269615f5bcdf75497978fb358bda738938d9844dea2404064c53b2cdda991014225297acce19e
+  languageName: node
+  linkType: hard
+
+"@vue/compiler-ssr@npm:3.5.13":
+  version: 3.5.13
+  resolution: "@vue/compiler-ssr@npm:3.5.13"
+  dependencies:
+    "@vue/compiler-dom": "npm:3.5.13"
+    "@vue/shared": "npm:3.5.13"
+  checksum: 10c0/67621337b12fc414fcf9f16578961850724713a9fb64501136e432c2dfe95de99932c46fa24be9820f8bcdf8e7281f815f585b519a95ea979753bafd637dde1b
+  languageName: node
+  linkType: hard
+
+"@vue/devtools-api@npm:^7.7.0":
+  version: 7.7.2
+  resolution: "@vue/devtools-api@npm:7.7.2"
+  dependencies:
+    "@vue/devtools-kit": "npm:^7.7.2"
+  checksum: 10c0/418d3c868143a91518bc846965f7c8a955f072b8526d0f739f4d7dc00b13a0f56b214d876bfff338dc841762b526a1a4c11b5e8b0ab6dd7f3250a694ec8dfbe3
+  languageName: node
+  linkType: hard
+
+"@vue/devtools-kit@npm:^7.7.2":
+  version: 7.7.2
+  resolution: "@vue/devtools-kit@npm:7.7.2"
+  dependencies:
+    "@vue/devtools-shared": "npm:^7.7.2"
+    birpc: "npm:^0.2.19"
+    hookable: "npm:^5.5.3"
+    mitt: "npm:^3.0.1"
+    perfect-debounce: "npm:^1.0.0"
+    speakingurl: "npm:^14.0.1"
+    superjson: "npm:^2.2.1"
+  checksum: 10c0/e052ba756558040855304b6ee13ba39131a44c89a9f78ab262c79f8a0e6b58fa379e1efa306a9a50675cac3e48baeb3f86b1560f64edf48cbc0695165d0b2be6
+  languageName: node
+  linkType: hard
+
+"@vue/devtools-shared@npm:^7.7.2":
+  version: 7.7.2
+  resolution: "@vue/devtools-shared@npm:7.7.2"
+  dependencies:
+    rfdc: "npm:^1.4.1"
+  checksum: 10c0/6399135da41a91f48c3db7c59cedb01ad331af7784ef0877c15c669ad5a5d1cce68f73d50d81f85a31a90b0d6323ff807ebe5b1fb041d1e86932f2c983a0cdad
+  languageName: node
+  linkType: hard
+
+"@vue/reactivity@npm:3.5.13":
+  version: 3.5.13
+  resolution: "@vue/reactivity@npm:3.5.13"
+  dependencies:
+    "@vue/shared": "npm:3.5.13"
+  checksum: 10c0/4bf2754a4b8cc31afc8da5bdfd12bba6be67b2963a65f7c9e2b59810883c58128dfc58cce6d1e479c4f666190bc0794f17208d9efd3fc909a2e4843d2cc0e69e
+  languageName: node
+  linkType: hard
+
+"@vue/runtime-core@npm:3.5.13":
+  version: 3.5.13
+  resolution: "@vue/runtime-core@npm:3.5.13"
+  dependencies:
+    "@vue/reactivity": "npm:3.5.13"
+    "@vue/shared": "npm:3.5.13"
+  checksum: 10c0/b6be854bf082a224222614a334fbeac0e7b6445f3cf4ea45cbd49ae4bb1551200c461c14c7a452d748f2459f7402ad4dee5522d51be5a28ea4ae1f699a7c016f
+  languageName: node
+  linkType: hard
+
+"@vue/runtime-dom@npm:3.5.13":
+  version: 3.5.13
+  resolution: "@vue/runtime-dom@npm:3.5.13"
+  dependencies:
+    "@vue/reactivity": "npm:3.5.13"
+    "@vue/runtime-core": "npm:3.5.13"
+    "@vue/shared": "npm:3.5.13"
+    csstype: "npm:^3.1.3"
+  checksum: 10c0/8ee7f3980d19f77f8e7ae854e3ff1f7ee9a9b8b4e214c8d0492e1180ae818e33c04803b3d094503524d557431a30728b78cf15c3683d8abbbbd1b263a299d62a
+  languageName: node
+  linkType: hard
+
+"@vue/server-renderer@npm:3.5.13":
+  version: 3.5.13
+  resolution: "@vue/server-renderer@npm:3.5.13"
+  dependencies:
+    "@vue/compiler-ssr": "npm:3.5.13"
+    "@vue/shared": "npm:3.5.13"
+  peerDependencies:
+    vue: 3.5.13
+  checksum: 10c0/f500bdabc199abf41f1d84defd2a365a47afce1f2223a34c32fada84f6193b39ec2ce50636483409eec81b788b8ef0fa1ff59c63ca0c74764d738c24409eef8f
+  languageName: node
+  linkType: hard
+
+"@vue/shared@npm:3.5.13, @vue/shared@npm:^3.5.13":
+  version: 3.5.13
+  resolution: "@vue/shared@npm:3.5.13"
+  checksum: 10c0/2c940ef907116f1c2583ca1d7733984e5705983ab07054c4e72f1d95eb0f7bdf4d01efbdaee1776c2008f79595963f44e98fced057f5957d86d57b70028f5025
+  languageName: node
+  linkType: hard
+
+"@vueuse/core@npm:12.8.2, @vueuse/core@npm:^12.4.0":
+  version: 12.8.2
+  resolution: "@vueuse/core@npm:12.8.2"
+  dependencies:
+    "@types/web-bluetooth": "npm:^0.0.21"
+    "@vueuse/metadata": "npm:12.8.2"
+    "@vueuse/shared": "npm:12.8.2"
+    vue: "npm:^3.5.13"
+  checksum: 10c0/79c74c7daa471bbf6d51bf83d3750105074d83f6628fcdf41828e9f87977de4a4744968f48ad2d13597e758baf1a0aa5fe4f348dbecf093c9437bd8fd9937b73
+  languageName: node
+  linkType: hard
+
+"@vueuse/integrations@npm:^12.4.0":
+  version: 12.8.2
+  resolution: "@vueuse/integrations@npm:12.8.2"
+  dependencies:
+    "@vueuse/core": "npm:12.8.2"
+    "@vueuse/shared": "npm:12.8.2"
+    vue: "npm:^3.5.13"
+  peerDependencies:
+    async-validator: ^4
+    axios: ^1
+    change-case: ^5
+    drauu: ^0.4
+    focus-trap: ^7
+    fuse.js: ^7
+    idb-keyval: ^6
+    jwt-decode: ^4
+    nprogress: ^0.2
+    qrcode: ^1.5
+    sortablejs: ^1
+    universal-cookie: ^7
+  peerDependenciesMeta:
+    async-validator:
+      optional: true
+    axios:
+      optional: true
+    change-case:
+      optional: true
+    drauu:
+      optional: true
+    focus-trap:
+      optional: true
+    fuse.js:
+      optional: true
+    idb-keyval:
+      optional: true
+    jwt-decode:
+      optional: true
+    nprogress:
+      optional: true
+    qrcode:
+      optional: true
+    sortablejs:
+      optional: true
+    universal-cookie:
+      optional: true
+  checksum: 10c0/f65423fb0cac6e892e0cc99ffea645588568fa5d40c5fc40123eb98fb43ef245c44bb69a172e9d45ff529a0bb429426f2221b02cd11b77e81d97b44584303339
+  languageName: node
+  linkType: hard
+
+"@vueuse/metadata@npm:12.8.2":
+  version: 12.8.2
+  resolution: "@vueuse/metadata@npm:12.8.2"
+  checksum: 10c0/95d352506608d5f8ae9c99ce494e7e55d864707d903f20401fecf378b08c8ed96631d0b8b38f6cd1d04dda69d9c8d3e4fa37e33caae10692805a9566e316e39c
+  languageName: node
+  linkType: hard
+
+"@vueuse/shared@npm:12.8.2":
+  version: 12.8.2
+  resolution: "@vueuse/shared@npm:12.8.2"
+  dependencies:
+    vue: "npm:^3.5.13"
+  checksum: 10c0/43a04cf44e3377ee7666787e597d45ddf83880d3e1835000201b0c471198ce83ad45e4f4a0182d68781c0a23c84a904aab253e4c6279400a235e0246b613bad2
+  languageName: node
+  linkType: hard
+
+"abbrev@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "abbrev@npm:3.0.0"
+  checksum: 10c0/049704186396f571650eb7b22ed3627b77a5aedf98bb83caf2eac81ca2a3e25e795394b0464cfb2d6076df3db6a5312139eac5b6a126ca296ac53c5008069c28
+  languageName: node
+  linkType: hard
+
+"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2":
+  version: 7.1.3
+  resolution: "agent-base@npm:7.1.3"
+  checksum: 10c0/6192b580c5b1d8fb399b9c62bf8343d76654c2dd62afcb9a52b2cf44a8b6ace1e3b704d3fe3547d91555c857d3df02603341ff2cb961b9cfe2b12f9f3c38ee11
+  languageName: node
+  linkType: hard
+
+"algoliasearch@npm:^5.14.2":
+  version: 5.23.2
+  resolution: "algoliasearch@npm:5.23.2"
+  dependencies:
+    "@algolia/client-abtesting": "npm:5.23.2"
+    "@algolia/client-analytics": "npm:5.23.2"
+    "@algolia/client-common": "npm:5.23.2"
+    "@algolia/client-insights": "npm:5.23.2"
+    "@algolia/client-personalization": "npm:5.23.2"
+    "@algolia/client-query-suggestions": "npm:5.23.2"
+    "@algolia/client-search": "npm:5.23.2"
+    "@algolia/ingestion": "npm:1.23.2"
+    "@algolia/monitoring": "npm:1.23.2"
+    "@algolia/recommend": "npm:5.23.2"
+    "@algolia/requester-browser-xhr": "npm:5.23.2"
+    "@algolia/requester-fetch": "npm:5.23.2"
+    "@algolia/requester-node-http": "npm:5.23.2"
+  checksum: 10c0/4d84968c3976f844cccb41f92eb0ca05e4cfcb47281d5b1242f3a6115e92fb1d385dac7aadd1e8a846848c3486a26f2c73a93a89b0eb44deafb34e673f87aaf6
+  languageName: node
+  linkType: hard
+
+"ansi-colors@npm:^4.1.1":
+  version: 4.1.3
+  resolution: "ansi-colors@npm:4.1.3"
+  checksum: 10c0/ec87a2f59902f74e61eada7f6e6fe20094a628dab765cfdbd03c3477599368768cffccdb5d3bb19a1b6c99126783a143b1fee31aab729b31ffe5836c7e5e28b9
+  languageName: node
+  linkType: hard
+
+"ansi-regex@npm:^5.0.1":
+  version: 5.0.1
+  resolution: "ansi-regex@npm:5.0.1"
+  checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737
+  languageName: node
+  linkType: hard
+
+"ansi-regex@npm:^6.0.1":
+  version: 6.1.0
+  resolution: "ansi-regex@npm:6.1.0"
+  checksum: 10c0/a91daeddd54746338478eef88af3439a7edf30f8e23196e2d6ed182da9add559c601266dbef01c2efa46a958ad6f1f8b176799657616c702b5b02e799e7fd8dc
+  languageName: node
+  linkType: hard
+
+"ansi-styles@npm:^4.0.0":
+  version: 4.3.0
+  resolution: "ansi-styles@npm:4.3.0"
+  dependencies:
+    color-convert: "npm:^2.0.1"
+  checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041
+  languageName: node
+  linkType: hard
+
+"ansi-styles@npm:^6.1.0":
+  version: 6.2.1
+  resolution: "ansi-styles@npm:6.2.1"
+  checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c
+  languageName: node
+  linkType: hard
+
+"balanced-match@npm:^1.0.0":
+  version: 1.0.2
+  resolution: "balanced-match@npm:1.0.2"
+  checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee
+  languageName: node
+  linkType: hard
+
+"birpc@npm:^0.2.19":
+  version: 0.2.19
+  resolution: "birpc@npm:0.2.19"
+  checksum: 10c0/be3c6a4044e3041a5d8eb4c4d50b57b46158dc8149ada718ead20544e50b68b72b34c9d8bf0457d23d5f18e5a66d206b8bef5ff22c1018e1e39d373187eed455
+  languageName: node
+  linkType: hard
+
+"boolbase@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "boolbase@npm:1.0.0"
+  checksum: 10c0/e4b53deb4f2b85c52be0e21a273f2045c7b6a6ea002b0e139c744cb6f95e9ec044439a52883b0d74dedd1ff3da55ed140cfdddfed7fb0cccbed373de5dce1bcf
+  languageName: node
+  linkType: hard
+
+"brace-expansion@npm:^2.0.1":
+  version: 2.0.1
+  resolution: "brace-expansion@npm:2.0.1"
+  dependencies:
+    balanced-match: "npm:^1.0.0"
+  checksum: 10c0/b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f
+  languageName: node
+  linkType: hard
+
+"cacache@npm:^19.0.1":
+  version: 19.0.1
+  resolution: "cacache@npm:19.0.1"
+  dependencies:
+    "@npmcli/fs": "npm:^4.0.0"
+    fs-minipass: "npm:^3.0.0"
+    glob: "npm:^10.2.2"
+    lru-cache: "npm:^10.0.1"
+    minipass: "npm:^7.0.3"
+    minipass-collect: "npm:^2.0.1"
+    minipass-flush: "npm:^1.0.5"
+    minipass-pipeline: "npm:^1.2.4"
+    p-map: "npm:^7.0.2"
+    ssri: "npm:^12.0.0"
+    tar: "npm:^7.4.3"
+    unique-filename: "npm:^4.0.0"
+  checksum: 10c0/01f2134e1bd7d3ab68be851df96c8d63b492b1853b67f2eecb2c37bb682d37cb70bb858a16f2f0554d3c0071be6dfe21456a1ff6fa4b7eed996570d6a25ffe9c
+  languageName: node
+  linkType: hard
+
+"ccount@npm:^2.0.0":
+  version: 2.0.1
+  resolution: "ccount@npm:2.0.1"
+  checksum: 10c0/3939b1664390174484322bc3f45b798462e6c07ee6384cb3d645e0aa2f318502d174845198c1561930e1d431087f74cf1fe291ae9a4722821a9f4ba67e574350
+  languageName: node
+  linkType: hard
+
+"character-entities-html4@npm:^2.0.0":
+  version: 2.1.0
+  resolution: "character-entities-html4@npm:2.1.0"
+  checksum: 10c0/fe61b553f083400c20c0b0fd65095df30a0b445d960f3bbf271536ae6c3ba676f39cb7af0b4bf2755812f08ab9b88f2feed68f9aebb73bb153f7a115fe5c6e40
+  languageName: node
+  linkType: hard
+
+"character-entities-legacy@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "character-entities-legacy@npm:3.0.0"
+  checksum: 10c0/ec4b430af873661aa754a896a2b55af089b4e938d3d010fad5219299a6b6d32ab175142699ee250640678cd64bdecd6db3c9af0b8759ab7b155d970d84c4c7d1
+  languageName: node
+  linkType: hard
+
+"cheerio-select@npm:^1.5.0":
+  version: 1.6.0
+  resolution: "cheerio-select@npm:1.6.0"
+  dependencies:
+    css-select: "npm:^4.3.0"
+    css-what: "npm:^6.0.1"
+    domelementtype: "npm:^2.2.0"
+    domhandler: "npm:^4.3.1"
+    domutils: "npm:^2.8.0"
+  checksum: 10c0/4adfdc79e93cba3c9e6162fa82b016ac945035aae2ae5eace0830f44e183f43353d8df42aa8d0aa7efe0d014b6a5ce703d41a887c2a54ec079edeb81bfea889d
+  languageName: node
+  linkType: hard
+
+"cheerio@npm:1.0.0-rc.10":
+  version: 1.0.0-rc.10
+  resolution: "cheerio@npm:1.0.0-rc.10"
+  dependencies:
+    cheerio-select: "npm:^1.5.0"
+    dom-serializer: "npm:^1.3.2"
+    domhandler: "npm:^4.2.0"
+    htmlparser2: "npm:^6.1.0"
+    parse5: "npm:^6.0.1"
+    parse5-htmlparser2-tree-adapter: "npm:^6.0.1"
+    tslib: "npm:^2.2.0"
+  checksum: 10c0/2bb0fae8b1941949f506ddc4df75e3c2d0e5cc6c05478f918dd64a4d2c5282ec84b243890f6a809052a8eb6214641084922c07f726b5287b5dba114b10e52cb9
+  languageName: node
+  linkType: hard
+
+"chownr@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "chownr@npm:3.0.0"
+  checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10
+  languageName: node
+  linkType: hard
+
+"color-convert@npm:^2.0.1":
+  version: 2.0.1
+  resolution: "color-convert@npm:2.0.1"
+  dependencies:
+    color-name: "npm:~1.1.4"
+  checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7
+  languageName: node
+  linkType: hard
+
+"color-name@npm:~1.1.4":
+  version: 1.1.4
+  resolution: "color-name@npm:1.1.4"
+  checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95
+  languageName: node
+  linkType: hard
+
+"comma-separated-tokens@npm:^2.0.0":
+  version: 2.0.3
+  resolution: "comma-separated-tokens@npm:2.0.3"
+  checksum: 10c0/91f90f1aae320f1755d6957ef0b864fe4f54737f3313bd95e0802686ee2ca38bff1dd381964d00ae5db42912dd1f4ae5c2709644e82706ffc6f6842a813cdd67
+  languageName: node
+  linkType: hard
+
+"commander@npm:9.2.0":
+  version: 9.2.0
+  resolution: "commander@npm:9.2.0"
+  checksum: 10c0/42eb2cf427fc5a1ca2ddf7ff6467d1a3cbec5c3e68dd61ead42eb01489ff068beadd4dc8303ab3cc48470d808372063c87fc8d9ed9e1be86365c3b0fc0a92732
+  languageName: node
+  linkType: hard
+
+"commander@npm:^6.1.0":
+  version: 6.2.1
+  resolution: "commander@npm:6.2.1"
+  checksum: 10c0/85748abd9d18c8bc88febed58b98f66b7c591d9b5017cad459565761d7b29ca13b7783ea2ee5ce84bf235897333706c4ce29adf1ce15c8252780e7000e2ce9ea
+  languageName: node
+  linkType: hard
+
+"copy-anything@npm:^3.0.2":
+  version: 3.0.5
+  resolution: "copy-anything@npm:3.0.5"
+  dependencies:
+    is-what: "npm:^4.1.8"
+  checksum: 10c0/01eadd500c7e1db71d32d95a3bfaaedcb839ef891c741f6305ab0461398056133de08f2d1bf4c392b364e7bdb7ce498513896e137a7a183ac2516b065c28a4fe
+  languageName: node
+  linkType: hard
+
+"cross-spawn@npm:^7.0.6":
+  version: 7.0.6
+  resolution: "cross-spawn@npm:7.0.6"
+  dependencies:
+    path-key: "npm:^3.1.0"
+    shebang-command: "npm:^2.0.0"
+    which: "npm:^2.0.1"
+  checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1
+  languageName: node
+  linkType: hard
+
+"css-select@npm:^4.3.0":
+  version: 4.3.0
+  resolution: "css-select@npm:4.3.0"
+  dependencies:
+    boolbase: "npm:^1.0.0"
+    css-what: "npm:^6.0.1"
+    domhandler: "npm:^4.3.1"
+    domutils: "npm:^2.8.0"
+    nth-check: "npm:^2.0.1"
+  checksum: 10c0/a489d8e5628e61063d5a8fe0fa1cc7ae2478cb334a388a354e91cf2908154be97eac9fa7ed4dffe87a3e06cf6fcaa6016553115335c4fd3377e13dac7bd5a8e1
+  languageName: node
+  linkType: hard
+
+"css-what@npm:^6.0.1":
+  version: 6.1.0
+  resolution: "css-what@npm:6.1.0"
+  checksum: 10c0/a09f5a6b14ba8dcf57ae9a59474722e80f20406c53a61e9aedb0eedc693b135113ffe2983f4efc4b5065ae639442e9ae88df24941ef159c218b231011d733746
+  languageName: node
+  linkType: hard
+
+"csstype@npm:^3.1.3":
+  version: 3.1.3
+  resolution: "csstype@npm:3.1.3"
+  checksum: 10c0/80c089d6f7e0c5b2bd83cf0539ab41474198579584fa10d86d0cafe0642202343cbc119e076a0b1aece191989477081415d66c9fefbf3c957fc2fc4b7009f248
+  languageName: node
+  linkType: hard
+
+"debug@npm:4, debug@npm:^4.3.4":
+  version: 4.4.0
+  resolution: "debug@npm:4.4.0"
+  dependencies:
+    ms: "npm:^2.1.3"
+  peerDependenciesMeta:
+    supports-color:
+      optional: true
+  checksum: 10c0/db94f1a182bf886f57b4755f85b3a74c39b5114b9377b7ab375dc2cfa3454f09490cc6c30f829df3fc8042bc8b8995f6567ce5cd96f3bc3688bd24027197d9de
+  languageName: node
+  linkType: hard
+
+"dequal@npm:^2.0.0":
+  version: 2.0.3
+  resolution: "dequal@npm:2.0.3"
+  checksum: 10c0/f98860cdf58b64991ae10205137c0e97d384c3a4edc7f807603887b7c4b850af1224a33d88012009f150861cbee4fa2d322c4cc04b9313bee312e47f6ecaa888
+  languageName: node
+  linkType: hard
+
+"devlop@npm:^1.0.0":
+  version: 1.1.0
+  resolution: "devlop@npm:1.1.0"
+  dependencies:
+    dequal: "npm:^2.0.0"
+  checksum: 10c0/e0928ab8f94c59417a2b8389c45c55ce0a02d9ac7fd74ef62d01ba48060129e1d594501b77de01f3eeafc7cb00773819b0df74d96251cf20b31c5b3071f45c0e
+  languageName: node
+  linkType: hard
+
+"docs@workspace:.":
+  version: 0.0.0-use.local
+  resolution: "docs@workspace:."
+  dependencies:
+    markdown-it-mathjax3: "npm:^4.3.2"
+    vitepress: "npm:^1.6.3"
+    vue: "npm:^3.5.13"
+  languageName: unknown
+  linkType: soft
+
+"dom-serializer@npm:^1.0.1, dom-serializer@npm:^1.3.2":
+  version: 1.4.1
+  resolution: "dom-serializer@npm:1.4.1"
+  dependencies:
+    domelementtype: "npm:^2.0.1"
+    domhandler: "npm:^4.2.0"
+    entities: "npm:^2.0.0"
+  checksum: 10c0/67d775fa1ea3de52035c98168ddcd59418356943b5eccb80e3c8b3da53adb8e37edb2cc2f885802b7b1765bf5022aec21dfc32910d7f9e6de4c3148f095ab5e0
+  languageName: node
+  linkType: hard
+
+"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0":
+  version: 2.3.0
+  resolution: "domelementtype@npm:2.3.0"
+  checksum: 10c0/686f5a9ef0fff078c1412c05db73a0dce096190036f33e400a07e2a4518e9f56b1e324f5c576a0a747ef0e75b5d985c040b0d51945ce780c0dd3c625a18cd8c9
+  languageName: node
+  linkType: hard
+
+"domhandler@npm:^3.3.0":
+  version: 3.3.0
+  resolution: "domhandler@npm:3.3.0"
+  dependencies:
+    domelementtype: "npm:^2.0.1"
+  checksum: 10c0/376e6462a6144121f6ae50c9c1b8e0b22d2e0c68f9fb2ef6e57a5f4f9395854b1258cb638c58b171ee291359a5f41a4a57f403954db976484a59ffcee4c1e405
+  languageName: node
+  linkType: hard
+
+"domhandler@npm:^4.0.0, domhandler@npm:^4.2.0, domhandler@npm:^4.3.1":
+  version: 4.3.1
+  resolution: "domhandler@npm:4.3.1"
+  dependencies:
+    domelementtype: "npm:^2.2.0"
+  checksum: 10c0/5c199c7468cb052a8b5ab80b13528f0db3d794c64fc050ba793b574e158e67c93f8336e87fd81e9d5ee43b0e04aea4d8b93ed7be4899cb726a1601b3ba18538b
+  languageName: node
+  linkType: hard
+
+"domutils@npm:^2.4.2, domutils@npm:^2.5.2, domutils@npm:^2.8.0":
+  version: 2.8.0
+  resolution: "domutils@npm:2.8.0"
+  dependencies:
+    dom-serializer: "npm:^1.0.1"
+    domelementtype: "npm:^2.2.0"
+    domhandler: "npm:^4.2.0"
+  checksum: 10c0/d58e2ae01922f0dd55894e61d18119924d88091837887bf1438f2327f32c65eb76426bd9384f81e7d6dcfb048e0f83c19b222ad7101176ad68cdc9c695b563db
+  languageName: node
+  linkType: hard
+
+"eastasianwidth@npm:^0.2.0":
+  version: 0.2.0
+  resolution: "eastasianwidth@npm:0.2.0"
+  checksum: 10c0/26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39
+  languageName: node
+  linkType: hard
+
+"emoji-regex-xs@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "emoji-regex-xs@npm:1.0.0"
+  checksum: 10c0/1082de006991eb05a3324ef0efe1950c7cdf66efc01d4578de82b0d0d62add4e55e97695a8a7eeda826c305081562dc79b477ddf18d886da77f3ba08c4b940a0
+  languageName: node
+  linkType: hard
+
+"emoji-regex@npm:^8.0.0":
+  version: 8.0.0
+  resolution: "emoji-regex@npm:8.0.0"
+  checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010
+  languageName: node
+  linkType: hard
+
+"emoji-regex@npm:^9.2.2":
+  version: 9.2.2
+  resolution: "emoji-regex@npm:9.2.2"
+  checksum: 10c0/af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639
+  languageName: node
+  linkType: hard
+
+"encoding@npm:^0.1.13":
+  version: 0.1.13
+  resolution: "encoding@npm:0.1.13"
+  dependencies:
+    iconv-lite: "npm:^0.6.2"
+  checksum: 10c0/36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039
+  languageName: node
+  linkType: hard
+
+"entities@npm:^2.0.0":
+  version: 2.2.0
+  resolution: "entities@npm:2.2.0"
+  checksum: 10c0/7fba6af1f116300d2ba1c5673fc218af1961b20908638391b4e1e6d5850314ee2ac3ec22d741b3a8060479911c99305164aed19b6254bde75e7e6b1b2c3f3aa3
+  languageName: node
+  linkType: hard
+
+"entities@npm:^4.5.0":
+  version: 4.5.0
+  resolution: "entities@npm:4.5.0"
+  checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250
+  languageName: node
+  linkType: hard
+
+"env-paths@npm:^2.2.0":
+  version: 2.2.1
+  resolution: "env-paths@npm:2.2.1"
+  checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4
+  languageName: node
+  linkType: hard
+
+"err-code@npm:^2.0.2":
+  version: 2.0.3
+  resolution: "err-code@npm:2.0.3"
+  checksum: 10c0/b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66
+  languageName: node
+  linkType: hard
+
+"esbuild@npm:^0.21.3":
+  version: 0.21.5
+  resolution: "esbuild@npm:0.21.5"
+  dependencies:
+    "@esbuild/aix-ppc64": "npm:0.21.5"
+    "@esbuild/android-arm": "npm:0.21.5"
+    "@esbuild/android-arm64": "npm:0.21.5"
+    "@esbuild/android-x64": "npm:0.21.5"
+    "@esbuild/darwin-arm64": "npm:0.21.5"
+    "@esbuild/darwin-x64": "npm:0.21.5"
+    "@esbuild/freebsd-arm64": "npm:0.21.5"
+    "@esbuild/freebsd-x64": "npm:0.21.5"
+    "@esbuild/linux-arm": "npm:0.21.5"
+    "@esbuild/linux-arm64": "npm:0.21.5"
+    "@esbuild/linux-ia32": "npm:0.21.5"
+    "@esbuild/linux-loong64": "npm:0.21.5"
+    "@esbuild/linux-mips64el": "npm:0.21.5"
+    "@esbuild/linux-ppc64": "npm:0.21.5"
+    "@esbuild/linux-riscv64": "npm:0.21.5"
+    "@esbuild/linux-s390x": "npm:0.21.5"
+    "@esbuild/linux-x64": "npm:0.21.5"
+    "@esbuild/netbsd-x64": "npm:0.21.5"
+    "@esbuild/openbsd-x64": "npm:0.21.5"
+    "@esbuild/sunos-x64": "npm:0.21.5"
+    "@esbuild/win32-arm64": "npm:0.21.5"
+    "@esbuild/win32-ia32": "npm:0.21.5"
+    "@esbuild/win32-x64": "npm:0.21.5"
+  dependenciesMeta:
+    "@esbuild/aix-ppc64":
+      optional: true
+    "@esbuild/android-arm":
+      optional: true
+    "@esbuild/android-arm64":
+      optional: true
+    "@esbuild/android-x64":
+      optional: true
+    "@esbuild/darwin-arm64":
+      optional: true
+    "@esbuild/darwin-x64":
+      optional: true
+    "@esbuild/freebsd-arm64":
+      optional: true
+    "@esbuild/freebsd-x64":
+      optional: true
+    "@esbuild/linux-arm":
+      optional: true
+    "@esbuild/linux-arm64":
+      optional: true
+    "@esbuild/linux-ia32":
+      optional: true
+    "@esbuild/linux-loong64":
+      optional: true
+    "@esbuild/linux-mips64el":
+      optional: true
+    "@esbuild/linux-ppc64":
+      optional: true
+    "@esbuild/linux-riscv64":
+      optional: true
+    "@esbuild/linux-s390x":
+      optional: true
+    "@esbuild/linux-x64":
+      optional: true
+    "@esbuild/netbsd-x64":
+      optional: true
+    "@esbuild/openbsd-x64":
+      optional: true
+    "@esbuild/sunos-x64":
+      optional: true
+    "@esbuild/win32-arm64":
+      optional: true
+    "@esbuild/win32-ia32":
+      optional: true
+    "@esbuild/win32-x64":
+      optional: true
+  bin:
+    esbuild: bin/esbuild
+  checksum: 10c0/fa08508adf683c3f399e8a014a6382a6b65542213431e26206c0720e536b31c09b50798747c2a105a4bbba1d9767b8d3615a74c2f7bf1ddf6d836cd11eb672de
+  languageName: node
+  linkType: hard
+
+"escape-goat@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "escape-goat@npm:3.0.0"
+  checksum: 10c0/a2b470bbdb95ccbcd19390576993a2b75735457b1979275f4f0d6da86d2e932a2a12edd9270208e3090299a26df857da1f80555c37bb1bac6fa9135322253ca4
+  languageName: node
+  linkType: hard
+
+"esm@npm:^3.2.25":
+  version: 3.2.25
+  resolution: "esm@npm:3.2.25"
+  checksum: 10c0/8e60e8075506a7ce28681c30c8f54623fe18a251c364cd481d86719fc77f58aa055b293d80632d9686d5408aaf865ffa434897dc9fd9153c8b3f469fad23f094
+  languageName: node
+  linkType: hard
+
+"estree-walker@npm:^2.0.2":
+  version: 2.0.2
+  resolution: "estree-walker@npm:2.0.2"
+  checksum: 10c0/53a6c54e2019b8c914dc395890153ffdc2322781acf4bd7d1a32d7aedc1710807bdcd866ac133903d5629ec601fbb50abe8c2e5553c7f5a0afdd9b6af6c945af
+  languageName: node
+  linkType: hard
+
+"exponential-backoff@npm:^3.1.1":
+  version: 3.1.2
+  resolution: "exponential-backoff@npm:3.1.2"
+  checksum: 10c0/d9d3e1eafa21b78464297df91f1776f7fbaa3d5e3f7f0995648ca5b89c069d17055033817348d9f4a43d1c20b0eab84f75af6991751e839df53e4dfd6f22e844
+  languageName: node
+  linkType: hard
+
+"fdir@npm:^6.4.3":
+  version: 6.4.3
+  resolution: "fdir@npm:6.4.3"
+  peerDependencies:
+    picomatch: ^3 || ^4
+  peerDependenciesMeta:
+    picomatch:
+      optional: true
+  checksum: 10c0/d13c10120e9625adf21d8d80481586200759928c19405a816b77dd28eaeb80e7c59c5def3e2941508045eb06d34eb47fad865ccc8bf98e6ab988bb0ed160fb6f
+  languageName: node
+  linkType: hard
+
+"focus-trap@npm:^7.6.4":
+  version: 7.6.4
+  resolution: "focus-trap@npm:7.6.4"
+  dependencies:
+    tabbable: "npm:^6.2.0"
+  checksum: 10c0/ed810d47fd904a5e0269e822d98e634c6cbdd7222046c712ef299bdd26a422db754e3cec04e6517065b12be4b47f65c21f6244e0c07a308b1060985463d518cb
+  languageName: node
+  linkType: hard
+
+"foreground-child@npm:^3.1.0":
+  version: 3.3.1
+  resolution: "foreground-child@npm:3.3.1"
+  dependencies:
+    cross-spawn: "npm:^7.0.6"
+    signal-exit: "npm:^4.0.1"
+  checksum: 10c0/8986e4af2430896e65bc2788d6679067294d6aee9545daefc84923a0a4b399ad9c7a3ea7bd8c0b2b80fdf4a92de4c69df3f628233ff3224260e9c1541a9e9ed3
+  languageName: node
+  linkType: hard
+
+"fs-minipass@npm:^3.0.0":
+  version: 3.0.3
+  resolution: "fs-minipass@npm:3.0.3"
+  dependencies:
+    minipass: "npm:^7.0.3"
+  checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94
+  languageName: node
+  linkType: hard
+
+"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
+  version: 2.3.3
+  resolution: "fsevents@npm:2.3.3"
+  dependencies:
+    node-gyp: "npm:latest"
+  checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60
+  conditions: os=darwin
+  languageName: node
+  linkType: hard
+
+"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
+  version: 2.3.3
+  resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
+  dependencies:
+    node-gyp: "npm:latest"
+  conditions: os=darwin
+  languageName: node
+  linkType: hard
+
+"glob@npm:^10.2.2":
+  version: 10.4.5
+  resolution: "glob@npm:10.4.5"
+  dependencies:
+    foreground-child: "npm:^3.1.0"
+    jackspeak: "npm:^3.1.2"
+    minimatch: "npm:^9.0.4"
+    minipass: "npm:^7.1.2"
+    package-json-from-dist: "npm:^1.0.0"
+    path-scurry: "npm:^1.11.1"
+  bin:
+    glob: dist/esm/bin.mjs
+  checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e
+  languageName: node
+  linkType: hard
+
+"graceful-fs@npm:^4.2.6":
+  version: 4.2.11
+  resolution: "graceful-fs@npm:4.2.11"
+  checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2
+  languageName: node
+  linkType: hard
+
+"hast-util-to-html@npm:^9.0.4":
+  version: 9.0.5
+  resolution: "hast-util-to-html@npm:9.0.5"
+  dependencies:
+    "@types/hast": "npm:^3.0.0"
+    "@types/unist": "npm:^3.0.0"
+    ccount: "npm:^2.0.0"
+    comma-separated-tokens: "npm:^2.0.0"
+    hast-util-whitespace: "npm:^3.0.0"
+    html-void-elements: "npm:^3.0.0"
+    mdast-util-to-hast: "npm:^13.0.0"
+    property-information: "npm:^7.0.0"
+    space-separated-tokens: "npm:^2.0.0"
+    stringify-entities: "npm:^4.0.0"
+    zwitch: "npm:^2.0.4"
+  checksum: 10c0/b7a08c30bab4371fc9b4a620965c40b270e5ae7a8e94cf885f43b21705179e28c8e43b39c72885d1647965fb3738654e6962eb8b58b0c2a84271655b4d748836
+  languageName: node
+  linkType: hard
+
+"hast-util-whitespace@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "hast-util-whitespace@npm:3.0.0"
+  dependencies:
+    "@types/hast": "npm:^3.0.0"
+  checksum: 10c0/b898bc9fe27884b272580d15260b6bbdabe239973a147e97fa98c45fa0ffec967a481aaa42291ec34fb56530dc2d484d473d7e2bae79f39c83f3762307edfea8
+  languageName: node
+  linkType: hard
+
+"hookable@npm:^5.5.3":
+  version: 5.5.3
+  resolution: "hookable@npm:5.5.3"
+  checksum: 10c0/275f4cc84d27f8d48c5a5cd5685b6c0fea9291be9deea5bff0cfa72856ed566abde1dcd8cb1da0f9a70b4da3d7ec0d60dc3554c4edbba647058cc38816eced3d
+  languageName: node
+  linkType: hard
+
+"html-void-elements@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "html-void-elements@npm:3.0.0"
+  checksum: 10c0/a8b9ec5db23b7c8053876dad73a0336183e6162bf6d2677376d8b38d654fdc59ba74fdd12f8812688f7db6fad451210c91b300e472afc0909224e0a44c8610d2
+  languageName: node
+  linkType: hard
+
+"htmlparser2@npm:^5.0.0":
+  version: 5.0.1
+  resolution: "htmlparser2@npm:5.0.1"
+  dependencies:
+    domelementtype: "npm:^2.0.1"
+    domhandler: "npm:^3.3.0"
+    domutils: "npm:^2.4.2"
+    entities: "npm:^2.0.0"
+  checksum: 10c0/3f276f7ac518930f5330cfe5129dd5764a63e9bae6f57350e90b26affc94b11b2fb6750f056fed245b726d500e78197b4a09c7108c71964fe91303e6e2a29107
+  languageName: node
+  linkType: hard
+
+"htmlparser2@npm:^6.1.0":
+  version: 6.1.0
+  resolution: "htmlparser2@npm:6.1.0"
+  dependencies:
+    domelementtype: "npm:^2.0.1"
+    domhandler: "npm:^4.0.0"
+    domutils: "npm:^2.5.2"
+    entities: "npm:^2.0.0"
+  checksum: 10c0/3058499c95634f04dc66be8c2e0927cd86799413b2d6989d8ae542ca4dbf5fa948695d02c27d573acf44843af977aec6d9a7bdd0f6faa6b2d99e2a729b2a31b6
+  languageName: node
+  linkType: hard
+
+"http-cache-semantics@npm:^4.1.1":
+  version: 4.1.1
+  resolution: "http-cache-semantics@npm:4.1.1"
+  checksum: 10c0/ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc
+  languageName: node
+  linkType: hard
+
+"http-proxy-agent@npm:^7.0.0":
+  version: 7.0.2
+  resolution: "http-proxy-agent@npm:7.0.2"
+  dependencies:
+    agent-base: "npm:^7.1.0"
+    debug: "npm:^4.3.4"
+  checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921
+  languageName: node
+  linkType: hard
+
+"https-proxy-agent@npm:^7.0.1":
+  version: 7.0.6
+  resolution: "https-proxy-agent@npm:7.0.6"
+  dependencies:
+    agent-base: "npm:^7.1.2"
+    debug: "npm:4"
+  checksum: 10c0/f729219bc735edb621fa30e6e84e60ee5d00802b8247aac0d7b79b0bd6d4b3294737a337b93b86a0bd9e68099d031858a39260c976dc14cdbba238ba1f8779ac
+  languageName: node
+  linkType: hard
+
+"iconv-lite@npm:^0.6.2":
+  version: 0.6.3
+  resolution: "iconv-lite@npm:0.6.3"
+  dependencies:
+    safer-buffer: "npm:>= 2.1.2 < 3.0.0"
+  checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1
+  languageName: node
+  linkType: hard
+
+"imurmurhash@npm:^0.1.4":
+  version: 0.1.4
+  resolution: "imurmurhash@npm:0.1.4"
+  checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6
+  languageName: node
+  linkType: hard
+
+"ip-address@npm:^9.0.5":
+  version: 9.0.5
+  resolution: "ip-address@npm:9.0.5"
+  dependencies:
+    jsbn: "npm:1.1.0"
+    sprintf-js: "npm:^1.1.3"
+  checksum: 10c0/331cd07fafcb3b24100613e4b53e1a2b4feab11e671e655d46dc09ee233da5011284d09ca40c4ecbdfe1d0004f462958675c224a804259f2f78d2465a87824bc
+  languageName: node
+  linkType: hard
+
+"is-fullwidth-code-point@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "is-fullwidth-code-point@npm:3.0.0"
+  checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc
+  languageName: node
+  linkType: hard
+
+"is-what@npm:^4.1.8":
+  version: 4.1.16
+  resolution: "is-what@npm:4.1.16"
+  checksum: 10c0/611f1947776826dcf85b57cfb7bd3b3ea6f4b94a9c2f551d4a53f653cf0cb9d1e6518846648256d46ee6c91d114b6d09d2ac8a07306f7430c5900f87466aae5b
+  languageName: node
+  linkType: hard
+
+"isexe@npm:^2.0.0":
+  version: 2.0.0
+  resolution: "isexe@npm:2.0.0"
+  checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d
+  languageName: node
+  linkType: hard
+
+"isexe@npm:^3.1.1":
+  version: 3.1.1
+  resolution: "isexe@npm:3.1.1"
+  checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7
+  languageName: node
+  linkType: hard
+
+"jackspeak@npm:^3.1.2":
+  version: 3.4.3
+  resolution: "jackspeak@npm:3.4.3"
+  dependencies:
+    "@isaacs/cliui": "npm:^8.0.2"
+    "@pkgjs/parseargs": "npm:^0.11.0"
+  dependenciesMeta:
+    "@pkgjs/parseargs":
+      optional: true
+  checksum: 10c0/6acc10d139eaefdbe04d2f679e6191b3abf073f111edf10b1de5302c97ec93fffeb2fdd8681ed17f16268aa9dd4f8c588ed9d1d3bffbbfa6e8bf897cbb3149b9
+  languageName: node
+  linkType: hard
+
+"jsbn@npm:1.1.0":
+  version: 1.1.0
+  resolution: "jsbn@npm:1.1.0"
+  checksum: 10c0/4f907fb78d7b712e11dea8c165fe0921f81a657d3443dde75359ed52eb2b5d33ce6773d97985a089f09a65edd80b11cb75c767b57ba47391fee4c969f7215c96
+  languageName: node
+  linkType: hard
+
+"juice@npm:^8.0.0":
+  version: 8.1.0
+  resolution: "juice@npm:8.1.0"
+  dependencies:
+    cheerio: "npm:1.0.0-rc.10"
+    commander: "npm:^6.1.0"
+    mensch: "npm:^0.3.4"
+    slick: "npm:^1.12.2"
+    web-resource-inliner: "npm:^6.0.1"
+  bin:
+    juice: bin/juice
+  checksum: 10c0/c67d328d5865ad32e938708bf38608bd0c856d9c9e226e43f293b049a3ae37340323c6e746288166a108e5ee7c0d00c2480d769c455094d9a7f0205534e44246
+  languageName: node
+  linkType: hard
+
+"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
+  version: 10.4.3
+  resolution: "lru-cache@npm:10.4.3"
+  checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb
+  languageName: node
+  linkType: hard
+
+"magic-string@npm:^0.30.11":
+  version: 0.30.17
+  resolution: "magic-string@npm:0.30.17"
+  dependencies:
+    "@jridgewell/sourcemap-codec": "npm:^1.5.0"
+  checksum: 10c0/16826e415d04b88378f200fe022b53e638e3838b9e496edda6c0e086d7753a44a6ed187adc72d19f3623810589bf139af1a315541cd6a26ae0771a0193eaf7b8
+  languageName: node
+  linkType: hard
+
+"make-fetch-happen@npm:^14.0.3":
+  version: 14.0.3
+  resolution: "make-fetch-happen@npm:14.0.3"
+  dependencies:
+    "@npmcli/agent": "npm:^3.0.0"
+    cacache: "npm:^19.0.1"
+    http-cache-semantics: "npm:^4.1.1"
+    minipass: "npm:^7.0.2"
+    minipass-fetch: "npm:^4.0.0"
+    minipass-flush: "npm:^1.0.5"
+    minipass-pipeline: "npm:^1.2.4"
+    negotiator: "npm:^1.0.0"
+    proc-log: "npm:^5.0.0"
+    promise-retry: "npm:^2.0.1"
+    ssri: "npm:^12.0.0"
+  checksum: 10c0/c40efb5e5296e7feb8e37155bde8eb70bc57d731b1f7d90e35a092fde403d7697c56fb49334d92d330d6f1ca29a98142036d6480a12681133a0a1453164cb2f0
+  languageName: node
+  linkType: hard
+
+"mark.js@npm:8.11.1":
+  version: 8.11.1
+  resolution: "mark.js@npm:8.11.1"
+  checksum: 10c0/5e69e776db61abdd857b5cbb7070c8a3b1b0e5c12bf077fcd5a8c6f17b1f85ed65275aba5662b57136d1b9f82b54bb34d4ef4220f7703c9a7ab806ae1e208cff
+  languageName: node
+  linkType: hard
+
+"markdown-it-mathjax3@npm:^4.3.2":
+  version: 4.3.2
+  resolution: "markdown-it-mathjax3@npm:4.3.2"
+  dependencies:
+    juice: "npm:^8.0.0"
+    mathjax-full: "npm:^3.2.0"
+  checksum: 10c0/aea708256bd465c513793186a119decf7c0768d29c0ff6b146d0849ea6ecfec838cbbd5bd3c9ad80fba3efc43bcf8a181a509b0f44d637e531bb5d7f3c7c9df1
+  languageName: node
+  linkType: hard
+
+"mathjax-full@npm:^3.2.0":
+  version: 3.2.2
+  resolution: "mathjax-full@npm:3.2.2"
+  dependencies:
+    esm: "npm:^3.2.25"
+    mhchemparser: "npm:^4.1.0"
+    mj-context-menu: "npm:^0.6.1"
+    speech-rule-engine: "npm:^4.0.6"
+  checksum: 10c0/48f55747bd91aa6bba905bab8654045e4d654a0a65869b1d446e25a89284ef37dc722c6c4a585e7fc14c5f87bb6954291f066e08e5a8245bb5ff0c4f4c54c8d4
+  languageName: node
+  linkType: hard
+
+"mdast-util-to-hast@npm:^13.0.0":
+  version: 13.2.0
+  resolution: "mdast-util-to-hast@npm:13.2.0"
+  dependencies:
+    "@types/hast": "npm:^3.0.0"
+    "@types/mdast": "npm:^4.0.0"
+    "@ungap/structured-clone": "npm:^1.0.0"
+    devlop: "npm:^1.0.0"
+    micromark-util-sanitize-uri: "npm:^2.0.0"
+    trim-lines: "npm:^3.0.0"
+    unist-util-position: "npm:^5.0.0"
+    unist-util-visit: "npm:^5.0.0"
+    vfile: "npm:^6.0.0"
+  checksum: 10c0/9ee58def9287df8350cbb6f83ced90f9c088d72d4153780ad37854f87144cadc6f27b20347073b285173b1649b0723ddf0b9c78158608a804dcacb6bda6e1816
+  languageName: node
+  linkType: hard
+
+"mensch@npm:^0.3.4":
+  version: 0.3.4
+  resolution: "mensch@npm:0.3.4"
+  checksum: 10c0/177f9c1cb1acd93da98a971288a5da99f819ac06de19ca450040b18ddf8728c7ae0ce22309fadbbfd4ceb773bc5c03bf1cb93ceb91441da9e76e010d314da2ea
+  languageName: node
+  linkType: hard
+
+"mhchemparser@npm:^4.1.0":
+  version: 4.2.1
+  resolution: "mhchemparser@npm:4.2.1"
+  checksum: 10c0/5afe8b5ed918fda71a17875227050fd7e8e706305df28bd0beb5a7cd07886a393d041317e3c4b9105fc518ab75065b65886b2574cbd8dec49520de04d119e64a
+  languageName: node
+  linkType: hard
+
+"micromark-util-character@npm:^2.0.0":
+  version: 2.1.1
+  resolution: "micromark-util-character@npm:2.1.1"
+  dependencies:
+    micromark-util-symbol: "npm:^2.0.0"
+    micromark-util-types: "npm:^2.0.0"
+  checksum: 10c0/d3fe7a5e2c4060fc2a076f9ce699c82a2e87190a3946e1e5eea77f563869b504961f5668d9c9c014724db28ac32fa909070ea8b30c3a39bd0483cc6c04cc76a1
+  languageName: node
+  linkType: hard
+
+"micromark-util-encode@npm:^2.0.0":
+  version: 2.0.1
+  resolution: "micromark-util-encode@npm:2.0.1"
+  checksum: 10c0/b2b29f901093845da8a1bf997ea8b7f5e061ffdba85070dfe14b0197c48fda64ffcf82bfe53c90cf9dc185e69eef8c5d41cae3ba918b96bc279326921b59008a
+  languageName: node
+  linkType: hard
+
+"micromark-util-sanitize-uri@npm:^2.0.0":
+  version: 2.0.1
+  resolution: "micromark-util-sanitize-uri@npm:2.0.1"
+  dependencies:
+    micromark-util-character: "npm:^2.0.0"
+    micromark-util-encode: "npm:^2.0.0"
+    micromark-util-symbol: "npm:^2.0.0"
+  checksum: 10c0/60e92166e1870fd4f1961468c2651013ff760617342918e0e0c3c4e872433aa2e60c1e5a672bfe5d89dc98f742d6b33897585cf86ae002cda23e905a3c02527c
+  languageName: node
+  linkType: hard
+
+"micromark-util-symbol@npm:^2.0.0":
+  version: 2.0.1
+  resolution: "micromark-util-symbol@npm:2.0.1"
+  checksum: 10c0/f2d1b207771e573232436618e78c5e46cd4b5c560dd4a6d63863d58018abbf49cb96ec69f7007471e51434c60de3c9268ef2bf46852f26ff4aacd10f9da16fe9
+  languageName: node
+  linkType: hard
+
+"micromark-util-types@npm:^2.0.0":
+  version: 2.0.2
+  resolution: "micromark-util-types@npm:2.0.2"
+  checksum: 10c0/c8c15b96c858db781c4393f55feec10004bf7df95487636c9a9f7209e51002a5cca6a047c5d2a5dc669ff92da20e57aaa881e81a268d9ccadb647f9dce305298
+  languageName: node
+  linkType: hard
+
+"mime@npm:^2.4.6":
+  version: 2.6.0
+  resolution: "mime@npm:2.6.0"
+  bin:
+    mime: cli.js
+  checksum: 10c0/a7f2589900d9c16e3bdf7672d16a6274df903da958c1643c9c45771f0478f3846dcb1097f31eb9178452570271361e2149310931ec705c037210fc69639c8e6c
+  languageName: node
+  linkType: hard
+
+"minimatch@npm:^9.0.4":
+  version: 9.0.5
+  resolution: "minimatch@npm:9.0.5"
+  dependencies:
+    brace-expansion: "npm:^2.0.1"
+  checksum: 10c0/de96cf5e35bdf0eab3e2c853522f98ffbe9a36c37797778d2665231ec1f20a9447a7e567cb640901f89e4daaa95ae5d70c65a9e8aa2bb0019b6facbc3c0575ed
+  languageName: node
+  linkType: hard
+
+"minipass-collect@npm:^2.0.1":
+  version: 2.0.1
+  resolution: "minipass-collect@npm:2.0.1"
+  dependencies:
+    minipass: "npm:^7.0.3"
+  checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e
+  languageName: node
+  linkType: hard
+
+"minipass-fetch@npm:^4.0.0":
+  version: 4.0.1
+  resolution: "minipass-fetch@npm:4.0.1"
+  dependencies:
+    encoding: "npm:^0.1.13"
+    minipass: "npm:^7.0.3"
+    minipass-sized: "npm:^1.0.3"
+    minizlib: "npm:^3.0.1"
+  dependenciesMeta:
+    encoding:
+      optional: true
+  checksum: 10c0/a3147b2efe8e078c9bf9d024a0059339c5a09c5b1dded6900a219c218cc8b1b78510b62dae556b507304af226b18c3f1aeb1d48660283602d5b6586c399eed5c
+  languageName: node
+  linkType: hard
+
+"minipass-flush@npm:^1.0.5":
+  version: 1.0.5
+  resolution: "minipass-flush@npm:1.0.5"
+  dependencies:
+    minipass: "npm:^3.0.0"
+  checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd
+  languageName: node
+  linkType: hard
+
+"minipass-pipeline@npm:^1.2.4":
+  version: 1.2.4
+  resolution: "minipass-pipeline@npm:1.2.4"
+  dependencies:
+    minipass: "npm:^3.0.0"
+  checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2
+  languageName: node
+  linkType: hard
+
+"minipass-sized@npm:^1.0.3":
+  version: 1.0.3
+  resolution: "minipass-sized@npm:1.0.3"
+  dependencies:
+    minipass: "npm:^3.0.0"
+  checksum: 10c0/298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb
+  languageName: node
+  linkType: hard
+
+"minipass@npm:^3.0.0":
+  version: 3.3.6
+  resolution: "minipass@npm:3.3.6"
+  dependencies:
+    yallist: "npm:^4.0.0"
+  checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c
+  languageName: node
+  linkType: hard
+
+"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2":
+  version: 7.1.2
+  resolution: "minipass@npm:7.1.2"
+  checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557
+  languageName: node
+  linkType: hard
+
+"minisearch@npm:^7.1.1":
+  version: 7.1.2
+  resolution: "minisearch@npm:7.1.2"
+  checksum: 10c0/039f494927a4a932c168654ba8ff434d2be6af8e3393b29577d54da64cd64d6ae9bfbf33b7cd7bdc1783cb354d67c5dc99bac1305599d8d653933676f2d1a355
+  languageName: node
+  linkType: hard
+
+"minizlib@npm:^3.0.1":
+  version: 3.0.2
+  resolution: "minizlib@npm:3.0.2"
+  dependencies:
+    minipass: "npm:^7.1.2"
+  checksum: 10c0/9f3bd35e41d40d02469cb30470c55ccc21cae0db40e08d1d0b1dff01cc8cc89a6f78e9c5d2b7c844e485ec0a8abc2238111213fdc5b2038e6d1012eacf316f78
+  languageName: node
+  linkType: hard
+
+"mitt@npm:^3.0.1":
+  version: 3.0.1
+  resolution: "mitt@npm:3.0.1"
+  checksum: 10c0/3ab4fdecf3be8c5255536faa07064d05caa3dd332bd318ff02e04621f7b3069ca1de9106cfe8e7ced675abfc2bec2ce4c4ef321c4a1bb1fb29df8ae090741913
+  languageName: node
+  linkType: hard
+
+"mj-context-menu@npm:^0.6.1":
+  version: 0.6.1
+  resolution: "mj-context-menu@npm:0.6.1"
+  checksum: 10c0/2bf3564bb5cbd3915e14417e8df4a4e3013f1e281d431ee1e88976ae6529cc88e6eea7d9b3e217d1d79666f89070c3e1a193383ba9c7bf82d63075e616664ce9
+  languageName: node
+  linkType: hard
+
+"mkdirp@npm:^3.0.1":
+  version: 3.0.1
+  resolution: "mkdirp@npm:3.0.1"
+  bin:
+    mkdirp: dist/cjs/src/bin.js
+  checksum: 10c0/9f2b975e9246351f5e3a40dcfac99fcd0baa31fbfab615fe059fb11e51f10e4803c63de1f384c54d656e4db31d000e4767e9ef076a22e12a641357602e31d57d
+  languageName: node
+  linkType: hard
+
+"ms@npm:^2.1.3":
+  version: 2.1.3
+  resolution: "ms@npm:2.1.3"
+  checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48
+  languageName: node
+  linkType: hard
+
+"nanoid@npm:^3.3.8":
+  version: 3.3.11
+  resolution: "nanoid@npm:3.3.11"
+  bin:
+    nanoid: bin/nanoid.cjs
+  checksum: 10c0/40e7f70b3d15f725ca072dfc4f74e81fcf1fbb02e491cf58ac0c79093adc9b0a73b152bcde57df4b79cd097e13023d7504acb38404a4da7bc1cd8e887b82fe0b
+  languageName: node
+  linkType: hard
+
+"negotiator@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "negotiator@npm:1.0.0"
+  checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b
+  languageName: node
+  linkType: hard
+
+"node-fetch@npm:^2.6.0":
+  version: 2.7.0
+  resolution: "node-fetch@npm:2.7.0"
+  dependencies:
+    whatwg-url: "npm:^5.0.0"
+  peerDependencies:
+    encoding: ^0.1.0
+  peerDependenciesMeta:
+    encoding:
+      optional: true
+  checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8
+  languageName: node
+  linkType: hard
+
+"node-gyp@npm:latest":
+  version: 11.2.0
+  resolution: "node-gyp@npm:11.2.0"
+  dependencies:
+    env-paths: "npm:^2.2.0"
+    exponential-backoff: "npm:^3.1.1"
+    graceful-fs: "npm:^4.2.6"
+    make-fetch-happen: "npm:^14.0.3"
+    nopt: "npm:^8.0.0"
+    proc-log: "npm:^5.0.0"
+    semver: "npm:^7.3.5"
+    tar: "npm:^7.4.3"
+    tinyglobby: "npm:^0.2.12"
+    which: "npm:^5.0.0"
+  bin:
+    node-gyp: bin/node-gyp.js
+  checksum: 10c0/bd8d8c76b06be761239b0c8680f655f6a6e90b48e44d43415b11c16f7e8c15be346fba0cbf71588c7cdfb52c419d928a7d3db353afc1d952d19756237d8f10b9
+  languageName: node
+  linkType: hard
+
+"nopt@npm:^8.0.0":
+  version: 8.1.0
+  resolution: "nopt@npm:8.1.0"
+  dependencies:
+    abbrev: "npm:^3.0.0"
+  bin:
+    nopt: bin/nopt.js
+  checksum: 10c0/62e9ea70c7a3eb91d162d2c706b6606c041e4e7b547cbbb48f8b3695af457dd6479904d7ace600856bf923dd8d1ed0696f06195c8c20f02ac87c1da0e1d315ef
+  languageName: node
+  linkType: hard
+
+"nth-check@npm:^2.0.1":
+  version: 2.1.1
+  resolution: "nth-check@npm:2.1.1"
+  dependencies:
+    boolbase: "npm:^1.0.0"
+  checksum: 10c0/5fee7ff309727763689cfad844d979aedd2204a817fbaaf0e1603794a7c20db28548d7b024692f953557df6ce4a0ee4ae46cd8ebd9b36cfb300b9226b567c479
+  languageName: node
+  linkType: hard
+
+"oniguruma-to-es@npm:^3.1.0":
+  version: 3.1.1
+  resolution: "oniguruma-to-es@npm:3.1.1"
+  dependencies:
+    emoji-regex-xs: "npm:^1.0.0"
+    regex: "npm:^6.0.1"
+    regex-recursion: "npm:^6.0.2"
+  checksum: 10c0/8eb8390076a396674bf8fe9fe6036fe4ea3316f9793d36fd7fe23dc443492ef52a06f5113408bec7d5ea5ad1dc1984623fcb998b2d2198e35ba2aed3ecb91676
+  languageName: node
+  linkType: hard
+
+"p-map@npm:^7.0.2":
+  version: 7.0.3
+  resolution: "p-map@npm:7.0.3"
+  checksum: 10c0/46091610da2b38ce47bcd1d8b4835a6fa4e832848a6682cf1652bc93915770f4617afc844c10a77d1b3e56d2472bb2d5622353fa3ead01a7f42b04fc8e744a5c
+  languageName: node
+  linkType: hard
+
+"package-json-from-dist@npm:^1.0.0":
+  version: 1.0.1
+  resolution: "package-json-from-dist@npm:1.0.1"
+  checksum: 10c0/62ba2785eb655fec084a257af34dbe24292ab74516d6aecef97ef72d4897310bc6898f6c85b5cd22770eaa1ce60d55a0230e150fb6a966e3ecd6c511e23d164b
+  languageName: node
+  linkType: hard
+
+"parse5-htmlparser2-tree-adapter@npm:^6.0.1":
+  version: 6.0.1
+  resolution: "parse5-htmlparser2-tree-adapter@npm:6.0.1"
+  dependencies:
+    parse5: "npm:^6.0.1"
+  checksum: 10c0/dfa5960e2aaf125707e19a4b1bc333de49232eba5a6ffffb95d313a7d6087c3b7a274b58bee8d3bd41bdf150638815d1d601a42bbf2a0345208c3c35b1279556
+  languageName: node
+  linkType: hard
+
+"parse5@npm:^6.0.1":
+  version: 6.0.1
+  resolution: "parse5@npm:6.0.1"
+  checksum: 10c0/595821edc094ecbcfb9ddcb46a3e1fe3a718540f8320eff08b8cf6742a5114cce2d46d45f95c26191c11b184dcaf4e2960abcd9c5ed9eb9393ac9a37efcfdecb
+  languageName: node
+  linkType: hard
+
+"path-key@npm:^3.1.0":
+  version: 3.1.1
+  resolution: "path-key@npm:3.1.1"
+  checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c
+  languageName: node
+  linkType: hard
+
+"path-scurry@npm:^1.11.1":
+  version: 1.11.1
+  resolution: "path-scurry@npm:1.11.1"
+  dependencies:
+    lru-cache: "npm:^10.2.0"
+    minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0"
+  checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d
+  languageName: node
+  linkType: hard
+
+"perfect-debounce@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "perfect-debounce@npm:1.0.0"
+  checksum: 10c0/e2baac416cae046ef1b270812cf9ccfb0f91c04ea36ac7f5b00bc84cb7f41bdbba087c0ab21b4e02a7ef3a1f1f6db399f137cecec46868bd7d8d88c2a9ee431f
+  languageName: node
+  linkType: hard
+
+"picocolors@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "picocolors@npm:1.1.1"
+  checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58
+  languageName: node
+  linkType: hard
+
+"picomatch@npm:^4.0.2":
+  version: 4.0.2
+  resolution: "picomatch@npm:4.0.2"
+  checksum: 10c0/7c51f3ad2bb42c776f49ebf964c644958158be30d0a510efd5a395e8d49cb5acfed5b82c0c5b365523ce18e6ab85013c9ebe574f60305892ec3fa8eee8304ccc
+  languageName: node
+  linkType: hard
+
+"postcss@npm:^8.4.43, postcss@npm:^8.4.48":
+  version: 8.5.3
+  resolution: "postcss@npm:8.5.3"
+  dependencies:
+    nanoid: "npm:^3.3.8"
+    picocolors: "npm:^1.1.1"
+    source-map-js: "npm:^1.2.1"
+  checksum: 10c0/b75510d7b28c3ab728c8733dd01538314a18c52af426f199a3c9177e63eb08602a3938bfb66b62dc01350b9aed62087eabbf229af97a1659eb8d3513cec823b3
+  languageName: node
+  linkType: hard
+
+"preact@npm:^10.0.0":
+  version: 10.26.4
+  resolution: "preact@npm:10.26.4"
+  checksum: 10c0/8abf64ec6f9773f0c4fb3746b7caa3d83e2de4d464928e7f64fc779c96ef9e135d23c1ade8d0923c9191f6d203d9f22bb92d8a50dc0f4f310073dfaa51a56922
+  languageName: node
+  linkType: hard
+
+"proc-log@npm:^5.0.0":
+  version: 5.0.0
+  resolution: "proc-log@npm:5.0.0"
+  checksum: 10c0/bbe5edb944b0ad63387a1d5b1911ae93e05ce8d0f60de1035b218cdcceedfe39dbd2c697853355b70f1a090f8f58fe90da487c85216bf9671f9499d1a897e9e3
+  languageName: node
+  linkType: hard
+
+"promise-retry@npm:^2.0.1":
+  version: 2.0.1
+  resolution: "promise-retry@npm:2.0.1"
+  dependencies:
+    err-code: "npm:^2.0.2"
+    retry: "npm:^0.12.0"
+  checksum: 10c0/9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96
+  languageName: node
+  linkType: hard
+
+"property-information@npm:^7.0.0":
+  version: 7.0.0
+  resolution: "property-information@npm:7.0.0"
+  checksum: 10c0/bf443e3bbdfc154da8f4ff4c85ed97c3d21f5e5f77cce84d2fd653c6dfb974a75ad61eafbccb2b8d2285942be35d763eaa99d51e29dccc28b40917d3f018107e
+  languageName: node
+  linkType: hard
+
+"regex-recursion@npm:^6.0.2":
+  version: 6.0.2
+  resolution: "regex-recursion@npm:6.0.2"
+  dependencies:
+    regex-utilities: "npm:^2.3.0"
+  checksum: 10c0/68e8b6889680e904b75d7f26edaf70a1a4dc1087406bff53face4c2929d918fd77c72223843fe816ac8ed9964f96b4160650e8d5909e26a998c6e9de324dadb1
+  languageName: node
+  linkType: hard
+
+"regex-utilities@npm:^2.3.0":
+  version: 2.3.0
+  resolution: "regex-utilities@npm:2.3.0"
+  checksum: 10c0/78c550a80a0af75223244fff006743922591bd8f61d91fef7c86b9b56cf9bbf8ee5d7adb6d8991b5e304c57c90103fc4818cf1e357b11c6c669b782839bd7893
+  languageName: node
+  linkType: hard
+
+"regex@npm:^6.0.1":
+  version: 6.0.1
+  resolution: "regex@npm:6.0.1"
+  dependencies:
+    regex-utilities: "npm:^2.3.0"
+  checksum: 10c0/687b3e063d4ca19b0de7c55c24353f868a0fb9ba21512692470d2fb412e3a410894dd5924c91ea49d8cb8fa865e36ec956e52436ae0a256bdc095ff136c30aba
+  languageName: node
+  linkType: hard
+
+"retry@npm:^0.12.0":
+  version: 0.12.0
+  resolution: "retry@npm:0.12.0"
+  checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe
+  languageName: node
+  linkType: hard
+
+"rfdc@npm:^1.4.1":
+  version: 1.4.1
+  resolution: "rfdc@npm:1.4.1"
+  checksum: 10c0/4614e4292356cafade0b6031527eea9bc90f2372a22c012313be1dcc69a3b90c7338158b414539be863fa95bfcb2ddcd0587be696841af4e6679d85e62c060c7
+  languageName: node
+  linkType: hard
+
+"rollup@npm:^4.20.0":
+  version: 4.39.0
+  resolution: "rollup@npm:4.39.0"
+  dependencies:
+    "@rollup/rollup-android-arm-eabi": "npm:4.39.0"
+    "@rollup/rollup-android-arm64": "npm:4.39.0"
+    "@rollup/rollup-darwin-arm64": "npm:4.39.0"
+    "@rollup/rollup-darwin-x64": "npm:4.39.0"
+    "@rollup/rollup-freebsd-arm64": "npm:4.39.0"
+    "@rollup/rollup-freebsd-x64": "npm:4.39.0"
+    "@rollup/rollup-linux-arm-gnueabihf": "npm:4.39.0"
+    "@rollup/rollup-linux-arm-musleabihf": "npm:4.39.0"
+    "@rollup/rollup-linux-arm64-gnu": "npm:4.39.0"
+    "@rollup/rollup-linux-arm64-musl": "npm:4.39.0"
+    "@rollup/rollup-linux-loongarch64-gnu": "npm:4.39.0"
+    "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.39.0"
+    "@rollup/rollup-linux-riscv64-gnu": "npm:4.39.0"
+    "@rollup/rollup-linux-riscv64-musl": "npm:4.39.0"
+    "@rollup/rollup-linux-s390x-gnu": "npm:4.39.0"
+    "@rollup/rollup-linux-x64-gnu": "npm:4.39.0"
+    "@rollup/rollup-linux-x64-musl": "npm:4.39.0"
+    "@rollup/rollup-win32-arm64-msvc": "npm:4.39.0"
+    "@rollup/rollup-win32-ia32-msvc": "npm:4.39.0"
+    "@rollup/rollup-win32-x64-msvc": "npm:4.39.0"
+    "@types/estree": "npm:1.0.7"
+    fsevents: "npm:~2.3.2"
+  dependenciesMeta:
+    "@rollup/rollup-android-arm-eabi":
+      optional: true
+    "@rollup/rollup-android-arm64":
+      optional: true
+    "@rollup/rollup-darwin-arm64":
+      optional: true
+    "@rollup/rollup-darwin-x64":
+      optional: true
+    "@rollup/rollup-freebsd-arm64":
+      optional: true
+    "@rollup/rollup-freebsd-x64":
+      optional: true
+    "@rollup/rollup-linux-arm-gnueabihf":
+      optional: true
+    "@rollup/rollup-linux-arm-musleabihf":
+      optional: true
+    "@rollup/rollup-linux-arm64-gnu":
+      optional: true
+    "@rollup/rollup-linux-arm64-musl":
+      optional: true
+    "@rollup/rollup-linux-loongarch64-gnu":
+      optional: true
+    "@rollup/rollup-linux-powerpc64le-gnu":
+      optional: true
+    "@rollup/rollup-linux-riscv64-gnu":
+      optional: true
+    "@rollup/rollup-linux-riscv64-musl":
+      optional: true
+    "@rollup/rollup-linux-s390x-gnu":
+      optional: true
+    "@rollup/rollup-linux-x64-gnu":
+      optional: true
+    "@rollup/rollup-linux-x64-musl":
+      optional: true
+    "@rollup/rollup-win32-arm64-msvc":
+      optional: true
+    "@rollup/rollup-win32-ia32-msvc":
+      optional: true
+    "@rollup/rollup-win32-x64-msvc":
+      optional: true
+    fsevents:
+      optional: true
+  bin:
+    rollup: dist/bin/rollup
+  checksum: 10c0/2dc0c23ca04bd00295035b405c977261559aed8acc9902ee9ff44e4a6b54734fcb64999c32143c43804dcb543da7983032831b893a902633b006c21848a093ce
+  languageName: node
+  linkType: hard
+
+"safer-buffer@npm:>= 2.1.2 < 3.0.0":
+  version: 2.1.2
+  resolution: "safer-buffer@npm:2.1.2"
+  checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4
+  languageName: node
+  linkType: hard
+
+"semver@npm:^7.3.5":
+  version: 7.7.1
+  resolution: "semver@npm:7.7.1"
+  bin:
+    semver: bin/semver.js
+  checksum: 10c0/fd603a6fb9c399c6054015433051bdbe7b99a940a8fb44b85c2b524c4004b023d7928d47cb22154f8d054ea7ee8597f586605e05b52047f048278e4ac56ae958
+  languageName: node
+  linkType: hard
+
+"shebang-command@npm:^2.0.0":
+  version: 2.0.0
+  resolution: "shebang-command@npm:2.0.0"
+  dependencies:
+    shebang-regex: "npm:^3.0.0"
+  checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e
+  languageName: node
+  linkType: hard
+
+"shebang-regex@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "shebang-regex@npm:3.0.0"
+  checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690
+  languageName: node
+  linkType: hard
+
+"shiki@npm:^2.1.0":
+  version: 2.5.0
+  resolution: "shiki@npm:2.5.0"
+  dependencies:
+    "@shikijs/core": "npm:2.5.0"
+    "@shikijs/engine-javascript": "npm:2.5.0"
+    "@shikijs/engine-oniguruma": "npm:2.5.0"
+    "@shikijs/langs": "npm:2.5.0"
+    "@shikijs/themes": "npm:2.5.0"
+    "@shikijs/types": "npm:2.5.0"
+    "@shikijs/vscode-textmate": "npm:^10.0.2"
+    "@types/hast": "npm:^3.0.4"
+  checksum: 10c0/1f7adf5dae47e37a828f8a442dc98a378014c2f8793e4e1f3562b85be23b295456ba82fded08da61d1f9d35917e6fb98517c6b6f43c65d972f7cbca6ecd596c9
+  languageName: node
+  linkType: hard
+
+"signal-exit@npm:^4.0.1":
+  version: 4.1.0
+  resolution: "signal-exit@npm:4.1.0"
+  checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83
+  languageName: node
+  linkType: hard
+
+"slick@npm:^1.12.2":
+  version: 1.12.2
+  resolution: "slick@npm:1.12.2"
+  checksum: 10c0/fea97c36b2bdcd1b80caea150cd8135dc9d3ffe659bbe04fa6f4b4dff373f5d5aef09a8ef384b331c3fdd9567faf447b75b850ab35d2c69ff8a8a92def3d49e1
+  languageName: node
+  linkType: hard
+
+"smart-buffer@npm:^4.2.0":
+  version: 4.2.0
+  resolution: "smart-buffer@npm:4.2.0"
+  checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539
+  languageName: node
+  linkType: hard
+
+"socks-proxy-agent@npm:^8.0.3":
+  version: 8.0.5
+  resolution: "socks-proxy-agent@npm:8.0.5"
+  dependencies:
+    agent-base: "npm:^7.1.2"
+    debug: "npm:^4.3.4"
+    socks: "npm:^2.8.3"
+  checksum: 10c0/5d2c6cecba6821389aabf18728325730504bf9bb1d9e342e7987a5d13badd7a98838cc9a55b8ed3cb866ad37cc23e1086f09c4d72d93105ce9dfe76330e9d2a6
+  languageName: node
+  linkType: hard
+
+"socks@npm:^2.8.3":
+  version: 2.8.4
+  resolution: "socks@npm:2.8.4"
+  dependencies:
+    ip-address: "npm:^9.0.5"
+    smart-buffer: "npm:^4.2.0"
+  checksum: 10c0/00c3271e233ccf1fb83a3dd2060b94cc37817e0f797a93c560b9a7a86c4a0ec2961fb31263bdd24a3c28945e24868b5f063cd98744171d9e942c513454b50ae5
+  languageName: node
+  linkType: hard
+
+"source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
+  version: 1.2.1
+  resolution: "source-map-js@npm:1.2.1"
+  checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf
+  languageName: node
+  linkType: hard
+
+"space-separated-tokens@npm:^2.0.0":
+  version: 2.0.2
+  resolution: "space-separated-tokens@npm:2.0.2"
+  checksum: 10c0/6173e1d903dca41dcab6a2deed8b4caf61bd13b6d7af8374713500570aa929ff9414ae09a0519f4f8772df993300305a395d4871f35bc4ca72b6db57e1f30af8
+  languageName: node
+  linkType: hard
+
+"speakingurl@npm:^14.0.1":
+  version: 14.0.1
+  resolution: "speakingurl@npm:14.0.1"
+  checksum: 10c0/1de1d1b938a7c4d9e79593ff7a26d312ec04a7c3234ca40b7f9b8106daf74ea9d2110a077f5db97ecf3762b83069e3ccbf9694431b51d4fcfd863f0b3333c342
+  languageName: node
+  linkType: hard
+
+"speech-rule-engine@npm:^4.0.6":
+  version: 4.0.7
+  resolution: "speech-rule-engine@npm:4.0.7"
+  dependencies:
+    commander: "npm:9.2.0"
+    wicked-good-xpath: "npm:1.3.0"
+    xmldom-sre: "npm:0.1.31"
+  bin:
+    sre: bin/sre
+  checksum: 10c0/f9e17b5c6e09de41b67a1895f246706e4f36f8338575cd61f1653a16a0babad90db28365da350a10c4ab561a268d2cd3212cae9eff921556c274deac6e92df3c
+  languageName: node
+  linkType: hard
+
+"sprintf-js@npm:^1.1.3":
+  version: 1.1.3
+  resolution: "sprintf-js@npm:1.1.3"
+  checksum: 10c0/09270dc4f30d479e666aee820eacd9e464215cdff53848b443964202bf4051490538e5dd1b42e1a65cf7296916ca17640aebf63dae9812749c7542ee5f288dec
+  languageName: node
+  linkType: hard
+
+"ssri@npm:^12.0.0":
+  version: 12.0.0
+  resolution: "ssri@npm:12.0.0"
+  dependencies:
+    minipass: "npm:^7.0.3"
+  checksum: 10c0/caddd5f544b2006e88fa6b0124d8d7b28208b83c72d7672d5ade44d794525d23b540f3396108c4eb9280dcb7c01f0bef50682f5b4b2c34291f7c5e211fd1417d
+  languageName: node
+  linkType: hard
+
+"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0":
+  version: 4.2.3
+  resolution: "string-width@npm:4.2.3"
+  dependencies:
+    emoji-regex: "npm:^8.0.0"
+    is-fullwidth-code-point: "npm:^3.0.0"
+    strip-ansi: "npm:^6.0.1"
+  checksum: 10c0/1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b
+  languageName: node
+  linkType: hard
+
+"string-width@npm:^5.0.1, string-width@npm:^5.1.2":
+  version: 5.1.2
+  resolution: "string-width@npm:5.1.2"
+  dependencies:
+    eastasianwidth: "npm:^0.2.0"
+    emoji-regex: "npm:^9.2.2"
+    strip-ansi: "npm:^7.0.1"
+  checksum: 10c0/ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca
+  languageName: node
+  linkType: hard
+
+"stringify-entities@npm:^4.0.0":
+  version: 4.0.4
+  resolution: "stringify-entities@npm:4.0.4"
+  dependencies:
+    character-entities-html4: "npm:^2.0.0"
+    character-entities-legacy: "npm:^3.0.0"
+  checksum: 10c0/537c7e656354192406bdd08157d759cd615724e9d0873602d2c9b2f6a5c0a8d0b1d73a0a08677848105c5eebac6db037b57c0b3a4ec86331117fa7319ed50448
+  languageName: node
+  linkType: hard
+
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1":
+  version: 6.0.1
+  resolution: "strip-ansi@npm:6.0.1"
+  dependencies:
+    ansi-regex: "npm:^5.0.1"
+  checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952
+  languageName: node
+  linkType: hard
+
+"strip-ansi@npm:^7.0.1":
+  version: 7.1.0
+  resolution: "strip-ansi@npm:7.1.0"
+  dependencies:
+    ansi-regex: "npm:^6.0.1"
+  checksum: 10c0/a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4
+  languageName: node
+  linkType: hard
+
+"superjson@npm:^2.2.1":
+  version: 2.2.2
+  resolution: "superjson@npm:2.2.2"
+  dependencies:
+    copy-anything: "npm:^3.0.2"
+  checksum: 10c0/aa49ebe6653e963020bc6a1ed416d267dfda84cfcc3cbd3beffd75b72e44eb9df7327215f3e3e77528f6e19ad8895b16a4964fdcd56d1799d14350db8c92afbc
+  languageName: node
+  linkType: hard
+
+"tabbable@npm:^6.2.0":
+  version: 6.2.0
+  resolution: "tabbable@npm:6.2.0"
+  checksum: 10c0/ced8b38f05f2de62cd46836d77c2646c42b8c9713f5bd265daf0e78ff5ac73d3ba48a7ca45f348bafeef29b23da7187c72250742d37627883ef89cbd7fa76898
+  languageName: node
+  linkType: hard
+
+"tar@npm:^7.4.3":
+  version: 7.4.3
+  resolution: "tar@npm:7.4.3"
+  dependencies:
+    "@isaacs/fs-minipass": "npm:^4.0.0"
+    chownr: "npm:^3.0.0"
+    minipass: "npm:^7.1.2"
+    minizlib: "npm:^3.0.1"
+    mkdirp: "npm:^3.0.1"
+    yallist: "npm:^5.0.0"
+  checksum: 10c0/d4679609bb2a9b48eeaf84632b6d844128d2412b95b6de07d53d8ee8baf4ca0857c9331dfa510390a0727b550fd543d4d1a10995ad86cdf078423fbb8d99831d
+  languageName: node
+  linkType: hard
+
+"tinyglobby@npm:^0.2.12":
+  version: 0.2.12
+  resolution: "tinyglobby@npm:0.2.12"
+  dependencies:
+    fdir: "npm:^6.4.3"
+    picomatch: "npm:^4.0.2"
+  checksum: 10c0/7c9be4fd3625630e262dcb19015302aad3b4ba7fc620f269313e688f2161ea8724d6cb4444baab5ef2826eb6bed72647b169a33ec8eea37501832a2526ff540f
+  languageName: node
+  linkType: hard
+
+"tr46@npm:~0.0.3":
+  version: 0.0.3
+  resolution: "tr46@npm:0.0.3"
+  checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11
+  languageName: node
+  linkType: hard
+
+"trim-lines@npm:^3.0.0":
+  version: 3.0.1
+  resolution: "trim-lines@npm:3.0.1"
+  checksum: 10c0/3a1611fa9e52aa56a94c69951a9ea15b8aaad760eaa26c56a65330dc8adf99cb282fc07cc9d94968b7d4d88003beba220a7278bbe2063328eb23fb56f9509e94
+  languageName: node
+  linkType: hard
+
+"tslib@npm:^2.2.0":
+  version: 2.8.1
+  resolution: "tslib@npm:2.8.1"
+  checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
+  languageName: node
+  linkType: hard
+
+"unique-filename@npm:^4.0.0":
+  version: 4.0.0
+  resolution: "unique-filename@npm:4.0.0"
+  dependencies:
+    unique-slug: "npm:^5.0.0"
+  checksum: 10c0/38ae681cceb1408ea0587b6b01e29b00eee3c84baee1e41fd5c16b9ed443b80fba90c40e0ba69627e30855570a34ba8b06702d4a35035d4b5e198bf5a64c9ddc
+  languageName: node
+  linkType: hard
+
+"unique-slug@npm:^5.0.0":
+  version: 5.0.0
+  resolution: "unique-slug@npm:5.0.0"
+  dependencies:
+    imurmurhash: "npm:^0.1.4"
+  checksum: 10c0/d324c5a44887bd7e105ce800fcf7533d43f29c48757ac410afd42975de82cc38ea2035c0483f4de82d186691bf3208ef35c644f73aa2b1b20b8e651be5afd293
+  languageName: node
+  linkType: hard
+
+"unist-util-is@npm:^6.0.0":
+  version: 6.0.0
+  resolution: "unist-util-is@npm:6.0.0"
+  dependencies:
+    "@types/unist": "npm:^3.0.0"
+  checksum: 10c0/9419352181eaa1da35eca9490634a6df70d2217815bb5938a04af3a662c12c5607a2f1014197ec9c426fbef18834f6371bfdb6f033040fa8aa3e965300d70e7e
+  languageName: node
+  linkType: hard
+
+"unist-util-position@npm:^5.0.0":
+  version: 5.0.0
+  resolution: "unist-util-position@npm:5.0.0"
+  dependencies:
+    "@types/unist": "npm:^3.0.0"
+  checksum: 10c0/dde3b31e314c98f12b4dc6402f9722b2bf35e96a4f2d463233dd90d7cde2d4928074a7a11eff0a5eb1f4e200f27fc1557e0a64a7e8e4da6558542f251b1b7400
+  languageName: node
+  linkType: hard
+
+"unist-util-stringify-position@npm:^4.0.0":
+  version: 4.0.0
+  resolution: "unist-util-stringify-position@npm:4.0.0"
+  dependencies:
+    "@types/unist": "npm:^3.0.0"
+  checksum: 10c0/dfe1dbe79ba31f589108cb35e523f14029b6675d741a79dea7e5f3d098785045d556d5650ec6a8338af11e9e78d2a30df12b1ee86529cded1098da3f17ee999e
+  languageName: node
+  linkType: hard
+
+"unist-util-visit-parents@npm:^6.0.0":
+  version: 6.0.1
+  resolution: "unist-util-visit-parents@npm:6.0.1"
+  dependencies:
+    "@types/unist": "npm:^3.0.0"
+    unist-util-is: "npm:^6.0.0"
+  checksum: 10c0/51b1a5b0aa23c97d3e03e7288f0cdf136974df2217d0999d3de573c05001ef04cccd246f51d2ebdfb9e8b0ed2704451ad90ba85ae3f3177cf9772cef67f56206
+  languageName: node
+  linkType: hard
+
+"unist-util-visit@npm:^5.0.0":
+  version: 5.0.0
+  resolution: "unist-util-visit@npm:5.0.0"
+  dependencies:
+    "@types/unist": "npm:^3.0.0"
+    unist-util-is: "npm:^6.0.0"
+    unist-util-visit-parents: "npm:^6.0.0"
+  checksum: 10c0/51434a1d80252c1540cce6271a90fd1a106dbe624997c09ed8879279667fb0b2d3a685e02e92bf66598dcbe6cdffa7a5f5fb363af8fdf90dda6c855449ae39a5
+  languageName: node
+  linkType: hard
+
+"valid-data-url@npm:^3.0.0":
+  version: 3.0.1
+  resolution: "valid-data-url@npm:3.0.1"
+  checksum: 10c0/ffc7cac681976ca2db01003dc14286f75241309e90d96e505580469125c83c2de6b5203f0222226cb08f6daf0aff7de9855655c28a64e8590e7b58c01694a896
+  languageName: node
+  linkType: hard
+
+"vfile-message@npm:^4.0.0":
+  version: 4.0.2
+  resolution: "vfile-message@npm:4.0.2"
+  dependencies:
+    "@types/unist": "npm:^3.0.0"
+    unist-util-stringify-position: "npm:^4.0.0"
+  checksum: 10c0/07671d239a075f888b78f318bc1d54de02799db4e9dce322474e67c35d75ac4a5ac0aaf37b18801d91c9f8152974ea39678aa72d7198758b07f3ba04fb7d7514
+  languageName: node
+  linkType: hard
+
+"vfile@npm:^6.0.0":
+  version: 6.0.3
+  resolution: "vfile@npm:6.0.3"
+  dependencies:
+    "@types/unist": "npm:^3.0.0"
+    vfile-message: "npm:^4.0.0"
+  checksum: 10c0/e5d9eb4810623f23758cfc2205323e33552fb5972e5c2e6587babe08fe4d24859866277404fb9e2a20afb71013860d96ec806cb257536ae463c87d70022ab9ef
+  languageName: node
+  linkType: hard
+
+"vite@npm:^5.4.14":
+  version: 5.4.17
+  resolution: "vite@npm:5.4.17"
+  dependencies:
+    esbuild: "npm:^0.21.3"
+    fsevents: "npm:~2.3.3"
+    postcss: "npm:^8.4.43"
+    rollup: "npm:^4.20.0"
+  peerDependencies:
+    "@types/node": ^18.0.0 || >=20.0.0
+    less: "*"
+    lightningcss: ^1.21.0
+    sass: "*"
+    sass-embedded: "*"
+    stylus: "*"
+    sugarss: "*"
+    terser: ^5.4.0
+  dependenciesMeta:
+    fsevents:
+      optional: true
+  peerDependenciesMeta:
+    "@types/node":
+      optional: true
+    less:
+      optional: true
+    lightningcss:
+      optional: true
+    sass:
+      optional: true
+    sass-embedded:
+      optional: true
+    stylus:
+      optional: true
+    sugarss:
+      optional: true
+    terser:
+      optional: true
+  bin:
+    vite: bin/vite.js
+  checksum: 10c0/3322bd6d8da30cbc87b1b24cd14fdbca75abb36de81217d1062c8b4c574a1a0d28d11dfe23a3eed08b3d179d2bdc1510e0d7b9f3e1b722a45bd7631c7cec72eb
+  languageName: node
+  linkType: hard
+
+"vitepress@npm:^1.6.3":
+  version: 1.6.3
+  resolution: "vitepress@npm:1.6.3"
+  dependencies:
+    "@docsearch/css": "npm:3.8.2"
+    "@docsearch/js": "npm:3.8.2"
+    "@iconify-json/simple-icons": "npm:^1.2.21"
+    "@shikijs/core": "npm:^2.1.0"
+    "@shikijs/transformers": "npm:^2.1.0"
+    "@shikijs/types": "npm:^2.1.0"
+    "@types/markdown-it": "npm:^14.1.2"
+    "@vitejs/plugin-vue": "npm:^5.2.1"
+    "@vue/devtools-api": "npm:^7.7.0"
+    "@vue/shared": "npm:^3.5.13"
+    "@vueuse/core": "npm:^12.4.0"
+    "@vueuse/integrations": "npm:^12.4.0"
+    focus-trap: "npm:^7.6.4"
+    mark.js: "npm:8.11.1"
+    minisearch: "npm:^7.1.1"
+    shiki: "npm:^2.1.0"
+    vite: "npm:^5.4.14"
+    vue: "npm:^3.5.13"
+  peerDependencies:
+    markdown-it-mathjax3: ^4
+    postcss: ^8
+  peerDependenciesMeta:
+    markdown-it-mathjax3:
+      optional: true
+    postcss:
+      optional: true
+  bin:
+    vitepress: bin/vitepress.js
+  checksum: 10c0/7a22868ae9c985fb8b4bc9df75f1ca20e2a0900d1620e38fbe0c3cc830e707029babd997aab6b0503f09a94d5df13037577bba17d9a294f5109ee78246060a25
+  languageName: node
+  linkType: hard
+
+"vue@npm:^3.5.13":
+  version: 3.5.13
+  resolution: "vue@npm:3.5.13"
+  dependencies:
+    "@vue/compiler-dom": "npm:3.5.13"
+    "@vue/compiler-sfc": "npm:3.5.13"
+    "@vue/runtime-dom": "npm:3.5.13"
+    "@vue/server-renderer": "npm:3.5.13"
+    "@vue/shared": "npm:3.5.13"
+  peerDependencies:
+    typescript: "*"
+  peerDependenciesMeta:
+    typescript:
+      optional: true
+  checksum: 10c0/4bbb5caf3f04fed933b01c100804f3693ff902984a3152ea1359a972264fa3240f6551d32f0163a79c64df3715b4d6691818c9f652cdd41b2473c69e2b0a373d
+  languageName: node
+  linkType: hard
+
+"web-resource-inliner@npm:^6.0.1":
+  version: 6.0.1
+  resolution: "web-resource-inliner@npm:6.0.1"
+  dependencies:
+    ansi-colors: "npm:^4.1.1"
+    escape-goat: "npm:^3.0.0"
+    htmlparser2: "npm:^5.0.0"
+    mime: "npm:^2.4.6"
+    node-fetch: "npm:^2.6.0"
+    valid-data-url: "npm:^3.0.0"
+  checksum: 10c0/b4b457de2448255100797b1eaefa0f62a8846b2452de8495b9ec17d3e223ebb4848a31b11a645e3541a5b114eb9f201219cda2f99d1b513631777f8c89d1c8a6
+  languageName: node
+  linkType: hard
+
+"webidl-conversions@npm:^3.0.0":
+  version: 3.0.1
+  resolution: "webidl-conversions@npm:3.0.1"
+  checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db
+  languageName: node
+  linkType: hard
+
+"whatwg-url@npm:^5.0.0":
+  version: 5.0.0
+  resolution: "whatwg-url@npm:5.0.0"
+  dependencies:
+    tr46: "npm:~0.0.3"
+    webidl-conversions: "npm:^3.0.0"
+  checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5
+  languageName: node
+  linkType: hard
+
+"which@npm:^2.0.1":
+  version: 2.0.2
+  resolution: "which@npm:2.0.2"
+  dependencies:
+    isexe: "npm:^2.0.0"
+  bin:
+    node-which: ./bin/node-which
+  checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f
+  languageName: node
+  linkType: hard
+
+"which@npm:^5.0.0":
+  version: 5.0.0
+  resolution: "which@npm:5.0.0"
+  dependencies:
+    isexe: "npm:^3.1.1"
+  bin:
+    node-which: bin/which.js
+  checksum: 10c0/e556e4cd8b7dbf5df52408c9a9dd5ac6518c8c5267c8953f5b0564073c66ed5bf9503b14d876d0e9c7844d4db9725fb0dcf45d6e911e17e26ab363dc3965ae7b
+  languageName: node
+  linkType: hard
+
+"wicked-good-xpath@npm:1.3.0":
+  version: 1.3.0
+  resolution: "wicked-good-xpath@npm:1.3.0"
+  checksum: 10c0/6837e1027e75e2ad7fe0620d11def4c42235cbe6c649727258973d7d77c2c5066a5376595e0283d3ba8e2fa22deb3cfe7358db5a02a938bde273b55b9ac57832
+  languageName: node
+  linkType: hard
+
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+  version: 7.0.0
+  resolution: "wrap-ansi@npm:7.0.0"
+  dependencies:
+    ansi-styles: "npm:^4.0.0"
+    string-width: "npm:^4.1.0"
+    strip-ansi: "npm:^6.0.0"
+  checksum: 10c0/d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da
+  languageName: node
+  linkType: hard
+
+"wrap-ansi@npm:^8.1.0":
+  version: 8.1.0
+  resolution: "wrap-ansi@npm:8.1.0"
+  dependencies:
+    ansi-styles: "npm:^6.1.0"
+    string-width: "npm:^5.0.1"
+    strip-ansi: "npm:^7.0.1"
+  checksum: 10c0/138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60
+  languageName: node
+  linkType: hard
+
+"xmldom-sre@npm:0.1.31":
+  version: 0.1.31
+  resolution: "xmldom-sre@npm:0.1.31"
+  checksum: 10c0/d4250a44949b7874661fc05cb708c84a1258871d63aae67271f6a0dec3003d301238f7b477ab7a31b828c9bdc1fc46f7469e81d04fe4a1bb6d3f527dfb602029
+  languageName: node
+  linkType: hard
+
+"yallist@npm:^4.0.0":
+  version: 4.0.0
+  resolution: "yallist@npm:4.0.0"
+  checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a
+  languageName: node
+  linkType: hard
+
+"yallist@npm:^5.0.0":
+  version: 5.0.0
+  resolution: "yallist@npm:5.0.0"
+  checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416
+  languageName: node
+  linkType: hard
+
+"zwitch@npm:^2.0.4":
+  version: 2.0.4
+  resolution: "zwitch@npm:2.0.4"
+  checksum: 10c0/3c7830cdd3378667e058ffdb4cf2bb78ac5711214e2725900873accb23f3dfe5f9e7e5a06dcdc5f29605da976fc45c26d9a13ca334d6eea2245a15e77b8fc06e
+  languageName: node
+  linkType: hard
diff --git a/go.mod b/go.mod
index c75bcf7a..f02ad708 100644
--- a/go.mod
+++ b/go.mod
@@ -37,7 +37,6 @@ require (
 	github.com/samber/lo v1.44.0
 	github.com/shopspring/decimal v1.4.0
 	github.com/smartwalle/alipay/v3 v3.2.21
-	github.com/sony/gobreaker v1.0.0
 	github.com/spf13/viper v1.18.2
 	github.com/sqids/sqids-go v0.4.1
 	github.com/stretchr/testify v1.9.0
diff --git a/go.sum b/go.sum
index 7a94d686..138f2a97 100644
--- a/go.sum
+++ b/go.sum
@@ -306,8 +306,6 @@ github.com/smartwalle/ngx v1.0.9 h1:pUXDvWRZJIHVrCKA1uZ15YwNti+5P4GuJGbpJ4WvpMw=
 github.com/smartwalle/ngx v1.0.9/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0=
 github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E=
 github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc=
-github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
-github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
 github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
 github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
 github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
diff --git a/i18n/en.json b/i18n/en.json
index 41bfc517..0a99ba71 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -656,7 +656,6 @@
   "请为渠道命名": "Please name the channel",
   "可空，请输入中转API地址，例如通过cloudflare中转": "Optional, please enter the transfer API address, for example, through cloudflare transfer",
   "请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel",
-  "单独设置代理地址，支持http和socks5，例如：http://127.0.0.1:1080": "Set the proxy address separately, support http and socks5, for example: http://127.0.0.1:1080",
   "用于测试使用的模型，为空时无法测速,如：gpt-3.5-turbo，仅支持chat模型": "The model used for testing, cannot be tested when empty, such as: gpt-3.5-turbo，Only support chat model.",
   "请选择该渠道所支持的模型,你也可以输入通配符*来匹配模型，例如：gpt-3.5*，表示支持所有gpt-3.5开头的模型，*号只能在最后一位使用，前面必须有字符，例如：gpt-3.5*是正确的，*gpt-3.5是错误的": "Please select the models supported by the channel. You can also enter the wildcard * to match the model, for example: gpt-3.5*, which means that all models starting with gpt-3.5 are supported. The * can only be used in the last position, and there must be characters in front of it. For example: gpt-3.5* is correct, *gpt-3.5 is incorrect",
   "请输入要修改的模型映射关系，格式为：api请求模型ID:实际转发给渠道的模型ID，使用JSON数组表示，例如：": "Please enter the model mapping relationship to be modified, the format is: api request model ID: model ID actually forwarded to the channel, represented by a JSON array, for example:",
diff --git a/model/balancer.go b/model/balancer.go
index 0ac5db78..6b6f96c7 100644
--- a/model/balancer.go
+++ b/model/balancer.go
@@ -258,6 +258,7 @@ func (cc *ChannelsChooser) Load() {
 
 	// 处理每个channel
 	for _, channel := range channels {
+		channel.SetProxy()
 		if *channel.Weight == 0 {
 			channel.Weight = &config.DefaultChannelWeight
 		}
diff --git a/model/channel.go b/model/channel.go
index c024898f..ee60ae15 100644
--- a/model/channel.go
+++ b/model/channel.go
@@ -1,6 +1,8 @@
 package model
 
 import (
+	"crypto/md5"
+	"encoding/hex"
 	"one-api/common/config"
 	"one-api/common/logger"
 	"one-api/common/utils"
@@ -57,54 +59,70 @@ var allowedChannelOrderFields = map[string]bool{
 type SearchChannelsParams struct {
 	Channel
 	PaginationParams
-	FilterTag bool `json:"filter_tag" form:"filter_tag"`
+	FilterTag int `json:"filter_tag" form:"filter_tag"`
 }
 
 func GetChannelsList(params *SearchChannelsParams) (*DataResult[Channel], error) {
 	var channels []*Channel
 
 	db := DB.Omit("key")
+	tagDB := DB.Model(&Channel{}).Select("Max(id) as id").Where("tag != ''").Group("tag")
 
 	if params.Type != 0 {
 		db = db.Where("type = ?", params.Type)
+		tagDB = tagDB.Where("type = ?", params.Type)
 	}
 
 	if params.Status != 0 {
 		db = db.Where("status = ?", params.Status)
+		tagDB = tagDB.Where("status = ?", params.Status)
 	}
 
 	if params.Name != "" {
 		db = db.Where("name LIKE ?", "%"+params.Name+"%")
+		tagDB = tagDB.Where("tag LIKE ?", "%"+params.Name+"%")
 	}
 
 	if params.Group != "" {
 		groupKey := quotePostgresField("group")
 		db = db.Where("( "+groupKey+" LIKE ? OR "+groupKey+" LIKE ? OR "+groupKey+" LIKE ? OR "+groupKey+" = ?)",
 			"%,"+params.Group+",%", params.Group+",%", "%,"+params.Group, params.Group)
+		tagDB = tagDB.Where("( "+groupKey+" LIKE ? OR "+groupKey+" LIKE ? OR "+groupKey+" LIKE ? OR "+groupKey+" = ?)",
+			"%,"+params.Group+",%", params.Group+",%", "%,"+params.Group, params.Group)
 	}
 
 	if params.Models != "" {
 		db = db.Where("models LIKE ?", "%"+params.Models+"%")
+		tagDB = tagDB.Where("models LIKE ?", "%"+params.Models+"%")
 	}
 
 	if params.Other != "" {
 		db = db.Where("other LIKE ?", params.Other+"%")
+		tagDB = tagDB.Where("other LIKE ?", params.Other+"%")
 	}
 
 	if params.Key != "" {
 		db = db.Where(quotePostgresField("key")+" = ?", params.Key)
+		tagDB = tagDB.Where(quotePostgresField("key")+" = ?", params.Key)
 	}
 
 	if params.TestModel != "" {
 		db = db.Where("test_model LIKE ?", params.TestModel+"%")
+		tagDB = tagDB.Where("test_model LIKE ?", params.TestModel+"%")
 	}
 
 	if params.Tag != "" {
 		db = db.Where("tag = ?", params.Tag)
+		tagDB = tagDB.Where("tag = ?", params.Tag)
 	}
 
-	if params.FilterTag {
+	switch params.FilterTag {
+	case 1:
 		db = db.Where("tag = ''")
+	case 2:
+		db = db.Where("id IN (?)", tagDB)
+	default:
+		db = db.Where("tag = '' OR id IN (?)", tagDB)
 	}
 
 	return PaginateAndOrder(db, &params.PaginationParams, &channels, allowedChannelOrderFields)
@@ -118,8 +136,7 @@ func GetAllChannels() ([]*Channel, error) {
 
 func GetChannelById(id int) (*Channel, error) {
 	channel := Channel{Id: id}
-	var err error = nil
-	err = DB.First(&channel, "id = ?", id).Error
+	err := DB.First(&channel, "id = ?", id).Error
 
 	return &channel, err
 }
@@ -135,6 +152,11 @@ func DeleteChannelTag(channelId int) error {
 	return err
 }
 
+func BatchDeleteChannel(ids []int) (int64, error) {
+	result := DB.Where("id IN ?", ids).Delete(&Channel{})
+	return result.RowsAffected, result.Error
+}
+
 func BatchInsertChannels(channels []Channel) error {
 	err := DB.Omit("UsedQuota").Create(&channels).Error
 	if err != nil {
@@ -192,6 +214,19 @@ func BatchDelModelChannels(params *BatchChannelsParams) (int64, error) {
 	return count, nil
 }
 
+func (c *Channel) SetProxy() {
+	if c.Proxy == nil {
+		return
+	}
+
+	if strings.Contains(*c.Proxy, "%s") {
+		md5Str := md5.Sum([]byte(c.Key))
+		idStr := hex.EncodeToString(md5Str[:])
+		*c.Proxy = strings.Replace(*c.Proxy, "%s", idStr, 1)
+	}
+
+}
+
 func (channel *Channel) GetPriority() int64 {
 	if channel.Priority == nil {
 		return 0
diff --git a/model/channel_tag.go b/model/channel_tag.go
index 9ded6bf7..844b43f5 100644
--- a/model/channel_tag.go
+++ b/model/channel_tag.go
@@ -16,26 +16,14 @@ type SearchChannelsTagParams struct {
 }
 
 type ChannelTag struct {
-	ID  int    `json:"id" gorm:"column:id"`
-	Tag string `json:"tag" gorm:"column:tag"`
+	ID  int    `json:"id"`
+	Tag string `json:"tag"`
 }
 
-func GetChannelsTagList(params *SearchChannelsTagParams) (*DataResult[Channel], error) {
+func GetChannelsTagList(tag string) ([]*Channel, error) {
 	var channels []*Channel
-	// 子查询：为每个tag选择最小的id
-	subQuery := DB.Model(&Channel{}).
-		Select("MIN(id) as id").
-		Where("tag != ''").
-		Group("tag")
-
-	db := DB.Select("tag, type, models, " + quotePostgresField("group"))
-	if params.Tag != "" {
-		subQuery = subQuery.Where("tag = ?", params.Tag)
-	}
-
-	db = db.Model(&Channel{}).Where("id IN (?)", subQuery)
-
-	return PaginateAndOrder(db, &params.PaginationParams, &channels, allowedChannelOrderFields)
+	err := DB.Model(&Channel{}).Where("tag = ?", tag).Find(&channels).Error
+	return channels, err
 }
 
 func GetChannelsTagAllList() ([]*ChannelTag, error) {
@@ -156,6 +144,7 @@ func UpdateChannelsTag(tag string, channel *Channel) error {
 
 	err = tx.Model(Channel{}).Where("tag = ?", tag).Updates(
 		Channel{
+			BaseURL:      channel.BaseURL,
 			Other:        channel.Other,
 			Models:       channel.Models,
 			Group:        channel.Group,
@@ -204,6 +193,21 @@ func DeleteChannelsTag(tag string, delDisabled bool) error {
 	return err
 }
 
+func ChangeChannelsTagStatus(tag string, status int) error {
+	if tag == "" {
+		return nil
+	}
+
+	err := DB.Model(&Channel{}).Where("tag = ?", tag).Update("status", status).Error
+	if err != nil {
+		return err
+	}
+
+	ChannelGroup.Load()
+
+	return nil
+}
+
 func UpdateChannelsTagPriority(tag string, value int) error {
 	err := DB.Model(&Channel{}).Where("tag = ?", tag).Update("priority", value).Error
 	if err != nil {
diff --git a/model/price.go b/model/price.go
index 5adc0865..eac4b261 100644
--- a/model/price.go
+++ b/model/price.go
@@ -88,23 +88,21 @@ func (price *Price) GetOutput() float64 {
 }
 
 func (price *Price) GetExtraRatio(key string) float64 {
-	if key == "cached_tokens_ratio" {
-		return DefaultCacheRatios
-	}
-
 	if ratio, ok := price.ExtraRatios[key]; ok {
 		return ratio
 	}
 
-	if key == "cached_write_ratio" {
+	switch key {
+	case "cached_tokens_ratio":
+		return DefaultCacheRatios
+	case "cached_write_ratio":
 		return DefaultCachedWriteRatio
-	}
-
-	if key == "cached_read_ratio" {
+	case "cached_read_ratio":
 		return DefaultCachedReadRatio
+	default:
+		return DefaultAudioRatio
 	}
 
-	return DefaultAudioRatio
 }
 
 func (price *Price) FetchInputCurrencyPrice(rate float64) string {
diff --git a/providers/ali/base.go b/providers/ali/base.go
index 466903e9..26d5789b 100644
--- a/providers/ali/base.go
+++ b/providers/ali/base.go
@@ -108,8 +108,12 @@ func errorHandle(aliError *AliError) *types.OpenAIError {
 func (p *AliProvider) GetFullRequestURL(requestURL string, modelName string) string {
 	baseURL := strings.TrimSuffix(p.GetBaseURL(), "/")
 
-	if strings.Contains(modelName, "-vl") {
-		requestURL = "/api/v1/services/aigc/multimodal-generation/generation"
+	modelKeywords := strings.Split(VisionModelKeywords, ",")
+	for _, keyword := range modelKeywords {
+		if strings.Contains(modelName, keyword) {
+			requestURL = "/api/v1/services/aigc/multimodal-generation/generation"
+			break
+		}
 	}
 
 	return fmt.Sprintf("%s%s", baseURL, requestURL)
diff --git a/providers/ali/chat.go b/providers/ali/chat.go
index ee479a59..53a50537 100644
--- a/providers/ali/chat.go
+++ b/providers/ali/chat.go
@@ -123,7 +123,15 @@ func (p *AliProvider) convertFromChatOpenai(request *types.ChatCompletionRequest
 	messages := make([]AliMessage, 0, len(request.Messages))
 	for i := 0; i < len(request.Messages); i++ {
 		message := request.Messages[i]
-		if !strings.HasPrefix(request.Model, "qwen-vl") {
+		modelKeywords := strings.Split(VisionModelKeywords, ",")
+		isVisionModel := false
+		for _, keyword := range modelKeywords {
+			if strings.Contains(request.Model, keyword) {
+				isVisionModel = true
+				break
+			}
+		}
+		if !isVisionModel {
 			messages = append(messages, AliMessage{
 				Content: message.StringContent(),
 				Role:    strings.ToLower(message.Role),
diff --git a/providers/ali/model.go b/providers/ali/model.go
index 829f01cb..8e5666fd 100644
--- a/providers/ali/model.go
+++ b/providers/ali/model.go
@@ -7,6 +7,11 @@ import (
 	"one-api/providers/openai"
 )
 
+const (
+	// VisionModelKeywords 定义视觉模型的关键词
+	VisionModelKeywords = "-vl,qvq,vision"
+)
+
 func (p *AliProvider) GetModelList() ([]string, error) {
 	fullRequestURL := fmt.Sprintf("%s%s", OpenaiBaseURL, p.Config.ModelList)
 	headers := p.GetRequestHeaders()
diff --git a/providers/gemini/chat.go b/providers/gemini/chat.go
index 3b1b9659..103e57b4 100644
--- a/providers/gemini/chat.go
+++ b/providers/gemini/chat.go
@@ -108,7 +108,7 @@ func (p *GeminiProvider) getChatRequest(geminiRequest *GeminiChatRequest, isRela
 
 	var body any
 	if isRelay {
-		body = p.Context.Request.Body
+		body = geminiRequest.GetJsonRaw()
 	} else {
 		p.pluginHandle(geminiRequest)
 		body = geminiRequest
diff --git a/providers/gemini/relay.go b/providers/gemini/relay.go
index 462507ea..f89b635f 100644
--- a/providers/gemini/relay.go
+++ b/providers/gemini/relay.go
@@ -3,6 +3,8 @@ package gemini
 import (
 	"bytes"
 	"encoding/json"
+	"net/http"
+	"one-api/common"
 	"one-api/common/requester"
 	"one-api/types"
 	"strings"
@@ -32,6 +34,10 @@ func (p *GeminiProvider) CreateGeminiChat(request *GeminiChatRequest) (*GeminiCh
 		return nil, errWithCode
 	}
 
+	if len(geminiResponse.Candidates) == 0 {
+		return nil, common.StringErrorWrapper("no candidates", "no_candidates", http.StatusInternalServerError)
+	}
+
 	usage := p.GetUsage()
 	*usage = convertOpenAIUsage(request.Model, geminiResponse.UsageMetadata)
 
diff --git a/providers/gemini/type.go b/providers/gemini/type.go
index 994b5d4d..42dca58c 100644
--- a/providers/gemini/type.go
+++ b/providers/gemini/type.go
@@ -14,6 +14,7 @@ import (
 	"strings"
 
 	goahocorasick "github.com/anknown/ahocorasick"
+	"github.com/gin-gonic/gin"
 )
 
 const GeminiImageSymbol = "![one-hub-gemini-image]"
@@ -34,6 +35,20 @@ type GeminiChatRequest struct {
 	Tools             []GeminiChatTools          `json:"tools,omitempty"`
 	ToolConfig        *GeminiToolConfig          `json:"toolConfig,omitempty"`
 	SystemInstruction any                        `json:"systemInstruction,omitempty"`
+
+	JsonRaw []byte `json:"-"`
+}
+
+func (r *GeminiChatRequest) GetJsonRaw() []byte {
+	return r.JsonRaw
+}
+
+func (r *GeminiChatRequest) SetJsonRaw(c *gin.Context) {
+	rawData, err := c.GetRawData()
+	if err != nil {
+		return
+	}
+	r.JsonRaw = rawData
 }
 
 type GeminiToolConfig struct {
diff --git a/relay/gemini.go b/relay/gemini.go
index c4b1a492..f9ab629e 100644
--- a/relay/gemini.go
+++ b/relay/gemini.go
@@ -50,6 +50,7 @@ func (r *relayGeminiOnly) setRequest() error {
 	if err := common.UnmarshalBodyReusable(r.c, r.geminiRequest); err != nil {
 		return err
 	}
+	r.geminiRequest.SetJsonRaw(r.c)
 	r.geminiRequest.Model = modelList[0]
 	r.geminiRequest.Stream = isStream
 	r.setOriginalModel(r.geminiRequest.Model)
diff --git a/router/api-router.go b/router/api-router.go
index 809d303a..edadf817 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -127,17 +127,19 @@ func SetApiRouter(router *gin.Engine) {
 			channelRoute.DELETE("/disabled", controller.DeleteDisabledChannel)
 			channelRoute.DELETE("/:id/tag", controller.DeleteChannelTag)
 			channelRoute.DELETE("/:id", controller.DeleteChannel)
+			channelRoute.DELETE("/batch", controller.BatchDeleteChannel)
 		}
 		channelTagRoute := apiRouter.Group("/channel_tag")
 		channelTagRoute.Use(middleware.AdminAuth())
 		{
 			channelTagRoute.GET("/_all", controller.GetChannelsTagAllList)
-			channelTagRoute.GET("/", controller.GetChannelsTagList)
+			channelTagRoute.GET("/:tag/list", controller.GetChannelsTagList)
 			channelTagRoute.GET("/:tag", controller.GetChannelsTag)
 			channelTagRoute.PUT("/:tag", controller.UpdateChannelsTag)
 			channelTagRoute.DELETE("/:tag", controller.DeleteChannelsTag)
 			channelTagRoute.DELETE("/:tag/disabled", controller.DeleteDisabledChannelsTag)
 			channelTagRoute.PUT("/:tag/priority", controller.UpdateChannelsTagPriority)
+			channelTagRoute.PUT("/:tag/status/:status", controller.ChangeChannelsTagStatus)
 
 		}
 
diff --git a/web/src/assets/scss/_themes-vars.module.scss b/web/src/assets/scss/_themes-vars.module.scss
index 76a12ad7..addfc218 100644
--- a/web/src/assets/scss/_themes-vars.module.scss
+++ b/web/src/assets/scss/_themes-vars.module.scss
@@ -2,88 +2,88 @@
 $paper: #ffffff;
 
 // primary
-$primaryLight: #eef2f6;
-$primaryMain: #2196f3;
-$primaryDark: #1e88e5;
-$primary200: #90caf9;
-$primary800: #1565c0;
-$primaryDark2: #68CDF9;
+$primaryLight: #F0F7FF;
+$primaryMain: #0884DD;
+$primaryDark: #0062A3;
+$primary200: #60B8FF;
+$primary800: #004D85;
+$primaryDark2: #3399E5;
 // $main: #078DEE;
 
 // secondary
-$secondaryLight: #ede7f6;
-$secondaryMain: #673ab7;
-$secondaryDark: #5e35b1;
-$secondary200: #b39ddb;
-$secondary800: #4527a0;
+$secondaryLight: #F6F0FF;
+$secondaryMain: #7B61FF;
+$secondaryDark: #5A3FD6;
+$secondary200: #B4A1FF;
+$secondary800: #4526BF;
 
 // success Colors
-$successLight: #b9f6ca;
-$success200: #69f0ae;
-$successMain: #00e676;
-$successDark: #00c853;
+$successLight: #E6F8F1;
+$success200: #66D9AB;
+$successMain: #22C882;
+$successDark: #0AA366;
 
 // error
-$errorLight: #ef9a9a;
-$errorMain: #f44336;
-$errorDark: #c62828;
+$errorLight: #FFEBEE;
+$errorMain: #FF4D4F;
+$errorDark: #CF1322;
 
 // orange
-$orangeLight: #fbe9e7;
-$orangeMain: #ffab91;
-$orangeDark: #d84315;
+$orangeLight: #FFF3E0;
+$orangeMain: #FF9800;
+$orangeDark: #E65100;
 
 // warning
-$warningLight: #fff8e1;
-$warningMain: #ffe57f;
-$warningDark: #ffc107;
+$warningLight: #FFF8E1;
+$warningMain: #FAAD14;
+$warningDark: #D48806;
 
 // grey
-$grey50: #f8fafc;
-$grey100: #eef2f6;
-$grey200: #e3e8ef;
-$grey300: #cdd5df;
-$grey500: #697586;
-$grey600: #4b5565;
-$grey700: #364152;
-$grey900: #121926;
-
-$tableBackground: #f4f6f8;
-$tableBorderBottom: #f1f3f4;
+$grey50: #FAFAFA;
+$grey100: #F5F5F5;
+$grey200: #EEEEEE;
+$grey300: #E0E0E0;
+$grey500: #9E9E9E;
+$grey600: #757575;
+$grey700: #616161;
+$grey900: #212121;
+
+$tableBackground: #F8FAFC;
+$tableBorderBottom: #F0F2F5;
 
 // ==============================|| DARK THEME VARIANTS ||============================== //
 
 // paper & background
-$darkBackground: #141A21; // level 3
-$darkPaper: #141A21; // level 4
-$darkDivider: rgba(227, 232, 239, 0.2);
-$darkSelectedBack : rgba(124, 77, 255, 0.15);
+$darkBackground: #101820; // level 3
+$darkPaper: #1E2A38; // level 4
+$darkDivider: rgba(240, 247, 255, 0.12);
+$darkSelectedBack : rgba(0, 132, 221, 0.12);
 
 // dark 800 & 900
-$darkLevel1: #29314f; // level 1
-$darkLevel2: #1C252E; // level 2
+$darkLevel1: #131E29; // level 1
+$darkLevel2: #1A2736; // level 2
 
-$darkTableHeader: #2A323C;
+$darkTableHeader: #253545;
 
 // primary dark
-$darkPrimaryLight: #eef2f6;
-$darkPrimaryMain: #2196f3;
-$darkPrimaryDark: #1e88e5;
-$darkPrimary200: #90caf9;
-$darkPrimary800: #1565c0;
+$darkPrimaryLight: #60B8FF;
+$darkPrimaryMain: #0884DD;
+$darkPrimaryDark: #0062A3;
+$darkPrimary200: #91CAFF;
+$darkPrimary800: #004D85;
 
 // secondary dark
-$darkSecondaryLight: #d1c4e9;
-$darkSecondaryMain: #7c4dff;
-$darkSecondaryDark: #651fff;
-$darkSecondary200: #b39ddb;
-$darkSecondary800: #6200ea;
+$darkSecondaryLight: #B4A1FF;
+$darkSecondaryMain: #7B61FF;
+$darkSecondaryDark: #5A3FD6;
+$darkSecondary200: #D4C8FF;
+$darkSecondary800: #4526BF;
 
 // text variants
-$darkTextTitle: #d7dcec;
-$darkTextPrimary: #637381;
-$darkTextSecondary: #8492c4;
-$darkTextDark: #919EAB;
+$darkTextTitle: #F5F5F7;
+$darkTextPrimary: #FFFFFF;
+$darkTextSecondary: #D0D5DD;
+$darkTextDark: #E6E8EC;
 
 // ==============================|| JAVASCRIPT ||============================== //
 
diff --git a/web/src/hooks/use-popover.js b/web/src/hooks/use-popover.js
new file mode 100644
index 00000000..6899f7a3
--- /dev/null
+++ b/web/src/hooks/use-popover.js
@@ -0,0 +1,23 @@
+import { useState, useCallback } from 'react';
+
+// ----------------------------------------------------------------------
+
+export function usePopover() {
+  const [anchorEl, setAnchorEl] = useState(null);
+
+  const onOpen = useCallback((event) => {
+    setAnchorEl(event.currentTarget);
+  }, []);
+
+  const onClose = useCallback(() => {
+    setAnchorEl(null);
+  }, []);
+
+  return {
+    open: !!anchorEl,
+    anchorEl,
+    onOpen,
+    onClose,
+    setAnchorEl,
+  };
+}
diff --git a/web/src/i18n/locales/en_US.json b/web/src/i18n/locales/en_US.json
index 8f3a6b11..96b6d423 100644
--- a/web/src/i18n/locales/en_US.json
+++ b/web/src/i18n/locales/en_US.json
@@ -139,7 +139,8 @@
     "unselectAll": "Anti-select all",
     "updateEnabledBalance": "Update Enabled Balance",
     "usedBalance": "Used Balance/Remaining Balance",
-    "weight": "Weight"
+    "weight": "Weight",
+    "onlyTags": "Display tags only"
   },
   "channel_row": {
     "auto": "automatic",
@@ -165,7 +166,38 @@
     "updateChannelBalance": "All enabled channel balances have been updated!",
     "updateOk": "update completed!",
     "weightTip": "Weight cannot be less than 1",
-    "check": "Check"
+    "check": "Check",
+    "batchAddIDRequired": "Please select at least one channel.",
+    "batchDelete": "Batch delete",
+    "batchDeleteConfirm": "Are you sure you want to delete the selected {{count}} channels? This action cannot be undone.",
+    "batchDeleteError": "Batch deletion failed: {{message}}",
+    "batchDeleteErrorTip": "Batch deletion error: {{message}}",
+    "batchDeleteSuccess": "Batch deletion successful!",
+    "batchDeleteTip": "Do you want to delete the selected channels in batch?",
+    "deleteTag": "Delete tag",
+    "deleteTagAndChannels": "Delete the tag and all its channels.",
+    "deleteTagConfirm": "Are you sure you want to delete the tag {{tag}} and all its channels? This action cannot be undone.",
+    "deleteTagError": "Failed to delete tag: {{message}}",
+    "deleteTagSuccess": "Tag {{tag}} deleted successfully",
+    "disable": "Disable",
+    "disableAllChannels": "Disable all channels",
+    "disableTagChannels": "Disable tag channel",
+    "enable": "Enable",
+    "enableAllChannels": "Enable all channels",
+    "enableTagChannels": "Enable tag channel",
+    "getTagChannelsError": "Failed to retrieve tag channel: {{message}}",
+    "getTagChannelsErrorTip": "Error getting tag channel: {{message}}",
+    "noTagChannels": "No tag channel found",
+    "priorityUpdateError": "Priority update failed: {{message}}",
+    "priorityUpdateSuccess": "Priority updated successfully",
+    "refreshList": "Refresh list",
+    "tag": "Tags",
+    "tagChannelList": "Tag channel list",
+    "tagChannelsConfirm": "Are you sure you want to {{action}} all channels under the tag {{tag}}?",
+    "tagChannelsError": "Channel {{action}} failed: {{message}}",
+    "tagChannelsSuccess": "The tag channel {{action}} is successful.",
+    "weightUpdateError": "Weight update failed: {{message}}",
+    "weightUpdateSuccess": "Weight update successful"
   },
   "common": {
     "again": "Retry ({{count}})",
@@ -201,7 +233,9 @@
     "unableServer": "Unable to connect to the server properly!",
     "unableServerTip": "New version available: {{version}}, please use the shortcut key Shift F5 to refresh the page",
     "unknown": "unknown",
-    "verificationCode": "Verification code"
+    "verificationCode": "Verification code",
+    "deleteError": "Deletion failed: {{message}}",
+    "deleteSuccess": "Delete successful!"
   },
   "dashboard": "Dashboard",
   "dashboard_index": {
@@ -249,6 +283,9 @@
   "logPage": {
     "cachedTokens": "Cache Tokens",
     "channelLabel": "Channel",
+    "columnSettings": "Column Settings",
+    "selectColumns": "Select Columns",
+    "columnSelectAll": "Select All",
     "detailLabel": "Detail",
     "durationLabel": "Duration",
     "durationTooltip": "t/s: The number of output tokens divided by the total generation time, indicating the generation speed",
@@ -607,6 +644,10 @@
         "topUpLink": {
           "label": "Top-up Link",
           "placeholder": "For example, the purchase link of the card issuing website"
+        },
+        "retryTimeOut": {
+          "label": "Retry Timeout (seconds)",
+          "placeholder": "Retry Timeout (seconds)"
         }
       },
       "logSettings": {
@@ -1062,7 +1103,6 @@
   "使用代码执行功能，开启后，计算tokens不准确，建议个人使用开启": "When using the code execution function, the calculation of tokens is inaccurate after it is turned on. It is recommended that individuals use it to turn it on.",
   "使用网页搜索功能，对用户输入的内容进行搜索": "Use the web search function to search for content entered by the user",
   "其他参数": "Other parameters",
-  "单独设置代理地址，支持http和socks5，例如：http://127.0.0.1:1080": "Set the proxy address separately, support http and socks5, for example: http://127.0.0.1:1080",
   "可空，请输入中转API地址，例如通过cloudflare中转": "Can be left empty, please enter the transfer API address, for example, through cloudflare transfer",
   "启用": "enable",
   "地址填写Suno-API部署的地址": "Address: Fill in the address of Suno-API deployment",
@@ -1145,5 +1185,8 @@
   "price": "Price",
   "模型映射关系：例如用户请求A模型，实际转发给渠道的模型为B。在B模型加前缀+，表示使用传入模型计费，例如：+gpt-3": {
     "5-turbo": "Model mapping relationship: For example, when a user requests Model A, the actual model forwarded to the channel is Model B. Adding a prefix \"+\" to Model B indicates using the incoming model for billing purposes, for example: +gpt-3.5-turbo"
+  },
+  "单独设置代理地址，支持http和socks5，例如：http://127[0][0]": {
+    "1:1080,代理地址中可以通过 `%s` 作为会话标识占位符，程序中检测到有占位符会根据Key生成唯一会话标识符进行替换": "Set the proxy address separately, supporting both http and socks5, for example: http://127.0.0.1:1080. You can use `%s` as a session identifier placeholder in the proxy address. When a placeholder is detected in the program, it will be replaced with a unique session identifier generated based on the Key."
   }
 }
diff --git a/web/src/i18n/locales/ja_JP.json b/web/src/i18n/locales/ja_JP.json
index 724a490d..02a61667 100644
--- a/web/src/i18n/locales/ja_JP.json
+++ b/web/src/i18n/locales/ja_JP.json
@@ -139,7 +139,8 @@
     "unselectAll": "すべてを非選択にする",
     "updateEnabledBalance": "有効残高を更新",
     "usedBalance": "使用済み/残高",
-    "weight": "ウェイト"
+    "weight": "ウェイト",
+    "onlyTags": "タグのみ表示"
   },
   "channel_row": {
     "auto": "自動",
@@ -165,7 +166,38 @@
     "updateChannelBalance": "有効になっているすべてのチャンネルバランスが更新されました。",
     "updateOk": "アップデート完了！",
     "weightTip": "重みは 1 未満にはできません",
-    "check": "検出"
+    "check": "検出",
+    "batchAddIDRequired": "少なくとも1つのチャネルを選択してください",
+    "batchDelete": "一括削除",
+    "batchDeleteConfirm": "選択された {{count}} 個のチャネルを削除しますか？この操作は取り消せません。",
+    "batchDeleteError": "一括削除に失敗しました：{{message}}",
+    "batchDeleteErrorTip": "一括削除エラー：{{message}}",
+    "batchDeleteSuccess": "一括削除しました！",
+    "batchDeleteTip": "選択したチャネルを一括削除しますか？",
+    "deleteTag": "タグを削除します。",
+    "deleteTagAndChannels": "タグとそのすべてのチャネルを削除します。",
+    "deleteTagConfirm": "タグ {{tag}} およびそのすべてのチャネルを削除してもよろしいですか？この操作は取り消せません。",
+    "deleteTagError": "タグの削除に失敗しました：{{message}}",
+    "deleteTagSuccess": "タグ {{tag}} が削除されました",
+    "disable": "無効",
+    "disableAllChannels": "すべてのチャネルを無効にする",
+    "disableTagChannels": "タグチャネルを無効にする",
+    "enable": "有効",
+    "enableAllChannels": "すべてのチャネルを有効にする",
+    "enableTagChannels": "タグチャネルを有効にする",
+    "getTagChannelsError": "タグチャネルの取得に失敗しました：{{message}}",
+    "getTagChannelsErrorTip": "タグチャネルの取得中にエラーが発生しました：{{message}}",
+    "noTagChannels": "タグチャネルが見つかりません",
+    "priorityUpdateError": "優先度の更新に失敗しました：{{message}}",
+    "priorityUpdateSuccess": "優先度が更新されました",
+    "refreshList": "リストを更新する",
+    "tag": "タグ",
+    "tagChannelList": "タグチャネルリスト",
+    "tagChannelsConfirm": "すべてのチャネルで{{action}}タグ{{tag}}を確認しますか？",
+    "tagChannelsError": "アクション{{action}}のラベルチャネルが失敗しました: {{message}}",
+    "tagChannelsSuccess": "タグチャネル{{action}}が成功しました",
+    "weightUpdateError": "重みの更新に失敗しました：{{message}}",
+    "weightUpdateSuccess": "重みが更新されました"
   },
   "common": {
     "again": "再試行 ({{count}})",
@@ -201,7 +233,9 @@
     "unableServer": "サーバーに正しく接続できません!",
     "unableServerTip": "新しいバージョンが利用可能です: {{version}}、ショートカット キー Shift F5 を使用してページを更新してください",
     "unknown": "未知",
-    "verificationCode": "検証コード"
+    "verificationCode": "検証コード",
+    "deleteError": "削除に失敗しました：{{message}}",
+    "deleteSuccess": "削除しました！"
   },
   "dashboard": "ダッシュボード",
   "dashboard_index": {
@@ -249,6 +283,9 @@
   "logPage": {
     "cachedTokens": "Cache Tokens",
     "channelLabel": "チャネル",
+    "columnSettings": "列設定",
+    "selectColumns": "列を選択",
+    "columnSelectAll": "すべて選択",
     "detailLabel": "詳細",
     "durationLabel": "時間",
     "durationTooltip": "t/s: 出力トークンの数を総生成時間で割った値であり、生成速度を示します。",
@@ -607,6 +644,10 @@
         "topUpLink": {
           "label": "チャージリンク",
           "placeholder": "例えば、カード発行ウェブサイトの購入リンク"
+        },
+        "retryTimeOut": {
+          "label": "再試行のタイムアウト時間（秒）",
+          "placeholder": "再試行のタイムアウト時間（秒）"
         }
       },
       "logSettings": {
@@ -1062,7 +1103,6 @@
   "使用代码执行功能，开启后，计算tokens不准确，建议个人使用开启": "コード実行機能を使用する場合、オンにするとトークンの計算が不正確になるため、個人で使用することを推奨します。",
   "使用网页搜索功能，对用户输入的内容进行搜索": "Web検索機能を使用して、ユーザーが入力したコンテンツを検索します。",
   "其他参数": "その他のパラメータ",
-  "单独设置代理地址，支持http和socks5，例如：http://127.0.0.1:1080": "プロキシ アドレスを個別に設定し、http と Socks5 をサポートします。例: http://127.0.0.1:1080",
   "可空，请输入中转API地址，例如通过cloudflare中转": "空のままにすることもできます。たとえば、cloudflare 転送を通じて転送 API アドレスを入力してください。",
   "启用": "有効にする",
   "地址填写Suno-API部署的地址": "アドレス: Suno-API デプロイメントのアドレスを入力します。",
@@ -1145,5 +1185,8 @@
   "price": "価格",
   "模型映射关系：例如用户请求A模型，实际转发给渠道的模型为B。在B模型加前缀+，表示使用传入模型计费，例如：+gpt-3": {
     "5-turbo": "モデルマッピング関係：例えば、ユーザーがAモデルをリクエストした場合、実際にチャンネルに転送されるのはBモデルです。 Bモデルにプレフィックス+を付けて、入力されたモデルを使用して課金することを示します。 例：+gpt-3.5-turbo"
+  },
+  "单独设置代理地址，支持http和socks5，例如：http://127[0][0]": {
+    "1:1080,代理地址中可以通过 `%s` 作为会话标识占位符，程序中检测到有占位符会根据Key生成唯一会话标识符进行替换": "単独のプロキシアドレスを設定し、httpおよびsocks5をサポートします。例：http://127.0.0.1:1080。プロキシアドレスには`％s`をセッション識別子のプレースホルダとして使用できます。プログラムがプレースホルダを検出すると、Keyに基づいて一意のセッション識別子を生成して置換します。"
   }
 }
diff --git a/web/src/i18n/locales/zh_CN.json b/web/src/i18n/locales/zh_CN.json
index daee3328..8769b7c0 100644
--- a/web/src/i18n/locales/zh_CN.json
+++ b/web/src/i18n/locales/zh_CN.json
@@ -122,7 +122,7 @@
     "disabled": "禁用",
     "speedTestDisabled": "测速禁用",
     "filterTags": "过滤标签",
-    "showAll": "显示全部",
+    "onlyTags": "仅显示标签",
     "batchAzureAPISuccess": "成功更新 {{count}} 条数据",
     "inputAPIVersion": "请输入api版本号",
     "selectAll": "全选",
@@ -201,6 +201,9 @@
     "title": "日志",
     "refreshButton": "刷新/清除搜索条件",
     "searchButton": "搜索",
+    "columnSettings": "列设置",
+    "selectColumns": "选择列",
+    "columnSelectAll": "全选",
     "timeLabel": "时间",
     "channelLabel": "渠道",
     "groupLabel": "分组",
@@ -895,7 +898,9 @@
     "exhaust": "已耗尽",
     "execute": "是否执行 {{title}}?",
     "executeConfirm": "执行",
-    "deleteConfirm": "是否删除 {{title}}?"
+    "deleteConfirm": "是否删除 {{title}}?",
+    "deleteSuccess": "删除成功！",
+    "deleteError": "删除失败: {{message}}"
   },
   "channel_row": {
     "priorityTip": "优先级不能小于 0",
@@ -921,7 +926,38 @@
     "delChannelCount": "已删除所有禁用渠道，共计 {{count}} 个",
     "updateOk": "更新成功！",
     "delTagInfo1": "是否删除标签",
-    "delTagInfo2": "⚠️ 注意：该操作会删除渠道。"
+    "delTagInfo2": "⚠️ 注意：该操作会删除渠道。",
+    "batchDelete": "批量删除",
+    "batchDeleteTip": "是否批量删除所选渠道？",
+    "batchDeleteSuccess": "批量删除成功！",
+    "batchDeleteError": "批量删除失败: {{message}}",
+    "batchDeleteErrorTip": "批量删除出错: {{message}}",
+    "batchAddIDRequired": "请至少选择一个渠道",
+    "getTagChannelsError": "获取标签渠道失败: {{message}}",
+    "getTagChannelsErrorTip": "获取标签渠道出错: {{message}}",
+    "tag": "标签",
+    "enableAllChannels": "启用所有渠道",
+    "disableAllChannels": "禁用所有渠道",
+    "priorityUpdateSuccess": "优先级更新成功",
+    "priorityUpdateError": "优先级更新失败: {{message}}",
+    "weightUpdateSuccess": "权重更新成功",
+    "weightUpdateError": "权重更新失败: {{message}}",
+    "deleteTagAndChannels": "删除标签及其所有渠道",
+    "tagChannelList": "标签渠道列表",
+    "refreshList": "刷新列表",
+    "noTagChannels": "没有找到标签渠道",
+    "deleteTag": "删除标签",
+    "deleteTagConfirm": "确定要删除标签 {{tag}} 及其所有渠道吗？此操作不可恢复。",
+    "deleteTagSuccess": "标签 {{tag}} 删除成功",
+    "deleteTagError": "删除标签失败: {{message}}",
+    "enableTagChannels": "启用标签渠道",
+    "disableTagChannels": "禁用标签渠道",
+    "tagChannelsConfirm": "确定要{{action}}标签 {{tag}} 下的所有渠道吗？",
+    "enable": "启用",
+    "disable": "禁用",
+    "tagChannelsSuccess": "标签渠道{{action}}成功",
+    "tagChannelsError": "标签渠道{{action}}失败: {{message}}",
+    "batchDeleteConfirm": "确定要删除选中的 {{count}} 个渠道吗？此操作不可恢复。"
   },
   "validation": {
     "requiredName": "名称 不能为空"
@@ -1027,7 +1063,7 @@
   "请为渠道命名": "请为渠道命名",
   "可空，请输入中转API地址，例如通过cloudflare中转": "可空，请输入中转API地址，例如通过cloudflare中转",
   "请输入渠道对应的鉴权密钥": "请输入渠道对应的鉴权密钥",
-  "单独设置代理地址，支持http和socks5，例如：http://127.0.0.1:1080": "单独设置代理地址，支持http和socks5，例如：http://127.0.0.1:1080",
+  "单独设置代理地址，支持http和socks5，例如：http://127.0.0.1:1080,代理地址中可以通过 `%s` 作为会话标识占位符，程序中检测到有占位符会根据Key生成唯一会话标识符进行替换": "单独设置代理地址，支持http和socks5，例如：http://127.0.0.1:1080,代理地址中可以通过 `%s` 作为会话标识占位符，程序中检测到有占位符会根据Key生成唯一会话标识符进行替换",
   "用于测试使用的模型，为空时无法测速,如：gpt-3.5-turbo，仅支持chat模型": "用于测试使用的模型，为空时无法测速,如：gpt-3.5-turbo，仅支持chat模型",
   "请选择该渠道所支持的模型,你也可以输入通配符*来匹配模型，例如：gpt-3.5*，表示支持所有gpt-3.5开头的模型，*号只能在最后一位使用，前面必须有字符，例如：gpt-3.5*是正确的，*gpt-3.5是错误的": "请选择该渠道所支持的模型,你也可以输入通配符*来匹配模型，例如：gpt-3.5*，表示支持所有gpt-3.5开头的模型，*号只能在最后一位使用，前面必须有字符，例如：gpt-3.5*是正确的，*gpt-3.5是错误的",
   "模型映射关系：例如用户请求A模型，实际转发给渠道的模型为B。在B模型加前缀+，表示使用传入模型计费，例如：+gpt-3.5-turbo": "模型映射关系：例如用户请求A模型，实际转发给渠道的模型为B。在B模型加前缀+，表示使用传入模型计费，例如：+gpt-3.5-turbo",
diff --git a/web/src/i18n/locales/zh_HK.json b/web/src/i18n/locales/zh_HK.json
index be066c3c..17efa286 100644
--- a/web/src/i18n/locales/zh_HK.json
+++ b/web/src/i18n/locales/zh_HK.json
@@ -140,7 +140,8 @@
     "unselectAll": "反全選",
     "updateEnabledBalance": "更新啟用餘額",
     "usedBalance": "已使用/餘額",
-    "weight": "權重"
+    "weight": "權重",
+    "onlyTags": "僅顯示標籤"
   },
   "channel_row": {
     "auto": "自動",
@@ -166,7 +167,38 @@
     "updateChannelBalance": "已更新完畢所有已啟用通道餘額！",
     "updateOk": "更新成功！",
     "weightTip": "權重不能小於 1",
-    "check": "檢測"
+    "check": "檢測",
+    "batchAddIDRequired": "請至少選擇一個渠道",
+    "batchDelete": "批量刪除",
+    "batchDeleteConfirm": "確定要刪除選中的 {{count}} 個渠道嗎？此操作將無法復原。",
+    "batchDeleteError": "批量刪除失敗: {{message}}",
+    "batchDeleteErrorTip": "批量刪除出錯: {{message}}",
+    "batchDeleteSuccess": "批量刪除成功！",
+    "batchDeleteTip": "是否批量刪除所選渠道？",
+    "deleteTag": "刪除標籤",
+    "deleteTagAndChannels": "刪除標籤及其所有渠道",
+    "deleteTagConfirm": "確定要刪除標籤 {{tag}} 及其所有頻道嗎？此操作將無法恢復。",
+    "deleteTagError": "刪除標籤失敗: {{message}}",
+    "deleteTagSuccess": "標籤 {{tag}} 刪除成功",
+    "disable": "停用",
+    "disableAllChannels": "停用所有渠道",
+    "disableTagChannels": "停用標籤渠道",
+    "enable": "啟用",
+    "enableAllChannels": "啟用所有渠道",
+    "enableTagChannels": "啟用標籤渠道",
+    "getTagChannelsError": "獲取標籤渠道失敗: {{message}}",
+    "getTagChannelsErrorTip": "獲取標籤渠道出錯: {{message}}",
+    "noTagChannels": "未能找到標籤渠道",
+    "priorityUpdateError": "優先級更新失敗: {{message}}",
+    "priorityUpdateSuccess": "優先級更新成功",
+    "refreshList": "更新列表",
+    "tag": "標籤",
+    "tagChannelList": "標籤渠道列表",
+    "tagChannelsConfirm": "確定要{{action}}標籤 {{tag}} 下的所有渠道嗎？",
+    "tagChannelsError": "標籤渠道{{action}}失敗: {{message}}",
+    "tagChannelsSuccess": "標籤渠道{{action}}成功",
+    "weightUpdateError": "權重更新失敗: {{message}}",
+    "weightUpdateSuccess": "權重更新成功"
   },
   "common": {
     "again": "重試 ({{count}})",
@@ -202,7 +234,9 @@
     "unableServer": "無法正常連接至伺服器！",
     "unableServerTip": "新版本可用：{{version}}，請使用快捷鍵 Shift + F5 刷新頁面",
     "unknown": "未知",
-    "verificationCode": "驗證碼"
+    "verificationCode": "驗證碼",
+    "deleteError": "刪除失敗：{{message}}",
+    "deleteSuccess": "刪除成功！"
   },
   "dashboard": "儀表板",
   "dashboard_index": {
@@ -250,6 +284,9 @@
   "logPage": {
     "cachedTokens": "緩存Tokens",
     "channelLabel": "渠道",
+    "columnSettings": "列設置",
+    "selectColumns": "選擇列",
+    "columnSelectAll": "全選",
     "detailLabel": "詳情",
     "durationLabel": "耗時",
     "durationTooltip": "t/s：輸出Token的數量除以總生成時間，表示生成速度",
@@ -284,7 +321,7 @@
       "original_times_price": "原價格: ${{ times }} / 次",
       "output_price": "輸出: ${{ price }} /M",
       "times_price": "${{ times }} / 次",
-      "free":"免費",
+      "free": "免費",
       "old_log": "舊版log",
       "illustrate": "* 本筆記錄為系統升級前所記錄的log"
     },
@@ -607,6 +644,10 @@
         "topUpLink": {
           "label": "充值鏈接",
           "placeholder": "例如發卡網站的購買鏈接"
+        },
+        "retryTimeOut": {
+          "label": "重試超時時間(秒)",
+          "placeholder": "重試超時時間(秒)"
         }
       },
       "logSettings": {
@@ -1062,7 +1103,6 @@
   "使用代码执行功能，开启后，计算tokens不准确，建议个人使用开启": "使用代碼執行功能，開啟後，計算tokens不準確，建議個人使用開啟",
   "使用网页搜索功能，对用户输入的内容进行搜索": "使用網頁搜索功能，對用戶輸入的內容進行搜索",
   "其他参数": "其他參數",
-  "单独设置代理地址，支持http和socks5，例如：http://127.0.0.1:1080": "單獨設置代理地址，支持http和socks5，例如：http://127.0.0.1:1080",
   "可空，请输入中转API地址，例如通过cloudflare中转": "可空，請輸入中轉API地址，例如通過cloudflare中轉",
   "启用": "啟用",
   "地址填写Suno-API部署的地址": "地址填寫Suno-API部署的地址",
@@ -1145,5 +1185,8 @@
   "price": "價格",
   "模型映射关系：例如用户请求A模型，实际转发给渠道的模型为B。在B模型加前缀+，表示使用传入模型计费，例如：+gpt-3": {
     "5-turbo": "模型映射關係：例如用戶請求A模型，實際轉發給渠道的模型為B。在B模型加前綴+，表示使用傳入模型計費，例如：+gpt-3.5-turbo"
+  },
+  "单独设置代理地址，支持http和socks5，例如：http://127[0][0]": {
+    "1:1080,代理地址中可以通过 `%s` 作为会话标识占位符，程序中检测到有占位符会根据Key生成唯一会话标识符进行替换": "單獨設置代理地址，支持http和socks5，例如：http://127.0.0.1:1080,代理地址中可以通過 `%s` 作為會話標識佔位符，程式中檢測到有占位符會根據Key生成唯一會話標識符進行替換"
   }
 }
diff --git a/web/src/layout/MainLayout/Header/ProfileSection/index.jsx b/web/src/layout/MainLayout/Header/ProfileSection/index.jsx
index 1b5f71bf..46dceb79 100644
--- a/web/src/layout/MainLayout/Header/ProfileSection/index.jsx
+++ b/web/src/layout/MainLayout/Header/ProfileSection/index.jsx
@@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom';
 import { useTheme } from '@mui/material/styles';
 import {
   Avatar,
-  Chip,
+  Box,
   ClickAwayListener,
   List,
   ListItemButton,
@@ -14,7 +14,9 @@ import {
   ListItemText,
   Paper,
   Popper,
-  Typography
+  Typography,
+  useMediaQuery,
+  Drawer
 } from '@mui/material';
 
 // project imports
@@ -37,6 +39,14 @@ const ProfileSection = () => {
   const customization = useSelector((state) => state.customization);
   const account = useSelector((state) => state.account);
   const { logout } = useLogin();
+  const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
+
+  console.log('Current device is mobile:', isMobile);
+
+  // 调试宽度
+  useEffect(() => {
+    console.log('Window width:', window.innerWidth, 'isMobile:', isMobile);
+  }, [isMobile]);
 
   const [open, setOpen] = useState(false);
   /**
@@ -55,118 +65,147 @@ const ProfileSection = () => {
   };
 
   const handleToggle = () => {
-    setOpen((prevOpen) => !prevOpen);
+    console.log('Toggle clicked - current menu state:', open);
+    setOpen(!open);
   };
 
-  const prevOpen = useRef(open);
-  useEffect(() => {
-    if (prevOpen.current === true && open === false) {
-      anchorRef.current.focus();
-    }
+  // 渲染移动端菜单内容
+  const renderMobileMenuContent = () => (
+    <Box sx={{ p: 0 }}>
+      <List>
+        <ListItemButton
+          onClick={() => {
+            navigate('/panel/profile');
+            setOpen(false);
+          }}
+        >
+          <ListItemIcon>
+            <Icon icon="solar:user-id-bold-duotone" width="1.5rem" />
+          </ListItemIcon>
+          <ListItemText primary={<Typography variant="body1">{t('setting')}</Typography>} />
+        </ListItemButton>
 
-    prevOpen.current = open;
-  }, [open]);
+        <ListItemButton
+          onClick={() => {
+            handleLogout();
+            setOpen(false);
+          }}
+        >
+          <ListItemIcon>
+            <Icon icon="solar:logout-3-bold-duotone" width="1.5rem" />
+          </ListItemIcon>
+          <ListItemText primary={<Typography variant="body1">{t('menu.signout')}</Typography>} />
+        </ListItemButton>
+      </List>
+    </Box>
+  );
 
-  return (
-    <>
-      <Chip
-        sx={{
-          height: '48px',
-          alignItems: 'center',
-          borderRadius: '27px',
-          transition: 'all .2s ease-in-out',
-          backgroundColor: 'transparent',
-          border: 'none',
-          '&[aria-controls="menu-list-grow"], &:hover': {
-            boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
-            borderRadius: '50%',
-            '& svg': {
-              stroke: theme.palette.primary.light
+  // 渲染桌面端菜单
+  const renderDesktopMenu = () => (
+    <Popper
+      placement="bottom-end"
+      open={open}
+      anchorEl={anchorRef.current}
+      role={undefined}
+      transition
+      disablePortal
+      popperOptions={{
+        modifiers: [
+          {
+            name: 'offset',
+            options: {
+              offset: [0, 14]
             }
-          },
-          '& .MuiChip-label': {
-            display: 'none'
           }
-        }}
-        icon={
-          <Avatar
-            src={account.user?.avatar_url || User1}
-            sx={{
-              ...theme.typography.mediumAvatar,
-              margin: '8px !important',
-              cursor: 'pointer'
-            }}
-            ref={anchorRef}
-            aria-controls={open ? 'menu-list-grow' : undefined}
-            aria-haspopup="true"
-            color="inherit"
-          />
-        }
-        ref={anchorRef}
-        aria-controls={open ? 'menu-list-grow' : undefined}
-        aria-haspopup="true"
-        onClick={handleToggle}
-        color="primary"
-      />
-      <Popper
-        placement="bottom-end"
-        open={open}
-        anchorEl={anchorRef.current}
-        role={undefined}
-        transition
-        disablePortal
-        popperOptions={{
-          modifiers: [
-            {
-              name: 'offset',
-              options: {
-                offset: [0, 14]
-              }
-            }
-          ]
-        }}
-      >
-        {({ TransitionProps }) => (
-          <Transitions in={open} {...TransitionProps}>
-            <Paper>
-              <ClickAwayListener onClickAway={handleClose}>
-                <MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
-                  <List
-                    component="nav"
-                    sx={{
-                      width: '100%',
-                      maxWidth: 350,
-                      minWidth: 150,
-                      backgroundColor: theme.palette.background.paper,
-                      borderRadius: '10px',
-                      [theme.breakpoints.down('md')]: {
-                        minWidth: '100%'
-                      },
-                      '& .MuiListItemButton-root': {
-                        mt: 0.5
-                      }
+        ]
+      }}
+      sx={{ zIndex: 1600 }}
+    >
+      {({ TransitionProps }) => (
+        <Transitions type="grow" position="top-right" in={open} {...TransitionProps}>
+          <Paper sx={{ width: 'auto', maxWidth: 350, overflow: 'hidden', borderRadius: '10px' }}>
+            <ClickAwayListener onClickAway={handleClose}>
+              <MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
+                <List
+                  component="nav"
+                  sx={{
+                    width: '100%',
+                    bgcolor: 'background.paper',
+                    py: 1,
+                    '& .MuiListItemButton-root': {
+                      mt: 0.5,
+                      py: 0.5,
+                      px: 2,
+                      borderRadius: `${customization.borderRadius}px`
+                    }
+                  }}
+                >
+                  <ListItemButton
+                    onClick={() => {
+                      navigate('/panel/profile');
+                      setOpen(false);
                     }}
                   >
-                    <ListItemButton sx={{ borderRadius: `${customization.borderRadius}px` }} onClick={() => navigate('/panel/profile')}>
-                      <ListItemIcon>
-                        <Icon icon="solar:user-id-bold-duotone" width="1.3rem" />
-                      </ListItemIcon>
-                      <ListItemText primary={<Typography variant="body2">{t('setting')}</Typography>} />
-                    </ListItemButton>
-
-                    <ListItemButton sx={{ borderRadius: `${customization.borderRadius}px` }} onClick={handleLogout}>
-                      <ListItemIcon>
-                        <Icon icon="solar:logout-3-bold-duotone" width="1.3rem" />
-                      </ListItemIcon>
-                      <ListItemText primary={<Typography variant="body2">{t('menu.signout')}</Typography>} />
-                    </ListItemButton>
-                  </List>
-                </MainCard>
-              </ClickAwayListener>
-            </Paper>
-          </Transitions>
-        )}
-      </Popper>
+                    <ListItemIcon>
+                      <Icon icon="solar:user-id-bold-duotone" width="1.3rem" />
+                    </ListItemIcon>
+                    <ListItemText primary={<Typography variant="body2">{t('setting')}</Typography>} />
+                  </ListItemButton>
+
+                  <ListItemButton onClick={handleLogout}>
+                    <ListItemIcon>
+                      <Icon icon="solar:logout-3-bold-duotone" width="1.3rem" />
+                    </ListItemIcon>
+                    <ListItemText primary={<Typography variant="body2">{t('menu.signout')}</Typography>} />
+                  </ListItemButton>
+                </List>
+              </MainCard>
+            </ClickAwayListener>
+          </Paper>
+        </Transitions>
+      )}
+    </Popper>
+  );
+
+  return (
+    <>
+      {/* 用户头像按钮 */}
+      <Box component="div" onClick={handleToggle} sx={{ cursor: 'pointer' }}>
+        <Avatar
+          src={account.user?.avatar_url || User1}
+          sx={{
+            ...theme.typography.mediumAvatar,
+            cursor: 'pointer',
+            border: `2px solid ${theme.palette.primary.light}`
+          }}
+          ref={anchorRef}
+        />
+      </Box>
+
+      {/* 调试信息 */}
+      <Box sx={{ display: 'none' }}>
+        Device: {isMobile ? 'Mobile' : 'Desktop'}, Menu: {open ? 'Open' : 'Closed'}
+      </Box>
+
+      {/* 移动端使用Drawer */}
+      {isMobile ? (
+        <Drawer
+          anchor="bottom"
+          open={open}
+          onClose={handleClose}
+          PaperProps={{
+            sx: {
+              borderTopLeftRadius: 16,
+              borderTopRightRadius: 16
+            }
+          }}
+          sx={{ zIndex: 1600 }}
+        >
+          {renderMobileMenuContent()}
+        </Drawer>
+      ) : (
+        renderDesktopMenu()
+      )}
     </>
   );
 };
diff --git a/web/src/layout/MainLayout/Header/index.jsx b/web/src/layout/MainLayout/Header/index.jsx
index 6b11a345..48e7dedd 100644
--- a/web/src/layout/MainLayout/Header/index.jsx
+++ b/web/src/layout/MainLayout/Header/index.jsx
@@ -4,7 +4,7 @@ import { Icon } from '@iconify/react';
 
 // material-ui
 import { useTheme } from '@mui/material/styles';
-import { Box, ButtonBase } from '@mui/material';
+import { Box, IconButton, Stack } from '@mui/material';
 
 // project imports
 import LogoSection from '../LogoSection';
@@ -29,6 +29,7 @@ const Header = ({ handleLeftDrawerToggle }) => {
         sx={{
           width: 228,
           display: 'flex',
+          alignItems: 'center',
           [theme.breakpoints.down('md')]: {
             width: 'auto'
           }
@@ -37,18 +38,20 @@ const Header = ({ handleLeftDrawerToggle }) => {
         <Box component="span" sx={{ display: { xs: 'none', md: 'block' }, flexGrow: 1 }}>
           <LogoSection />
         </Box>
-        <ButtonBase
+        <IconButton
+          size="medium"
+          edge="start"
+          color="inherit"
+          aria-label="menu"
           sx={{
-            width: '36px',
-            height: '36px',
-            borderRadius: '50%',
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'center',
-            transition: 'all 0.2s ease-in-out',
+            width: '38px',
+            height: '38px',
+            borderRadius: '8px',
+            backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.03)',
             '&:hover': {
-              backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.04)'
-            }
+              backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.06)'
+            },
+            transition: 'background-color 0.2s ease-in-out'
           }}
           onClick={() => {
             setIsDrawerOpen(!isDrawerOpen);
@@ -56,23 +59,23 @@ const Header = ({ handleLeftDrawerToggle }) => {
           }}
         >
           <Icon
-            icon={isDrawerOpen ? 'material-symbols:menu-open' : 'material-symbols:menu'}
-            width="20px"
-            color={theme.palette.mode === 'dark' ? '#ffffff' : '#000000'}
-            style={{
-              opacity: theme.palette.mode === 'dark' ? 0.9 : 0.7,
-              transition: 'transform 0.2s ease-in-out'
-            }}
+            icon={isDrawerOpen ? 'tabler:layout-sidebar-right-collapse' : 'tabler:layout-sidebar-left-expand'}
+            width="22px"
+            height="22px"
+            color={theme.palette.mode === 'dark' ? theme.palette.text.secondary : theme.palette.text.primary}
           />
-        </ButtonBase>
+        </IconButton>
       </Box>
 
       <Box sx={{ flexGrow: 1 }} />
-      <Box sx={{ flexGrow: 1 }} />
-      <NoticeButton />
-      <ThemeButton />
-      <I18nButton />
-      <ProfileSection />
+
+      {/* 右侧功能按钮区 */}
+      <Stack direction="row" spacing={1} alignItems="center">
+        <NoticeButton />
+        <ThemeButton />
+        <I18nButton />
+        <ProfileSection />
+      </Stack>
     </>
   );
 };
diff --git a/web/src/layout/MainLayout/LogoSection/index.jsx b/web/src/layout/MainLayout/LogoSection/index.jsx
index e9ad78a5..fd9e0a06 100644
--- a/web/src/layout/MainLayout/LogoSection/index.jsx
+++ b/web/src/layout/MainLayout/LogoSection/index.jsx
@@ -2,7 +2,7 @@ import { Link } from 'react-router-dom';
 import { useDispatch, useSelector } from 'react-redux';
 
 // material-ui
-import { ButtonBase } from '@mui/material';
+import { ButtonBase, Box } from '@mui/material';
 
 // project imports
 import config from 'config';
@@ -14,9 +14,23 @@ import { MENU_OPEN } from 'store/actions';
 const LogoSection = () => {
   const defaultId = useSelector((state) => state.customization.defaultId);
   const dispatch = useDispatch();
+
   return (
-    <ButtonBase disableRipple onClick={() => dispatch({ type: MENU_OPEN, id: defaultId })} component={Link} to={config.basename}>
-      <Logo />
+    <ButtonBase
+      disableRipple
+      onClick={() => dispatch({ type: MENU_OPEN, id: defaultId })}
+      component={Link}
+      to={config.basename}
+      sx={{
+        transition: 'all 0.2s ease-in-out',
+        '&:hover': {
+          opacity: 0.9
+        }
+      }}
+    >
+      <Box sx={{ display: 'flex', alignItems: 'center' }}>
+        <Logo />
+      </Box>
     </ButtonBase>
   );
 };
diff --git a/web/src/layout/MainLayout/Sidebar/MenuCard/index.jsx b/web/src/layout/MainLayout/Sidebar/MenuCard/index.jsx
index 51eeaf58..967a34b2 100644
--- a/web/src/layout/MainLayout/Sidebar/MenuCard/index.jsx
+++ b/web/src/layout/MainLayout/Sidebar/MenuCard/index.jsx
@@ -93,7 +93,7 @@ const MenuCard = () => {
 
   return (
     <CardStyle>
-      <CardContent sx={{ p: 2 }}>
+      <CardContent sx={{ p: 2, pb: '8px !important' }}>
         <List sx={{ p: 0, m: 0 }}>
           <ListItem alignItems="flex-start" disableGutters sx={{ p: 0 }}>
             <ListItemAvatar sx={{ mt: 0 }}>
diff --git a/web/src/layout/MainLayout/Sidebar/MenuList/NavCollapse/index.jsx b/web/src/layout/MainLayout/Sidebar/MenuList/NavCollapse/index.jsx
index 0632d56f..750e7fdf 100644
--- a/web/src/layout/MainLayout/Sidebar/MenuList/NavCollapse/index.jsx
+++ b/web/src/layout/MainLayout/Sidebar/MenuList/NavCollapse/index.jsx
@@ -1,7 +1,7 @@
 import PropTypes from 'prop-types';
 import { useEffect, useState } from 'react';
 import { useSelector } from 'react-redux';
-import { useLocation, useNavigate } from 'react-router';
+import { useLocation } from 'react-router';
 
 // material-ui
 import { useTheme } from '@mui/material/styles';
@@ -19,7 +19,6 @@ import { IconChevronDown, IconChevronUp } from '@tabler/icons-react';
 const NavCollapse = ({ menu, level }) => {
   const theme = useTheme();
   const customization = useSelector((state) => state.customization);
-  const navigate = useNavigate();
 
   const [open, setOpen] = useState(false);
   const [selected, setSelected] = useState(null);
@@ -27,9 +26,16 @@ const NavCollapse = ({ menu, level }) => {
   const handleClick = () => {
     setOpen(!open);
     setSelected(!selected ? menu.id : null);
-    if (menu?.id !== 'authentication') {
-      navigate(menu.children[0]?.url);
-    }
+    // 触发一个小延迟后的滚动容器重新计算
+    setTimeout(() => {
+      // 触发窗口的resize事件，让PerfectScrollbar重新计算
+      window.dispatchEvent(new Event('resize'));
+      // 找到当前滚动容器并进行滚动更新
+      const scrollContainer = document.querySelector('.ps--active-y');
+      if (scrollContainer?.ps) {
+        scrollContainer.ps.update();
+      }
+    }, 300); // 等待折叠动画完成
   };
 
   const { pathname } = useLocation();
@@ -77,9 +83,9 @@ const NavCollapse = ({ menu, level }) => {
     }
   });
 
-  const Icon = menu.icon;
+  const IconComponent = menu.icon;
   const menuIcon = menu.icon ? (
-    <Icon strokeWidth={1.5} size="1.3rem" style={{ marginTop: 'auto', marginBottom: 'auto' }} />
+    <IconComponent strokeWidth={1.5} size="1.3rem" style={{ marginTop: 'auto', marginBottom: 'auto' }} />
   ) : (
     <FiberManualRecordIcon
       sx={{
@@ -98,7 +104,7 @@ const NavCollapse = ({ menu, level }) => {
           mb: 0.5,
           alignItems: 'flex-start',
           backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
-          py: level > 1 ? 1 : 1.25,
+          py: level > 1 ? 0.6 : 0.75,
           pl: `${level * 24}px`
         }}
         selected={selected === menu.id}
@@ -120,12 +126,54 @@ const NavCollapse = ({ menu, level }) => {
           }
         />
         {open ? (
-          <IconChevronUp stroke={1.5} size="1rem" style={{ marginTop: 'auto', marginBottom: 'auto' }} />
+          <IconChevronUp
+            stroke={1.5}
+            size="1rem"
+            style={{
+              marginTop: 'auto',
+              marginBottom: 'auto',
+              color: theme.palette.primary.main
+            }}
+          />
         ) : (
-          <IconChevronDown stroke={1.5} size="1rem" style={{ marginTop: 'auto', marginBottom: 'auto' }} />
+          <IconChevronDown
+            stroke={1.5}
+            size="1rem"
+            style={{
+              marginTop: 'auto',
+              marginBottom: 'auto',
+              color: theme.palette.primary.main
+            }}
+          />
         )}
       </ListItemButton>
-      <Collapse in={open} timeout="auto" unmountOnExit>
+      <Collapse
+        in={open}
+        timeout="auto"
+        unmountOnExit
+        onEntered={() => {
+          window.dispatchEvent(new Event('resize'));
+          // 找到当前滚动容器并立即更新
+          const scrollContainer = document.querySelector('.ps--active-y');
+          if (scrollContainer?.ps) {
+            scrollContainer.ps.update();
+          }
+        }}
+        onExited={() => {
+          window.dispatchEvent(new Event('resize'));
+          // 找到当前滚动容器并立即更新
+          const scrollContainer = document.querySelector('.ps--active-y');
+          if (scrollContainer?.ps) {
+            scrollContainer.ps.update();
+          }
+          // 触发第二次更新以确保所有变化都被捕获
+          setTimeout(() => {
+            if (scrollContainer?.ps) {
+              scrollContainer.ps.update();
+            }
+          }, 100);
+        }}
+      >
         <List
           component="div"
           disablePadding
diff --git a/web/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx b/web/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx
index ddce9cf4..88777c67 100644
--- a/web/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx
+++ b/web/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx
@@ -73,7 +73,7 @@ const NavItem = ({ item, level }) => {
         mb: 0.5,
         alignItems: 'flex-start',
         backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
-        py: level > 1 ? 1 : 1.25,
+        py: level > 1 ? 0.6 : 0.75,
         pl: `${level * 24}px`
       }}
       selected={customization.isOpen.findIndex((id) => id === item.id) > -1}
diff --git a/web/src/layout/MainLayout/Sidebar/index.jsx b/web/src/layout/MainLayout/Sidebar/index.jsx
index 2b7d919d..203510bd 100644
--- a/web/src/layout/MainLayout/Sidebar/index.jsx
+++ b/web/src/layout/MainLayout/Sidebar/index.jsx
@@ -1,8 +1,10 @@
 import PropTypes from 'prop-types';
+import { useEffect } from 'react';
 
 // material-ui
 import { useTheme } from '@mui/material/styles';
 import { Box, Chip, Drawer, Stack, useMediaQuery } from '@mui/material';
+import { useSelector } from 'react-redux';
 
 // third-party
 import PerfectScrollbar from 'react-perfect-scrollbar';
@@ -21,56 +23,186 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
   const theme = useTheme();
   const matchUpMd = useMediaQuery(theme.breakpoints.up('md'));
   const { t } = useTranslation();
+  const customization = useSelector((state) => state.customization);
+
+  // 监听菜单打开状态变化
+  useEffect(() => {
+    // 菜单状态变化时，触发容器更新
+    const updatePerfectScrollbar = () => {
+      setTimeout(() => {
+        window.dispatchEvent(new Event('resize'));
+      }, 300);
+    };
+    updatePerfectScrollbar();
+  }, [customization.isOpen]);
+
+  // 处理滚动事件，阻止冒泡
+  const handleScroll = (e) => {
+    e.stopPropagation();
+  };
 
   const drawer = (
     <>
-      <Box sx={{ display: { xs: 'block', md: 'none' } }}>
-        <Box sx={{ display: 'flex', p: 2, mx: 'auto' }}>
-          <LogoSection />
-        </Box>
-      </Box>
       <BrowserView>
-        <PerfectScrollbar
-          component="div"
-          style={{
-            height: !matchUpMd ? 'calc(100vh - 56px)' : 'calc(100vh - 88px)',
-            paddingLeft: '16px',
-            paddingRight: '16px'
+        <Box
+          onWheel={handleScroll}
+          onTouchMove={handleScroll}
+          sx={{
+            height: !matchUpMd ? 'calc(100vh - 56px)' : 'calc(100vh - 64px)',
+            position: 'relative'
           }}
         >
-          <MenuList />
-          <MenuCard />
-          <Stack direction="row" justifyContent="center" sx={{ mb: 2 }}>
-            <Chip
-              label={import.meta.env.VITE_APP_VERSION || t('menu.unknownVersion')}
-              disabled
-              chipcolor="secondary"
-              size="small"
-              sx={{ cursor: 'pointer' }}
-            />
-          </Stack>
-        </PerfectScrollbar>
+          <PerfectScrollbar
+            component="div"
+            key={`perfect-scrollbar-${customization.isOpen.length}`}
+            options={{
+              wheelPropagation: false,
+              suppressScrollX: true,
+              minScrollbarLength: 40,
+              maxScrollbarLength: 100,
+              updateOnWindowResize: true
+            }}
+            style={{
+              height: '100%',
+              paddingLeft: '16px',
+              paddingRight: '16px',
+              paddingTop: '8px',
+              paddingBottom: '16px'
+            }}
+            containerRef={(ref) => {
+              if (ref) {
+                // 在组件挂载和更新时添加resize监听
+                const resizeObserver = new ResizeObserver(() => {
+                  // 当内容大小变化时，通知PerfectScrollbar更新
+                  if (ref.ps) ref.ps.update();
+                });
+                resizeObserver.observe(ref);
+                // 保存引用以便在组件卸载时清理
+                ref._resizeObserver = resizeObserver;
+              }
+            }}
+          >
+            <MenuCard />
+            <MenuList />
+
+            <Box
+              sx={{
+                pt: 2,
+                pb: 2,
+                mt: 2,
+                borderTop: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'}`
+              }}
+            >
+              <Stack direction="row" justifyContent="center">
+                <Chip
+                  label={import.meta.env.VITE_APP_VERSION || t('menu.unknownVersion')}
+                  disabled
+                  chipcolor="secondary"
+                  size="small"
+                  sx={{
+                    cursor: 'pointer',
+                    backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)',
+                    color: theme.palette.text.secondary,
+                    fontSize: '0.75rem',
+                    height: '24px',
+                    '& .MuiChip-label': {
+                      px: 1.5
+                    }
+                  }}
+                />
+              </Stack>
+            </Box>
+          </PerfectScrollbar>
+        </Box>
       </BrowserView>
       <MobileView>
-        <Box sx={{ px: 2 }}>
-          <MenuList />
-          <MenuCard />
-          <Stack direction="row" justifyContent="center" sx={{ mb: 2 }}>
-            <Chip
-              label={import.meta.env.VITE_APP_VERSION || t('menu.unknownVersion')}
-              disabled
-              chipcolor="secondary"
-              size="small"
-              sx={{ cursor: 'pointer' }}
-            />
-          </Stack>
+        <Box
+          sx={{
+            height: '100vh',
+            position: 'relative'
+          }}
+        >
+          <Box
+            component="div"
+            onWheel={handleScroll}
+            onTouchMove={handleScroll}
+            key={`mobile-view-${customization.isOpen.length}`}
+            sx={{
+              height: '100%',
+              overflowY: 'auto',
+              WebkitOverflowScrolling: 'touch',
+              scrollbarWidth: 'thin',
+              display: 'flex',
+              flexDirection: 'column',
+              '&::-webkit-scrollbar': {
+                width: '4px'
+              },
+              '&::-webkit-scrollbar-thumb': {
+                background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.15)',
+                borderRadius: '4px'
+              }
+            }}
+          >
+            {!matchUpMd && (
+              <Box
+                sx={{
+                  px: 2,
+                  pt: 2.5,
+                  pb: 1.5,
+                  mt: 0.5,
+                  display: 'flex',
+                  justifyContent: 'center'
+                }}
+              >
+                <LogoSection />
+              </Box>
+            )}
+
+            <Box
+              sx={{
+                px: 2,
+                pt: 1,
+                pb: 2,
+                flex: 1
+              }}
+            >
+              <MenuCard />
+              <MenuList />
+
+              <Box
+                sx={{
+                  pt: 2,
+                  pb: 2,
+                  mt: 2,
+                  borderTop: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'}`
+                }}
+              >
+                <Stack direction="row" justifyContent="center">
+                  <Chip
+                    label={import.meta.env.VITE_APP_VERSION || t('menu.unknownVersion')}
+                    disabled
+                    chipcolor="secondary"
+                    size="small"
+                    sx={{
+                      cursor: 'pointer',
+                      backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)',
+                      color: theme.palette.text.secondary,
+                      fontSize: '0.75rem',
+                      height: '24px',
+                      '& .MuiChip-label': {
+                        px: 1.5
+                      }
+                    }}
+                  />
+                </Stack>
+              </Box>
+            </Box>
+          </Box>
         </Box>
       </MobileView>
     </>
   );
 
-  const container = window !== undefined ? () => window.document.body : undefined;
-
   return (
     <Box
       component="nav"
@@ -81,7 +213,7 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
       aria-label="mailbox folders"
     >
       <Drawer
-        container={container}
+        container={window?.document.body}
         variant={matchUpMd ? 'persistent' : 'temporary'}
         anchor="left"
         open={drawerOpen}
@@ -91,13 +223,36 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
             width: drawerWidth,
             background: theme.palette.background.default,
             color: theme.palette.text.primary,
-            borderRight: theme.palette.mode === 'dark' ? '1px solid rgba(255, 255, 255, 0.12)' : 'none',
+            borderRight: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
+            transition: theme.transitions.create(['width', 'box-shadow'], {
+              easing: theme.transitions.easing.easeOut,
+              duration: theme.transitions.duration.enteringScreen
+            }),
+            boxSizing: 'border-box',
+            borderRadius: 0,
             [theme.breakpoints.up('md')]: {
-              top: '88px'
+              top: '64px',
+              height: 'calc(100% - 64px)',
+              boxShadow: 'none'
+            },
+            [theme.breakpoints.down('md')]: {
+              top: '0',
+              height: '100%',
+              boxShadow: theme.shadows[8],
+              zIndex: 1300
+            },
+            overflowX: 'hidden'
+          },
+          '& .MuiBackdrop-root': {
+            [theme.breakpoints.down('md')]: {
+              zIndex: 1290
             }
           }
         }}
-        ModalProps={{ keepMounted: true }}
+        ModalProps={{
+          keepMounted: true,
+          closeAfterTransition: true
+        }}
         color="inherit"
       >
         {drawer}
diff --git a/web/src/layout/MainLayout/index.jsx b/web/src/layout/MainLayout/index.jsx
index ad00a303..33254908 100644
--- a/web/src/layout/MainLayout/index.jsx
+++ b/web/src/layout/MainLayout/index.jsx
@@ -21,8 +21,8 @@ import { Icon } from '@iconify/react';
 // styles
 export const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
   ...theme.typography.mainContent,
-  borderBottomLeftRadius: 0,
-  borderBottomRightRadius: 0,
+  borderRadius: 0,
+  backgroundColor: theme.palette.background.default,
   transition: theme.transitions.create(
     'margin',
     open
@@ -35,20 +35,42 @@ export const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open
           duration: theme.transitions.duration.leavingScreen
         }
   ),
+  overflowY: 'auto',
+  overflowX: 'hidden',
+  height: 'calc(100vh - 64px)',
+  paddingBottom: '30px',
+  marginTop: '64px',
+  position: 'relative',
+  scrollbarWidth: 'thin',
+  '&::-webkit-scrollbar': {
+    width: '8px',
+    height: '8px'
+  },
+  '&::-webkit-scrollbar-thumb': {
+    background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.15)',
+    borderRadius: '4px'
+  },
+  '&::-webkit-scrollbar-track': {
+    background: 'transparent'
+  },
   [theme.breakpoints.up('md')]: {
     marginLeft: open ? 0 : -(drawerWidth - 20),
     width: `calc(100% - ${drawerWidth}px)`
   },
   [theme.breakpoints.down('md')]: {
-    marginLeft: '20px',
-    width: `calc(100% - ${drawerWidth}px)`,
-    padding: '16px'
+    marginLeft: '0',
+    width: '100%',
+    padding: '16px',
+    marginTop: '64px',
+    height: 'calc(100vh - 64px)'
   },
   [theme.breakpoints.down('sm')]: {
-    marginLeft: '10px',
-    width: `calc(100% - ${drawerWidth}px)`,
+    marginLeft: '0',
+    width: '100%',
     padding: '16px',
-    marginRight: '10px'
+    marginRight: '0',
+    marginTop: '56px',
+    height: 'calc(100vh - 56px)'
   }
 }));
 
@@ -65,7 +87,16 @@ const MainLayout = () => {
   };
 
   return (
-    <Box sx={{ display: 'flex' }}>
+    <Box
+      sx={{
+        display: 'flex',
+        overflow: 'hidden',
+        width: '100%',
+        height: '100vh',
+        position: 'relative',
+        backgroundColor: theme.palette.background.default
+      }}
+    >
       <CssBaseline />
       {/* header */}
       <AppBar
@@ -75,10 +106,17 @@ const MainLayout = () => {
         elevation={0}
         sx={{
           bgcolor: theme.palette.background.default,
-          transition: leftDrawerOpened ? theme.transitions.create('width') : 'none'
+          boxShadow: 'none',
+          borderBottom: 'none',
+          transition: leftDrawerOpened ? theme.transitions.create('width') : 'none',
+          zIndex: {
+            xs: matchDownMd && leftDrawerOpened ? 0 : theme.zIndex.drawer - 1,
+            md: theme.zIndex.drawer + 1
+          },
+          width: '100%'
         }}
       >
-        <Toolbar>
+        <Toolbar sx={{ px: { xs: 1.5, sm: 2, md: 3 }, minHeight: '64px', height: '64px' }}>
           <Header handleLeftDrawerToggle={handleLeftDrawerToggle} />
         </Toolbar>
       </AppBar>
@@ -89,7 +127,7 @@ const MainLayout = () => {
       {/* main content */}
       <Main theme={theme} open={leftDrawerOpened}>
         {/* breadcrumb */}
-        <Breadcrumbs separator={<Icon icon="solar:arrow-right-linear" />} navigation={navigation} icon title rightAlign />
+        <Breadcrumbs separator={<Icon icon="solar:arrow-right-linear" width="16" />} navigation={navigation} icon title rightAlign />
         <AuthGuard>
           <AdminContainer>
             <Outlet />
diff --git a/web/src/layout/MinimalLayout/Header/index.jsx b/web/src/layout/MinimalLayout/Header/index.jsx
index 882d9791..d62548ed 100644
--- a/web/src/layout/MinimalLayout/Header/index.jsx
+++ b/web/src/layout/MinimalLayout/Header/index.jsx
@@ -29,7 +29,7 @@ import MainCard from 'ui-component/cards/MainCard';
 import { useMediaQuery } from '@mui/material';
 import { useTranslation } from 'react-i18next';
 
-// ==============================|| MAIN NAVBAR / HEADER ||============================== //
+// ==============================|| MINIMAL NAVBAR / HEADER ||============================== //
 
 const Header = () => {
   const theme = useTheme();
@@ -53,56 +53,133 @@ const Header = () => {
         sx={{
           width: 228,
           display: 'flex',
+          alignItems: 'center',
           [theme.breakpoints.down('md')]: {
             width: 'auto'
           }
         }}
       >
-        <Box component="span" sx={{ flexGrow: 1 }}>
+        <Box component="span" sx={{ display: 'block' }}>
           <LogoSection />
         </Box>
       </Box>
 
       <Box sx={{ flexGrow: 1 }} />
-      <Box sx={{ flexGrow: 1 }} />
-      <Stack spacing={2} direction="row" justifyContent="center" alignItems="center">
+      <Stack spacing={{ xs: 0.5, sm: 1, md: 2 }} direction="row" justifyContent="center" alignItems="center">
         {isMobile ? (
           <>
-            <NoticeButton />
-            <ThemeButton />
-            <I18nButton />
-            <IconButton onClick={handleOpenMenu}>
-              <IconMenu2 />
+            <NoticeButton sx={{ color: theme.palette.text.primary, mr: 1 }} />
+            <ThemeButton sx={{ color: theme.palette.text.primary, mr: 1 }} />
+            <I18nButton sx={{ color: theme.palette.text.primary, mr: 1 }} />
+            <IconButton
+              onClick={handleOpenMenu}
+              sx={{
+                color: theme.palette.text.primary,
+                borderRadius: '12px',
+                padding: '8px',
+                backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.04)',
+                '&:hover': {
+                  backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.08)'
+                }
+              }}
+            >
+              <IconMenu2 stroke={1.5} size="1.3rem" />
             </IconButton>
           </>
         ) : (
           <>
-            <Button component={Link} variant="text" to="/" color={pathname === '/' ? 'primary' : 'inherit'}>
+            <Button
+              component={Link}
+              variant="text"
+              to="/"
+              color={pathname === '/' ? 'primary' : 'inherit'}
+              sx={{
+                fontSize: '0.875rem',
+                fontWeight: 500,
+                textTransform: 'none'
+              }}
+            >
               {t('menu.home')}
             </Button>
             {account.user && (
-              <Button component={Link} variant="text" to="/playground" color={pathname === '/playground' ? 'primary' : 'inherit'}>
+              <Button
+                component={Link}
+                variant="text"
+                to="/playground"
+                color={pathname === '/playground' ? 'primary' : 'inherit'}
+                sx={{
+                  fontSize: '0.875rem',
+                  fontWeight: 500,
+                  textTransform: 'none'
+                }}
+              >
                 {t('playground')}
               </Button>
             )}
-            <Button component={Link} variant="text" to="/price" color={pathname === '/price' ? 'primary' : 'inherit'}>
+            <Button
+              component={Link}
+              variant="text"
+              to="/price"
+              color={pathname === '/price' ? 'primary' : 'inherit'}
+              sx={{
+                fontSize: '0.875rem',
+                fontWeight: 500,
+                textTransform: 'none'
+              }}
+            >
               {t('price')}
             </Button>
-            <Button component={Link} variant="text" to="/about" color={pathname === '/about' ? 'primary' : 'inherit'}>
+            <Button
+              component={Link}
+              variant="text"
+              to="/about"
+              color={pathname === '/about' ? 'primary' : 'inherit'}
+              sx={{
+                fontSize: '0.875rem',
+                fontWeight: 500,
+                textTransform: 'none'
+              }}
+            >
               {t('menu.about')}
             </Button>
-            <NoticeButton />
-            <ThemeButton />
-            <I18nButton />
+            <NoticeButton sx={{ color: theme.palette.text.primary, ml: 1 }} />
+            <ThemeButton sx={{ color: theme.palette.text.primary, ml: 0.5 }} />
+            <I18nButton sx={{ color: theme.palette.text.primary, ml: 0.5 }} />
             {account.user ? (
               <>
-                <Button component={Link} variant="contained" to="/panel" color="primary">
+                <Button
+                  component={Link}
+                  variant="contained"
+                  to="/panel"
+                  color="primary"
+                  sx={{
+                    ml: 2,
+                    px: 2,
+                    height: '40px',
+                    fontSize: '0.875rem',
+                    fontWeight: 500,
+                    textTransform: 'none'
+                  }}
+                >
                   {t('menu.console')}
                 </Button>
                 <ProfileSection />
               </>
             ) : (
-              <Button component={Link} variant="contained" to="/login" color="primary">
+              <Button
+                component={Link}
+                variant="contained"
+                to="/login"
+                color="primary"
+                sx={{
+                  ml: 2,
+                  px: 2.5,
+                  height: '40px',
+                  fontSize: '0.875rem',
+                  fontWeight: 500,
+                  textTransform: 'none'
+                }}
+              >
                 {t('menu.login')}
               </Button>
             )}
@@ -115,6 +192,7 @@ const Header = () => {
         anchorEl={open}
         transition
         disablePortal
+        placement="bottom-end"
         popperOptions={{
           modifiers: [
             {
@@ -125,48 +203,171 @@ const Header = () => {
             }
           ]
         }}
-        style={{ width: '100vw' }}
+        style={{ zIndex: theme.zIndex.drawer + 2 }}
       >
         {({ TransitionProps }) => (
           <Transitions in={open} {...TransitionProps}>
             <ClickAwayListener onClickAway={handleCloseMenu}>
-              <Paper style={{ width: '100%' }}>
-                <MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
+              <Paper
+                sx={{
+                  width: { xs: '100vw', sm: '320px' },
+                  borderRadius: '12px',
+                  overflow: 'hidden',
+                  boxShadow: theme.shadows[8]
+                }}
+              >
+                <MainCard border={false} elevation={0} content={false} boxShadow>
                   <List
                     component="nav"
                     sx={{
                       width: '100%',
-                      maxWidth: '100%',
-                      minWidth: '100%',
                       backgroundColor: theme.palette.background.paper,
-
+                      py: 1,
                       '& .MuiListItemButton-root': {
-                        mt: 0.5
+                        py: 1.25,
+                        px: 2.5,
+                        '&:hover': {
+                          backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.04)'
+                        }
+                      },
+                      '& .Mui-selected': {
+                        backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.05)',
+                        '&:hover': {
+                          backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.08)'
+                        }
                       }
                     }}
                     onClick={handleCloseMenu}
                   >
-                    <ListItemButton component={Link} variant="text" to="/">
-                      <ListItemText primary={<Typography variant="body2">{t('menu.home')}</Typography>} />
+                    <ListItemButton component={Link} to="/" selected={pathname === '/'}>
+                      <ListItemText
+                        primary={
+                          <Typography
+                            variant="body1"
+                            sx={{
+                              fontWeight: pathname === '/' ? 500 : 400,
+                              color: pathname === '/' ? theme.palette.primary.main : theme.palette.text.primary
+                            }}
+                          >
+                            {t('menu.home')}
+                          </Typography>
+                        }
+                      />
                     </ListItemButton>
 
                     {account.user && (
-                      <ListItemButton component={Link} variant="text" to="/playground">
-                        <ListItemText primary={<Typography variant="body2">Playground</Typography>} />
+                      <ListItemButton component={Link} to="/playground" selected={pathname === '/playground'}>
+                        <ListItemText
+                          primary={
+                            <Typography
+                              variant="body1"
+                              sx={{
+                                fontWeight: pathname === '/playground' ? 500 : 400,
+                                color: pathname === '/playground' ? theme.palette.primary.main : theme.palette.text.primary
+                              }}
+                            >
+                              {t('playground')}
+                            </Typography>
+                          }
+                        />
                       </ListItemButton>
                     )}
 
-                    <ListItemButton component={Link} variant="text" to="/about">
-                      <ListItemText primary={<Typography variant="body2">{t('menu.about')}</Typography>} />
+                    <ListItemButton component={Link} to="/price" selected={pathname === '/price'}>
+                      <ListItemText
+                        primary={
+                          <Typography
+                            variant="body1"
+                            sx={{
+                              fontWeight: pathname === '/price' ? 500 : 400,
+                              color: pathname === '/price' ? theme.palette.primary.main : theme.palette.text.primary
+                            }}
+                          >
+                            {t('price')}
+                          </Typography>
+                        }
+                      />
+                    </ListItemButton>
+
+                    <ListItemButton component={Link} to="/about" selected={pathname === '/about'}>
+                      <ListItemText
+                        primary={
+                          <Typography
+                            variant="body1"
+                            sx={{
+                              fontWeight: pathname === '/about' ? 500 : 400,
+                              color: pathname === '/about' ? theme.palette.primary.main : theme.palette.text.primary
+                            }}
+                          >
+                            {t('menu.about')}
+                          </Typography>
+                        }
+                      />
                     </ListItemButton>
-                    <Divider />
+                    <Divider sx={{ my: 1 }} />
                     {account.user ? (
-                      <ListItemButton component={Link} variant="contained" to="/panel" color="primary">
-                        {t('menu.console')}
+                      <ListItemButton
+                        component={Link}
+                        to="/panel"
+                        sx={{
+                          backgroundColor: theme.palette.primary.light,
+                          borderRadius: '8px',
+                          mx: 1.5,
+                          '&:hover': {
+                            backgroundColor: theme.palette.primary.main,
+                            '& .MuiTypography-root': {
+                              color: '#fff'
+                            }
+                          }
+                        }}
+                      >
+                        <ListItemText
+                          primary={
+                            <Typography
+                              color="primary"
+                              variant="body1"
+                              sx={{
+                                fontWeight: 600,
+                                textAlign: 'center',
+                                transition: 'color 0.2s ease'
+                              }}
+                            >
+                              {t('menu.console')}
+                            </Typography>
+                          }
+                        />
                       </ListItemButton>
                     ) : (
-                      <ListItemButton component={Link} variant="contained" to="/login" color="primary">
-                        {t('menu.login')}
+                      <ListItemButton
+                        component={Link}
+                        to="/login"
+                        sx={{
+                          backgroundColor: theme.palette.primary.light,
+                          borderRadius: '8px',
+                          mx: 1.5,
+                          '&:hover': {
+                            backgroundColor: theme.palette.primary.main,
+                            '& .MuiTypography-root': {
+                              color: '#fff'
+                            }
+                          }
+                        }}
+                      >
+                        <ListItemText
+                          primary={
+                            <Typography
+                              color="primary"
+                              variant="body1"
+                              sx={{
+                                fontWeight: 600,
+                                textAlign: 'center',
+                                transition: 'color 0.2s ease'
+                              }}
+                            >
+                              {t('menu.login')}
+                            </Typography>
+                          }
+                        />
                       </ListItemButton>
                     )}
                   </List>
diff --git a/web/src/layout/MinimalLayout/index.jsx b/web/src/layout/MinimalLayout/index.jsx
index 81047fd1..8c3ad034 100644
--- a/web/src/layout/MinimalLayout/index.jsx
+++ b/web/src/layout/MinimalLayout/index.jsx
@@ -1,6 +1,6 @@
 import { Outlet } from 'react-router-dom';
 import { useTheme } from '@mui/material/styles';
-import { AppBar, Box, CssBaseline, Toolbar, Container } from '@mui/material';
+import { AppBar, Box, CssBaseline, Toolbar, Container, useMediaQuery } from '@mui/material';
 import Header from './Header';
 import Footer from 'ui-component/Footer';
 
@@ -8,6 +8,8 @@ import Footer from 'ui-component/Footer';
 
 const MinimalLayout = () => {
   const theme = useTheme();
+  const matchDownSm = useMediaQuery(theme.breakpoints.down('sm'));
+  const matchDownMd = useMediaQuery(theme.breakpoints.down('md'));
 
   return (
     <Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
@@ -19,19 +21,44 @@ const MinimalLayout = () => {
         elevation={0}
         sx={{
           bgcolor: theme.palette.background.default,
-          flex: 'none'
+          boxShadow: 'none',
+          borderBottom: 'none',
+          zIndex: theme.zIndex.drawer + 1,
+          width: '100%'
         }}
       >
-        <Container>
-          <Toolbar>
+        <Container maxWidth="xl">
+          <Toolbar sx={{ px: { xs: 1.5, sm: 2, md: 3 }, minHeight: '64px', height: '64px' }}>
             <Header />
           </Toolbar>
         </Container>
       </AppBar>
-      <Box sx={{ flex: '1 1 auto', overflow: 'auto' }} marginTop={'80px'}>
+      <Box
+        sx={{
+          flex: '1 1 auto',
+          overflow: 'auto',
+          marginTop: { xs: '56px', sm: '64px' },
+          backgroundColor: theme.palette.background.default,
+          // padding: { xs: '16px', sm: '20px', md: '24px' },
+          position: 'relative',
+          minHeight: `calc(100vh - ${matchDownSm ? '56px' : '64px'} - ${matchDownMd ? '80px' : '60px'})`,
+          scrollbarWidth: 'thin',
+          '&::-webkit-scrollbar': {
+            width: '8px',
+            height: '8px'
+          },
+          '&::-webkit-scrollbar-thumb': {
+            background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.15)',
+            borderRadius: '4px'
+          },
+          '&::-webkit-scrollbar-track': {
+            background: 'transparent'
+          }
+        }}
+      >
         <Outlet />
       </Box>
-      <Box sx={{ flex: 'none' }}>
+      <Box sx={{ flex: 'none', position: 'relative', zIndex: 1 }}>
         <Footer />
       </Box>
     </Box>
diff --git a/web/src/routes/OtherRoutes.jsx b/web/src/routes/OtherRoutes.jsx
index 833eee92..24548f3b 100644
--- a/web/src/routes/OtherRoutes.jsx
+++ b/web/src/routes/OtherRoutes.jsx
@@ -1,4 +1,5 @@
 import { lazy } from 'react';
+import { Box } from '@mui/material';
 
 // project imports
 import Loadable from 'ui-component/Loadable';
@@ -19,7 +20,17 @@ const Jump = Loadable(lazy(() => import('views/Jump')));
 const Playground = Loadable(lazy(() => import('views/Playground')));
 const ModelPrice = Loadable(lazy(() => import('views/ModelPrice')));
 
-const WithMargins = ({ children }) => <div style={{ maxWidth: '1200px', margin: '0 auto', padding: '0 24px' }}>{children}</div>;
+const WithMargins = ({ children }) => (
+  <Box
+    sx={{
+      maxWidth: '1200px',
+      margin: '0 auto',
+      padding: { xs: 0, sm: '0 24px' }
+    }}
+  >
+    {children}
+  </Box>
+);
 
 // ==============================|| AUTHENTICATION ROUTING ||============================== //
 
diff --git a/web/src/themes/compStyleOverride.js b/web/src/themes/compStyleOverride.js
index 3448409d..4304634a 100644
--- a/web/src/themes/compStyleOverride.js
+++ b/web/src/themes/compStyleOverride.js
@@ -1,51 +1,97 @@
 export default function componentStyleOverrides(theme) {
   const bgColor = theme.mode === 'dark' ? theme.paper : theme.colors?.grey50;
   return {
+    MuiCssBaseline: {
+      styleOverrides: `
+        * {
+          box-sizing: border-box;
+        }
+        html, body {
+          font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+          -webkit-font-smoothing: antialiased;
+          -moz-osx-font-smoothing: grayscale;
+        }
+        .apexcharts-title-text {
+          fill: ${theme.textDark} !important
+        }
+        .apexcharts-text {
+          fill: ${theme.textDark} !important
+        }
+        .apexcharts-legend-text {
+          color: ${theme.textDark} !important
+        }
+        .apexcharts-menu {
+          background: ${theme.backgroundDefault} !important
+        }
+        .apexcharts-gridline, .apexcharts-xaxistooltip-background, .apexcharts-yaxistooltip-background {
+          stroke: ${theme.divider} !important;
+        }
+      `
+    },
     MuiButton: {
       styleOverrides: {
         root: {
           fontWeight: 500,
-          borderRadius: '4px',
+          borderRadius: '10px',
+          textTransform: 'none',
+          boxShadow: 'none',
+          minHeight: '36px',
+          padding: '6px 16px',
+          letterSpacing: '0.01em',
+          transition: 'background-color 0.2s ease-in-out',
           '&.Mui-disabled': {
             color: theme.colors?.grey600
+          },
+          '&:hover': {
+            boxShadow: 'none'
+          },
+          '&:active': {
+            transform: 'translateY(0)'
           }
-        }
-      }
-    },
-    //MuiAutocomplete-popper MuiPopover-root
-    MuiAutocomplete: {
-      styleOverrides: {
-        popper: {
-          // 继承 MuiPopover-root
-          boxShadow: '0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.14),0px 3px 14px 2px rgba(0,0,0,0.12)',
-          borderRadius: '12px',
-          color: '#364152'
         },
-        listbox: {
-          // 继承 MuiPopover-root
-          padding: '0px',
-          paddingTop: '8px',
-          paddingBottom: '8px'
+        containedPrimary: {
+          background: theme.colors?.primaryMain,
+          '&:hover': {
+            background: theme.colors?.primaryDark
+          }
         },
-        option: {
-          fontSize: '16px',
-          fontWeight: '400',
-          lineHeight: '1.334em',
-          alignItems: 'center',
-          paddingTop: '6px',
-          paddingBottom: '6px',
-          paddingLeft: '16px',
-          paddingRight: '16px'
+        outlinedPrimary: {
+          borderColor: theme.colors?.primaryMain,
+          color: theme.colors?.primaryMain,
+          '&:hover': {
+            backgroundColor: 'rgba(8, 132, 221, 0.04)'
+          }
+        },
+        text: {
+          '&:hover': {
+            backgroundColor: 'rgba(0, 0, 0, 0.04)'
+          }
+        },
+        sizeSmall: {
+          padding: '6px 16px',
+          fontSize: '0.8125rem',
+          minHeight: '32px',
+          borderRadius: '8px'
+        },
+        sizeLarge: {
+          padding: '10px 24px',
+          fontSize: '1rem',
+          minHeight: '48px'
         }
       }
     },
     MuiIconButton: {
       styleOverrides: {
         root: {
+          borderRadius: '8px',
+          padding: '8px',
           color: theme.darkTextPrimary,
           '&:hover': {
-            backgroundColor: theme.colors?.grey200
+            backgroundColor: 'rgba(0, 0, 0, 0.04)'
           }
+        },
+        sizeSmall: {
+          padding: '4px'
         }
       }
     },
@@ -55,10 +101,37 @@ export default function componentStyleOverrides(theme) {
       },
       styleOverrides: {
         root: {
-          backgroundImage: 'none'
+          backgroundImage: 'none',
+          borderRadius: '14px',
+          boxShadow: theme.mode === 'dark' ? '0 2px 8px 0 rgba(0,0,0,0.2)' : '0 1px 3px 0 rgba(0,0,0,0.1), 0 1px 2px 0 rgba(0,0,0,0.06)'
         },
         rounded: {
-          borderRadius: `${theme?.customization?.borderRadius}px`
+          borderRadius: `${theme?.customization?.borderRadius || 14}px`
+        },
+        elevation1: {
+          boxShadow: theme.mode === 'dark' ? '0 2px 8px 0 rgba(0,0,0,0.2)' : '0 1px 3px 0 rgba(0,0,0,0.1), 0 1px 2px 0 rgba(0,0,0,0.06)'
+        },
+        elevation2: {
+          boxShadow:
+            theme.mode === 'dark' ? '0 3px 12px 0 rgba(0,0,0,0.22)' : '0 3px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06)'
+        }
+      }
+    },
+    MuiCard: {
+      styleOverrides: {
+        root: {
+          borderRadius: '14px',
+          padding: 0,
+          boxShadow: theme.mode === 'dark' ? '0 2px 8px 0 rgba(0,0,0,0.2)' : '0 1px 3px 0 rgba(0,0,0,0.1), 0 1px 2px 0 rgba(0,0,0,0.06)',
+          transition: 'box-shadow 0.3s ease',
+          overflow: 'hidden',
+          '&:hover': {
+            boxShadow:
+              theme.mode === 'dark' ? '0 6px 16px 0 rgba(0,0,0,0.3)' : '0 8px 14px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05)'
+          },
+          '& .MuiTableContainer-root': {
+            borderRadius: 0
+          }
         }
       }
     },
@@ -69,21 +142,62 @@ export default function componentStyleOverrides(theme) {
           padding: '24px'
         },
         title: {
-          fontSize: '1.125rem'
+          fontSize: '1.125rem',
+          fontWeight: 600
+        },
+        subheader: {
+          fontSize: '0.875rem',
+          color: theme.darkTextSecondary,
+          marginTop: '4px'
+        },
+        avatar: {
+          marginRight: '16px'
         }
       }
     },
     MuiCardContent: {
       styleOverrides: {
         root: {
-          padding: '24px'
+          padding: '24px',
+          '&:last-child': {
+            paddingBottom: '24px'
+          },
+          '& .MuiTableContainer-root': {
+            margin: '-24px',
+            width: 'calc(100% + 48px)',
+            maxWidth: 'calc(100% + 48px)'
+          }
         }
       }
     },
     MuiCardActions: {
       styleOverrides: {
         root: {
-          padding: '24px'
+          padding: '16px 20px'
+        }
+      }
+    },
+    MuiAutocomplete: {
+      styleOverrides: {
+        popper: {
+          boxShadow: theme.mode === 'dark' ? '0 4px 20px 0 rgba(0,0,0,0.3)' : '0 2px 10px 0 rgba(0,0,0,0.12)',
+          borderRadius: '8px',
+          color: theme.darkTextPrimary
+        },
+        listbox: {
+          padding: '4px 0'
+        },
+        option: {
+          fontSize: '0.875rem',
+          fontWeight: 400,
+          lineHeight: '1.43',
+          padding: '8px 16px',
+          '&:hover': {
+            backgroundColor: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.04)'
+          },
+          '&[aria-selected="true"]': {
+            backgroundColor: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.16)' : 'rgba(0, 0, 0, 0.08)'
+          }
         }
       }
     },
@@ -91,8 +205,8 @@ export default function componentStyleOverrides(theme) {
       styleOverrides: {
         root: {
           color: theme.darkTextPrimary,
-          paddingTop: '10px',
-          paddingBottom: '10px',
+          borderRadius: '8px',
+          padding: '8px 16px',
           '&.Mui-selected': {
             color: theme.menuSelected,
             backgroundColor: theme.menuSelectedBack,
@@ -104,7 +218,7 @@ export default function componentStyleOverrides(theme) {
             }
           },
           '&:hover': {
-            backgroundColor: theme.menuSelectedBack,
+            backgroundColor: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)',
             color: theme.menuSelected,
             '& .MuiListItemIcon-root': {
               color: theme.menuSelected
@@ -124,12 +238,21 @@ export default function componentStyleOverrides(theme) {
     MuiListItemText: {
       styleOverrides: {
         primary: {
-          color: theme.textDark
+          color: theme.textDark,
+          fontSize: '0.875rem'
+        },
+        secondary: {
+          fontSize: '0.75rem',
+          color: theme.darkTextSecondary
         }
       }
     },
     MuiInputBase: {
       styleOverrides: {
+        root: {
+          fontSize: '0.875rem',
+          lineHeight: '1.5'
+        },
         input: {
           color: theme.textDark,
           '&::placeholder': {
@@ -143,40 +266,105 @@ export default function componentStyleOverrides(theme) {
       styleOverrides: {
         root: {
           background: bgColor,
-          borderRadius: `${theme?.customization?.borderRadius}px`,
+          borderRadius: '12px',
           '& .MuiOutlinedInput-notchedOutline': {
-            borderColor: theme.colors?.grey400
+            borderColor: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.12)' : theme.colors?.grey300,
+            borderWidth: '1px',
+            transition: 'border-color 0.2s ease-in-out'
           },
-          '&:hover $notchedOutline': {
-            borderColor: theme.colors?.primaryLight
+          '&:hover .MuiOutlinedInput-notchedOutline': {
+            borderColor: theme.colors?.primaryMain,
+            borderWidth: '1px'
           },
-          '&.MuiInputBase-multiline': {
-            padding: 1
+          '&.Mui-focused': {
+            '& .MuiOutlinedInput-notchedOutline': {
+              borderColor: theme.colors?.primaryMain,
+              borderWidth: '1.5px'
+            }
+          },
+          '&.Mui-error': {
+            '& .MuiOutlinedInput-notchedOutline': {
+              borderColor: theme.colors?.errorMain
+            }
+          },
+          '& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button': {
+            '-webkit-appearance': 'none',
+            margin: 0
+          },
+          '& input[type=number]': {
+            '-moz-appearance': 'textfield'
           }
         },
         input: {
-          fontWeight: 500,
-          background: bgColor,
-          padding: '15.5px 14px',
-          borderRadius: `${theme?.customization?.borderRadius}px`,
-          '&.MuiInputBase-inputSizeSmall': {
-            padding: '10px 14px',
-            '&.MuiInputBase-inputAdornedStart': {
-              paddingLeft: 0
-            }
-          }
+          padding: '14px 16px',
+          height: 'auto',
+          fontSize: '0.9375rem'
         },
-        inputAdornedStart: {
-          paddingLeft: 4
+        inputMultiline: {
+          padding: '4px 8px'
         },
-        notchedOutline: {
-          borderRadius: `${theme?.customization?.borderRadius}px`
+        sizeSmall: {
+          '& input': {
+            padding: '10px 14px'
+          }
+        }
+      }
+    },
+    MuiFormLabel: {
+      styleOverrides: {
+        root: {
+          fontSize: '0.9375rem',
+          color: theme.darkTextSecondary,
+          transform: 'translate(16px, 16px) scale(1)',
+          '&.Mui-focused': {
+            color: theme.colors?.primaryMain
+          },
+          '&.Mui-error': {
+            color: theme.colors?.errorMain
+          },
+          '&.MuiInputLabel-shrink': {
+            transform: 'translate(16px, -9px) scale(0.75)'
+          }
+        }
+      }
+    },
+    MuiFormHelperText: {
+      styleOverrides: {
+        root: {
+          fontSize: '0.75rem',
+          marginLeft: '4px',
+          marginTop: '4px'
+        }
+      }
+    },
+    MuiInputAdornment: {
+      styleOverrides: {
+        root: {
+          marginLeft: 0,
+          width: 'auto',
+          '& .MuiIconButton-root': {
+            padding: 0,
+            width: '20px',
+            height: '20px',
+            minWidth: '20px',
+            margin: 0
+          },
+          '& .MuiSvgIcon-root, & .iconify': {
+            fontSize: '16px',
+            width: '16px',
+            height: '16px'
+          }
+        },
+        positionEnd: {
+          marginLeft: 0,
+          paddingLeft: 0
         }
       }
     },
     MuiSlider: {
       styleOverrides: {
         root: {
+          height: 4,
           '&.Mui-disabled': {
             color: theme.colors?.grey300
           }
@@ -187,6 +375,10 @@ export default function componentStyleOverrides(theme) {
         },
         valueLabel: {
           color: theme?.colors?.primaryLight
+        },
+        thumb: {
+          width: 14,
+          height: 14
         }
       }
     },
@@ -194,7 +386,7 @@ export default function componentStyleOverrides(theme) {
       styleOverrides: {
         root: {
           borderColor: theme.divider,
-          opacity: 1
+          opacity: 0.6
         }
       }
     },
@@ -202,15 +394,113 @@ export default function componentStyleOverrides(theme) {
       styleOverrides: {
         root: {
           color: theme.colors?.primaryDark,
-          background: theme.colors?.primary200
+          background: theme.colors?.primary200,
+          fontSize: '1rem',
+          fontWeight: 500
         }
       }
     },
     MuiChip: {
       styleOverrides: {
         root: {
-          '&.MuiChip-deletable .MuiChip-deleteIcon': {
-            color: 'inherit'
+          fontSize: '0.8125rem',
+          fontWeight: 500,
+          height: '32px',
+          borderRadius: '16px',
+          backgroundColor: theme.mode === 'dark' ? '#ffffff' : 'rgba(0, 0, 0, 0.8)',
+          color: theme.mode === 'dark' ? 'rgba(0, 0, 0, 0.87)' : '#ffffff',
+          transition: 'all 0.2s ease-in-out',
+          '&.MuiChip-outlined': {
+            borderColor: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.8)',
+            backgroundColor: 'transparent',
+            color: theme.textDark
+          },
+          '&.MuiChip-clickable': {
+            '&:hover': {
+              backgroundColor: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.9)' : '#000000'
+            }
+          }
+        },
+        label: {
+          padding: '0 12px',
+          lineHeight: '32px'
+        },
+        icon: {
+          fontSize: '1.25rem',
+          marginLeft: '8px',
+          marginRight: '-6px',
+          color: theme.mode === 'dark' ? 'rgba(0, 0, 0, 0.87)' : '#ffffff'
+        },
+        deleteIcon: {
+          fontSize: '1.25rem',
+          color: theme.mode === 'dark' ? 'rgba(0, 0, 0, 0.6)' : 'rgba(255, 255, 255, 0.8)',
+          margin: '0 5px 0 -6px',
+          padding: '2px',
+          '&:hover': {
+            color: theme.mode === 'dark' ? 'rgba(0, 0, 0, 0.87)' : '#ffffff',
+            backgroundColor: 'transparent'
+          }
+        },
+        sizeSmall: {
+          height: '26px',
+          fontSize: '0.75rem',
+          '& .MuiChip-label': {
+            padding: '0 10px',
+            lineHeight: '26px'
+          },
+          '& .MuiChip-icon': {
+            fontSize: '1rem'
+          },
+          '& .MuiChip-deleteIcon': {
+            fontSize: '1rem'
+          }
+        }
+      }
+    },
+    MuiTableContainer: {
+      styleOverrides: {
+        root: {
+          overflowX: 'auto',
+          overflowY: 'auto',
+          borderRadius: '12px',
+          boxShadow: 'none',
+          ...(theme.breakpoints && {
+            [theme.breakpoints.down('sm')]: {
+              maxWidth: '100%',
+              whiteSpace: 'nowrap'
+            }
+          })
+        }
+      }
+    },
+    MuiTable: {
+      styleOverrides: {
+        root: {
+          borderCollapse: 'separate',
+          borderSpacing: 0,
+          width: '100%',
+          margin: 0,
+          padding: 0
+        }
+      }
+    },
+    MuiTableHead: {
+      styleOverrides: {
+        root: {
+          backgroundColor: theme.headBackgroundColor,
+          borderBottom: `1px dashed ${theme.tableBorderBottom}`,
+          width: '100%',
+          margin: 0,
+          '& tr': {
+            width: '100%',
+            '& th:first-of-type': {
+              borderTopLeftRadius: 0,
+              paddingLeft: '24px'
+            },
+            '& th:last-of-type': {
+              borderTopRightRadius: 0,
+              paddingRight: '24px'
+            }
           }
         }
       }
@@ -218,51 +508,540 @@ export default function componentStyleOverrides(theme) {
     MuiTableCell: {
       styleOverrides: {
         root: {
-          // color: theme.darkTextSecondary,
-          borderBottom: '1px solid ' + theme.tableBorderBottom,
-          textAlign: 'center'
+          borderBottom: `1px dashed ${theme.tableBorderBottom}`,
+          fontSize: '0.875rem',
+          padding: '16px 12px',
+          textAlign: 'center',
+          '&:first-of-type': {
+            paddingLeft: '12px'
+          },
+          '&:last-of-type': {
+            paddingRight: '12px'
+          }
         },
         head: {
+          fontSize: '0.875rem',
+          fontWeight: 600,
           color: theme.darkTextSecondary,
-          backgroundColor: theme.headBackgroundColor
+          borderBottom: 'none',
+          whiteSpace: 'nowrap',
+          padding: '14px 12px',
+          textAlign: 'center'
+        },
+        body: {
+          color: theme.textDark
         }
       }
     },
     MuiTableRow: {
       styleOverrides: {
         root: {
+          transition: 'background-color 0.2s ease',
           '&:hover': {
-            backgroundColor: theme.headBackgroundColorHover
+            backgroundColor: theme.mode === 'dark' ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.04)'
+          },
+          '&:last-child td': {
+            borderBottom: 0
+          },
+          '&.MuiTableRow-root.MuiTableRow-hover:hover': {
+            backgroundColor: theme.mode === 'dark' ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.04)'
+          },
+          '& .MuiCollapse-root': {
+            '&:hover': {
+              backgroundColor: 'transparent'
+            }
+          },
+          '& .MuiCollapse-root .MuiTableRow-root': {
+            '&:hover': {
+              backgroundColor: 'transparent'
+            }
           }
         }
       }
     },
+    MuiTablePagination: {
+      styleOverrides: {
+        root: {
+          color: theme.textDark,
+          borderTop: `1px dashed ${theme.tableBorderBottom}`,
+          overflow: 'auto',
+          backgroundColor: theme.headBackgroundColor,
+          minHeight: '56px',
+          width: '100%',
+          margin: 0,
+          padding: '0 24px',
+          '& .MuiToolbar-root': {
+            minHeight: '56px',
+            padding: '0',
+            ...(theme.breakpoints && {
+              [theme.breakpoints.down('sm')]: {
+                flexWrap: 'wrap',
+                justifyContent: 'center',
+                padding: '8px 0'
+              }
+            }),
+            '& > p:first-of-type': {
+              fontSize: '0.875rem',
+              color: theme.darkTextSecondary
+            }
+          }
+        },
+        select: {
+          paddingTop: '6px',
+          paddingBottom: '6px',
+          paddingLeft: '12px',
+          paddingRight: '28px',
+          borderRadius: '8px',
+          backgroundColor: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.03)',
+          '&:focus': {
+            backgroundColor: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.09)' : 'rgba(0, 0, 0, 0.05)'
+          }
+        },
+        selectLabel: {
+          paddingLeft: '20px'
+        },
+        selectIcon: {
+          color: theme.darkTextSecondary,
+          right: '6px'
+        },
+        actions: {
+          padding: '0 30px',
+          marginLeft: '16px',
+          ...(theme.breakpoints && {
+            [theme.breakpoints.down('sm')]: {
+              marginLeft: '0',
+              marginTop: '8px'
+            }
+          }),
+          '& .MuiIconButton-root': {
+            padding: '8px',
+            backgroundColor: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.03)',
+            borderRadius: '8px',
+            color: theme.darkTextSecondary,
+            margin: '0 4px',
+            '&:hover': {
+              backgroundColor: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.09)' : 'rgba(0, 0, 0, 0.05)'
+            },
+            '&.Mui-disabled': {
+              color: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.26)',
+              backgroundColor: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.02)' : 'rgba(0, 0, 0, 0.01)'
+            }
+          }
+        },
+        displayedRows: {
+          fontSize: '0.875rem',
+          color: theme.darkTextSecondary,
+          margin: 0,
+          ...(theme.breakpoints && {
+            [theme.breakpoints.down('sm')]: {
+              margin: '8px 0'
+            }
+          })
+        }
+      }
+    },
     MuiTooltip: {
       styleOverrides: {
         tooltip: {
           color: theme.colors.paper,
-          background: theme.colors?.grey700
+          background: theme.mode === 'dark' ? theme.colors?.grey700 : '#212121',
+          borderRadius: '6px',
+          fontWeight: 400,
+          fontSize: '0.75rem',
+          padding: '6px 10px',
+          boxShadow: theme.mode === 'dark' ? '0 2px 8px 0 rgba(0,0,0,0.3)' : '0 1px 4px 0 rgba(0,0,0,0.15)'
+        },
+        arrow: {
+          color: theme.mode === 'dark' ? theme.colors?.grey700 : '#212121'
         }
       }
     },
-    MuiCssBaseline: {
-      styleOverrides: `
-      .apexcharts-title-text {
-          fill: ${theme.textDark} !important
+    MuiAlert: {
+      styleOverrides: {
+        root: {
+          fontSize: '0.875rem',
+          borderRadius: '8px',
+          alignItems: 'center',
+          padding: '12px 16px'
+        },
+        icon: {
+          fontSize: '20px',
+          opacity: 1
+        },
+        standardSuccess: {
+          backgroundColor: theme.mode === 'dark' ? 'rgba(16, 185, 129, 0.16)' : theme.colors?.successLight,
+          color: theme.mode === 'dark' ? '#E0F2E9' : theme.colors?.successDark,
+          '& .MuiAlert-icon': {
+            color: theme.mode === 'dark' ? '#34D399' : theme.colors?.successMain
+          }
+        },
+        standardError: {
+          backgroundColor: theme.mode === 'dark' ? 'rgba(239, 68, 68, 0.16)' : theme.colors?.errorLight,
+          color: theme.mode === 'dark' ? '#FEE2E2' : theme.colors?.errorDark,
+          '& .MuiAlert-icon': {
+            color: theme.mode === 'dark' ? '#F87171' : theme.colors?.errorMain
+          }
+        },
+        standardWarning: {
+          backgroundColor: theme.mode === 'dark' ? 'rgba(250, 173, 20, 0.16)' : theme.colors?.warningLight,
+          color: theme.mode === 'dark' ? '#FEF3C7' : theme.colors?.warningDark,
+          '& .MuiAlert-icon': {
+            color: theme.mode === 'dark' ? '#FBBF24' : theme.colors?.warningMain
+          }
+        },
+        standardInfo: {
+          backgroundColor: theme.mode === 'dark' ? 'rgba(8, 132, 221, 0.16)' : 'rgba(8, 132, 221, 0.08)',
+          color: theme.mode === 'dark' ? '#E0F1FF' : theme.colors?.primaryDark,
+          '& .MuiAlert-icon': {
+            color: theme.mode === 'dark' ? '#60B8FF' : theme.colors?.primaryMain
+          }
+        }
+      }
+    },
+    MuiTabs: {
+      styleOverrides: {
+        root: {
+          borderBottom: `1px solid ${theme.divider}`,
+          minHeight: '40px'
+        },
+        indicator: {
+          backgroundColor: theme.colors?.primaryMain,
+          height: 2
+        }
+      }
+    },
+    MuiTab: {
+      styleOverrides: {
+        root: {
+          textTransform: 'none',
+          minHeight: '40px',
+          fontSize: '0.875rem',
+          fontWeight: 500,
+          color: theme.darkTextSecondary,
+          '&.Mui-selected': {
+            color: theme.colors?.primaryMain
+          }
         }
-      .apexcharts-text {
-        fill: ${theme.textDark} !important
       }
-      .apexcharts-legend-text {
-        color: ${theme.textDark} !important
+    },
+    MuiDialog: {
+      styleOverrides: {
+        paper: {
+          borderRadius: '16px',
+          boxShadow: theme.mode === 'dark' ? '0 12px 40px 0 rgba(0,0,0,0.5)' : '0 25px 50px -12px rgba(0,0,0,0.25)',
+          overflow: 'visible',
+          '&.MuiPaper-rounded': {
+            borderRadius: '16px'
+          }
+        },
+        paperWidthXs: {
+          maxWidth: '360px'
+        },
+        paperWidthSm: {
+          maxWidth: '480px'
+        },
+        paperWidthMd: {
+          maxWidth: '640px'
+        }
       }
-      .apexcharts-menu {
-        background: ${theme.backgroundDefault} !important
+    },
+    MuiDialogTitle: {
+      styleOverrides: {
+        root: {
+          fontSize: '1.25rem',
+          fontWeight: 600,
+          padding: '24px 24px 12px',
+          color: theme.textDark
+        }
       }
-      .apexcharts-gridline, .apexcharts-xaxistooltip-background, .apexcharts-yaxistooltip-background {
-        stroke: ${theme.divider} !important;
+    },
+    MuiDialogContent: {
+      styleOverrides: {
+        root: {
+          padding: '12px 24px 24px',
+          fontSize: '0.9375rem',
+          color: theme.darkTextPrimary
+        }
+      }
+    },
+    MuiDialogActions: {
+      styleOverrides: {
+        root: {
+          padding: '12px 24px 24px',
+          '& .MuiButton-root': {
+            minWidth: '100px'
+          }
+        }
+      }
+    },
+    MuiLink: {
+      styleOverrides: {
+        root: {
+          color: theme.colors?.primaryMain,
+          textDecoration: 'none',
+          '&:hover': {
+            textDecoration: 'underline'
+          }
+        }
+      }
+    },
+    MuiBadge: {
+      styleOverrides: {
+        standard: {
+          fontSize: '0.6875rem',
+          fontWeight: 500,
+          lineHeight: 1,
+          minWidth: '18px',
+          height: '18px',
+          padding: '0 5px',
+          borderRadius: '9px'
+        }
+      }
+    },
+    MuiSelect: {
+      styleOverrides: {
+        select: {
+          padding: '14px 16px',
+          display: 'flex',
+          alignItems: 'center'
+        },
+        icon: {
+          top: 'calc(50% - 12px)',
+          right: '12px',
+          color: theme.darkTextSecondary,
+          transition: 'transform 0.2s ease-in-out',
+          '.Mui-focused &': {
+            transform: 'rotate(180deg)'
+          }
+        },
+        iconOpen: {
+          transform: 'rotate(180deg)'
+        }
+      }
+    },
+    MuiMenuItem: {
+      styleOverrides: {
+        root: {
+          fontSize: '0.9375rem',
+          padding: '10px 16px',
+          minHeight: '42px',
+          '&.Mui-selected': {
+            backgroundColor: theme.mode === 'dark' ? 'rgba(8, 132, 221, 0.15)' : 'rgba(8, 132, 221, 0.08)',
+            '&.Mui-focusVisible': {
+              backgroundColor: theme.mode === 'dark' ? 'rgba(8, 132, 221, 0.25)' : 'rgba(8, 132, 221, 0.12)'
+            }
+          }
+        }
+      }
+    },
+    MuiDataGrid: {
+      styleOverrides: {
+        root: {
+          border: 'none',
+          borderRadius: 0,
+          backgroundColor: theme.mode === 'dark' ? theme.paper : theme.colors?.paper,
+          overflow: 'hidden',
+          width: '100%',
+          margin: 0,
+          padding: 0,
+          '& .MuiPaper-root': {
+            borderRadius: 0
+          },
+          '&.MuiPaper-root': {
+            borderRadius: 0
+          },
+          '& .MuiDataGrid-main': {
+            width: '100%',
+            margin: 0,
+            padding: 0,
+            '& .MuiDataGrid-columnHeaders': {
+              borderBottom: `1px dashed ${theme.tableBorderBottom}`,
+              borderRadius: 0,
+              backgroundColor: theme.headBackgroundColor,
+              minHeight: '48px',
+              width: '100%',
+              margin: 0
+            },
+            '& .MuiDataGrid-virtualScroller': {
+              backgroundColor: theme.mode === 'dark' ? theme.paper : '#fff',
+              width: '100%',
+              margin: 0
+            },
+            '& .MuiDataGrid-columnHeadersInner': {
+              width: '100%',
+              margin: 0,
+              padding: 0,
+              '& .MuiDataGrid-columnHeader': {
+                padding: 0,
+                margin: 0,
+                '& .MuiDataGrid-columnHeaderTitleContainer': {
+                  justifyContent: 'center',
+                  padding: '0 16px',
+                  margin: 0
+                },
+                '&:first-of-type': {
+                  '& .MuiDataGrid-columnHeaderTitleContainer': {
+                    paddingLeft: '24px'
+                  }
+                },
+                '&:last-of-type': {
+                  '& .MuiDataGrid-columnHeaderTitleContainer': {
+                    paddingRight: '24px'
+                  }
+                }
+              }
+            },
+            '& .MuiDataGrid-cellContent': {
+              justifyContent: 'center',
+              width: '100%',
+              display: 'flex'
+            }
+          },
+          footerContainer: {
+            borderTop: `1px dashed ${theme.tableBorderBottom}`,
+            minHeight: '56px',
+            backgroundColor: theme.headBackgroundColor,
+            width: '100%',
+            margin: 0,
+            padding: '0 24px',
+            '& .MuiTablePagination-root': {
+              overflow: 'visible',
+              backgroundColor: 'transparent',
+              color: theme.textDark,
+              borderTop: 'none'
+            },
+            '& .MuiToolbar-root': {
+              minHeight: '56px',
+              padding: '0',
+              '& > p:first-of-type': {
+                fontSize: '0.875rem',
+                color: theme.darkTextSecondary
+              }
+            }
+          }
+        },
+        columnHeader: {
+          padding: '12px 16px',
+          fontSize: '0.875rem',
+          fontWeight: 600,
+          color: theme.darkTextSecondary,
+          height: '48px',
+          textAlign: 'center',
+          '&:focus': {
+            outline: 'none'
+          },
+          '&:focus-within': {
+            outline: 'none'
+          }
+        },
+        columnHeaderTitle: {
+          color: theme.darkTextSecondary,
+          fontWeight: 600,
+          fontSize: '0.875rem',
+          textAlign: 'center'
+        },
+        columnSeparator: {
+          color: theme.divider
+        },
+        cell: {
+          fontSize: '0.875rem',
+          padding: '12px 16px',
+          borderBottom: `1px dashed ${theme.tableBorderBottom}`,
+          textAlign: 'center',
+          '&:focus': {
+            outline: 'none'
+          },
+          '&:focus-within': {
+            outline: 'none'
+          }
+        },
+        row: {
+          transition: 'background-color 0.2s ease',
+          '&:hover': {
+            backgroundColor: theme.mode === 'dark' ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.04)'
+          },
+          '&.Mui-selected': {
+            backgroundColor: theme.mode === 'dark' ? 'rgba(8, 132, 221, 0.15)' : 'rgba(8, 132, 221, 0.08)',
+            '&:hover': {
+              backgroundColor: theme.mode === 'dark' ? 'rgba(8, 132, 221, 0.25)' : 'rgba(8, 132, 221, 0.12)'
+            }
+          }
+        },
+        rowCount: {
+          color: theme.darkTextSecondary
+        },
+        selectedRowCount: {
+          color: theme.colors?.primaryMain,
+          fontWeight: 500
+        },
+        toolbarContainer: {
+          backgroundColor: theme.mode === 'dark' ? theme.paper : theme.colors?.paper,
+          padding: '12px 16px',
+          '& .MuiButton-root': {
+            marginRight: '8px'
+          }
+        },
+        panelHeader: {
+          backgroundColor: theme.mode === 'dark' ? theme.paper : theme.colors?.paper,
+          padding: '16px 20px',
+          borderBottom: `1px dashed ${theme.divider}`
+        },
+        panelContent: {
+          padding: '16px 20px'
+        }
+      }
+    },
+    MuiGridFilterForm: {
+      styleOverrides: {
+        root: {
+          padding: '16px 20px'
+        }
+      }
+    },
+    MuiDataGridPanel: {
+      styleOverrides: {
+        root: {
+          backgroundColor: theme.mode === 'dark' ? theme.paper : '#fff',
+          boxShadow: theme.mode === 'dark' ? '0 8px 24px 0 rgba(0,0,0,0.4)' : '0 8px 24px -4px rgba(0,0,0,0.15)',
+          borderRadius: '14px'
+        }
+      }
+    },
+    MuiDataGridColumnHeaderFilterIconButton: {
+      styleOverrides: {
+        root: {
+          color: theme.darkTextSecondary,
+          '&:hover': {
+            backgroundColor: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.04)'
+          }
+        }
+      }
+    },
+    MuiDataGridPanelFooter: {
+      styleOverrides: {
+        root: {
+          padding: '16px 20px',
+          borderTop: `1px dashed ${theme.divider}`
+        }
+      }
+    },
+    MuiDataGridMenuList: {
+      styleOverrides: {
+        root: {
+          backgroundColor: theme.mode === 'dark' ? theme.paper : '#fff',
+          padding: '8px 0'
+        }
+      }
+    },
+    MuiDataGridMenu: {
+      styleOverrides: {
+        root: {
+          '& .MuiPaper-root': {
+            boxShadow: theme.mode === 'dark' ? '0 8px 24px 0 rgba(0,0,0,0.4)' : '0 8px 24px -4px rgba(0,0,0,0.15)',
+            borderRadius: '12px'
+          }
+        }
       }
-      `
     }
   };
 }
diff --git a/web/src/themes/index.js b/web/src/themes/index.js
index c2920726..352731bc 100644
--- a/web/src/themes/index.js
+++ b/web/src/themes/index.js
@@ -7,7 +7,13 @@ import colors from 'assets/scss/_themes-vars.module.scss';
 import componentStyleOverrides from './compStyleOverride';
 import themePalette from './palette';
 import themeTypography from './typography';
-import { varAlpha } from './utils';
+import { varAlpha, createGradient } from './utils';
+
+// 创建自定义渐变背景色
+const customGradients = {
+  primary: createGradient(colors.primaryMain, colors.primaryDark),
+  secondary: createGradient(colors.secondaryMain, colors.secondaryDark)
+};
 
 /**
  * Represent theme style and structure as per Material-UI
@@ -19,6 +25,7 @@ export const theme = (customization) => {
   const options = customization.theme === 'light' ? GetLightOption() : GetDarkOption();
   const themeOption = {
     colors: color,
+    gradients: customGradients,
     ...options,
     customization
   };
@@ -29,13 +36,30 @@ export const theme = (customization) => {
     mixins: {
       toolbar: {
         minHeight: '48px',
-        padding: '16px',
+        padding: '8px 16px',
         '@media (min-width: 600px)': {
           minHeight: '48px'
         }
       }
     },
-    typography: themeTypography(themeOption)
+    shape: {
+      borderRadius: themeOption?.customization?.borderRadius || 12
+    },
+    typography: themeTypography(themeOption),
+    breakpoints: {
+      values: {
+        xs: 0,
+        sm: 600,
+        md: 960,
+        lg: 1280,
+        xl: 1920
+      }
+    },
+    zIndex: {
+      modal: 1300,
+      snackbar: 1400,
+      tooltip: 1500
+    }
   };
 
   const themes = createTheme(themeOptions);
@@ -51,22 +75,22 @@ function GetDarkOption() {
   return {
     mode: 'dark',
     heading: color.darkTextTitle,
-    paper: color.darkLevel2,
-    backgroundDefault: color.darkPaper,
-    background: color.darkBackground,
-    darkTextPrimary: color.darkTextDark,
-    darkTextSecondary: color.darkPrimaryLight,
-    textDark: color.darkTextTitle,
+    paper: '#1A1D23',
+    backgroundDefault: '#13151A',
+    background: '#1E2128',
+    darkTextPrimary: '#E0E4EC',
+    darkTextSecondary: '#A9B2C3',
+    textDark: '#F8F9FC',
     menuSelected: color.primary200,
-    menuSelectedBack: varAlpha(color.primaryMain, 0.16),
-    divider: color.darkDivider,
-    borderColor: color.darkBorderColor,
-    menuButton: color.darkLevel1,
+    menuSelectedBack: varAlpha(color.primaryMain, 0.12),
+    divider: 'rgba(255, 255, 255, 0.1)',
+    borderColor: 'rgba(255, 255, 255, 0.12)',
+    menuButton: '#292D36',
     menuButtonColor: color.primaryMain,
-    menuChip: color.darkLevel1,
-    headBackgroundColor: color.darkTableHeader,
-    headBackgroundColorHover: varAlpha(color.darkTableHeader, 0.4),
-    tableBorderBottom: color.darkDivider
+    menuChip: '#292D36',
+    headBackgroundColor: '#25282F',
+    headBackgroundColorHover: varAlpha('#25282F', 0.08),
+    tableBorderBottom: 'rgba(255, 255, 255, 0.08)'
   };
 }
 
@@ -74,22 +98,22 @@ function GetLightOption() {
   const color = colors;
   return {
     mode: 'light',
-    heading: color.grey900,
-    paper: color.paper,
-    backgroundDefault: color.paper,
-    background: color.primaryLight,
-    darkTextPrimary: color.grey700,
-    darkTextSecondary: color.grey500,
-    textDark: color.grey900,
+    heading: '#202939',
+    paper: '#FFFFFF',
+    backgroundDefault: '#F5F7FA',
+    background: '#F8FAFD',
+    darkTextPrimary: '#3E4555',
+    darkTextSecondary: '#6C7A92',
+    textDark: '#252F40',
     menuSelected: color.primaryMain,
     menuSelectedBack: varAlpha(color.primary200, 0.08),
-    divider: color.grey200,
-    borderColor: color.grey300,
-    menuButton: varAlpha(color.primary200, 0.2),
+    divider: '#E9EDF5',
+    borderColor: '#E0E6ED',
+    menuButton: varAlpha(color.primary200, 0.12),
     menuButtonColor: color.primaryMain,
-    menuChip: color.primaryLight,
-    headBackgroundColor: color.tableBackground,
-    headBackgroundColorHover: varAlpha(color.darkTableHeader, 0.08),
-    tableBorderBottom: color.tableBorderBottom
+    menuChip: '#EEF2F6',
+    headBackgroundColor: '#F5F7FA',
+    headBackgroundColorHover: varAlpha('#F5F7FA', 0.12),
+    tableBorderBottom: '#E9EDF5'
   };
 }
diff --git a/web/src/themes/palette.js b/web/src/themes/palette.js
index 70c78782..64e6fac5 100644
--- a/web/src/themes/palette.js
+++ b/web/src/themes/palette.js
@@ -3,6 +3,8 @@
  * @param {JsonObject} theme Theme customization object
  */
 
+import { createGradient } from './utils';
+
 export default function themePalette(theme) {
   return {
     mode: theme.mode,
@@ -10,43 +12,62 @@ export default function themePalette(theme) {
       black: theme.colors?.darkPaper
     },
     primary: {
+      lighter: theme.mode === 'dark' ? '#60B8FF' : '#F0F7FF',
       light: theme.colors?.primaryLight,
       main: theme.colors?.primaryMain,
       dark: theme.colors?.primaryDark,
       200: theme.colors?.primary200,
-      800: theme.colors?.primary800
+      800: theme.colors?.primary800,
+      contrastText: '#fff',
+      gradient: createGradient(theme.colors?.primaryMain, theme.colors?.primaryDark)
     },
     secondary: {
+      lighter: theme.mode === 'dark' ? '#B4A1FF' : '#F6F0FF',
       light: theme.colors?.secondaryLight,
       main: theme.colors?.secondaryMain,
       dark: theme.colors?.secondaryDark,
       200: theme.colors?.secondary200,
-      800: theme.colors?.secondary800
+      800: theme.colors?.secondary800,
+      contrastText: '#fff',
+      gradient: createGradient(theme.colors?.secondaryMain, theme.colors?.secondaryDark)
     },
     error: {
+      lighter: '#FFEBEE',
       light: theme.colors?.errorLight,
       main: theme.colors?.errorMain,
-      dark: theme.colors?.errorDark
+      dark: theme.colors?.errorDark,
+      contrastText: '#fff',
+      gradient: createGradient(theme.colors?.errorMain, theme.colors?.errorDark)
     },
     orange: {
+      lighter: '#FFF3E0',
       light: theme.colors?.orangeLight,
       main: theme.colors?.orangeMain,
-      dark: theme.colors?.orangeDark
+      dark: theme.colors?.orangeDark,
+      contrastText: theme.mode === 'dark' ? '#fff' : '#000'
     },
     warning: {
+      lighter: '#FFF8E1',
       light: theme.colors?.warningLight,
       main: theme.colors?.warningMain,
-      dark: theme.colors?.warningDark
+      dark: theme.colors?.warningDark,
+      contrastText: theme.mode === 'dark' ? '#000' : '#000',
+      gradient: createGradient(theme.colors?.warningMain, theme.colors?.warningDark)
     },
     success: {
+      lighter: '#E6F8F1',
       light: theme.colors?.successLight,
       200: theme.colors?.success200,
       main: theme.colors?.successMain,
-      dark: theme.colors?.successDark
+      dark: theme.colors?.successDark,
+      contrastText: '#fff',
+      gradient: createGradient(theme.colors?.successMain, theme.colors?.successDark)
     },
     grey: {
       50: theme.colors?.grey50,
       100: theme.colors?.grey100,
+      200: theme.colors?.grey200,
+      300: theme.colors?.grey300,
       500: theme.darkTextSecondary,
       600: theme.heading,
       700: theme.darkTextPrimary,
@@ -63,11 +84,20 @@ export default function themePalette(theme) {
       primary: theme.darkTextPrimary,
       secondary: theme.darkTextSecondary,
       dark: theme.textDark,
-      hint: theme.colors?.grey100
+      hint: theme.colors?.grey100,
+      disabled: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.38)'
     },
+    divider: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.09)',
     background: {
       paper: theme.paper,
       default: theme.backgroundDefault
+    },
+    action: {
+      hover: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.03)',
+      selected: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.16)' : 'rgba(0, 0, 0, 0.06)',
+      disabled: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.26)',
+      disabledBackground: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.08)',
+      focus: theme.mode === 'dark' ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.05)'
     }
   };
 }
diff --git a/web/src/themes/typography.js b/web/src/themes/typography.js
index 6a5fa02c..c2caab78 100644
--- a/web/src/themes/typography.js
+++ b/web/src/themes/typography.js
@@ -5,65 +5,85 @@
 
 export default function themeTypography(theme) {
   return {
-    fontFamily: theme?.customization?.fontFamily,
-    h6: {
-      fontWeight: 500,
-      color: theme.heading,
-      fontSize: '0.75rem'
-    },
-    h5: {
-      fontSize: '0.875rem',
-      color: theme.heading,
-      fontWeight: 500
+    fontFamily:
+      theme?.customization?.fontFamily ||
+      "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
+    h1: {
+      fontWeight: 600,
+      fontSize: '2rem',
+      lineHeight: 1.25,
+      letterSpacing: '-0.01em',
+      color: theme.heading
     },
-    h4: {
-      fontSize: '1rem',
-      color: theme.heading,
-      fontWeight: 600
+    h2: {
+      fontWeight: 600,
+      fontSize: '1.5rem',
+      lineHeight: 1.3,
+      letterSpacing: '0em',
+      color: theme.heading
     },
     h3: {
+      fontWeight: 600,
       fontSize: '1.25rem',
-      color: theme.heading,
-      fontWeight: 600
+      lineHeight: 1.4,
+      color: theme.heading
     },
-    h2: {
-      fontSize: '1.5rem',
-      color: theme.heading,
-      fontWeight: 700
+    h4: {
+      fontWeight: 600,
+      fontSize: '1.125rem',
+      lineHeight: 1.4,
+      color: theme.heading
     },
-    h1: {
-      fontSize: '2.125rem',
-      color: theme.heading,
-      fontWeight: 700
+    h5: {
+      fontWeight: 500,
+      fontSize: '1rem',
+      lineHeight: 1.5,
+      color: theme.heading
+    },
+    h6: {
+      fontWeight: 500,
+      fontSize: '0.875rem',
+      lineHeight: 1.5,
+      color: theme.heading
     },
     subtitle1: {
       fontSize: '0.875rem',
+      lineHeight: 1.5,
       fontWeight: 500,
       color: theme.textDark
     },
     subtitle2: {
       fontSize: '0.75rem',
-      fontWeight: 400,
+      lineHeight: 1.5,
+      fontWeight: 500,
       color: theme.darkTextSecondary
     },
-    caption: {
-      fontSize: '0.75rem',
-      color: theme.darkTextSecondary,
-      fontWeight: 400
-    },
     body1: {
       fontSize: '0.875rem',
-      fontWeight: 400,
-      lineHeight: '1.334em'
+      lineHeight: 1.5,
+      color: theme.darkTextPrimary
     },
     body2: {
-      letterSpacing: '0em',
-      fontWeight: 400,
-      lineHeight: '1.5em',
+      fontSize: '0.75rem',
+      lineHeight: 1.5,
       color: theme.darkTextPrimary
     },
+    caption: {
+      fontSize: '0.75rem',
+      lineHeight: 1.4,
+      color: theme.darkTextSecondary
+    },
+    overline: {
+      fontSize: '0.625rem',
+      fontWeight: 500,
+      lineHeight: 1.5,
+      letterSpacing: '0.5px',
+      textTransform: 'uppercase',
+      color: theme.darkTextSecondary
+    },
     button: {
-      textTransform: 'capitalize'
+      textTransform: 'none',
+      fontWeight: 500
     },
     customInput: {
       marginTop: 1,
@@ -95,10 +115,12 @@ export default function themeTypography(theme) {
       width: '100%',
       minHeight: 'calc(100vh - 88px)',
       flexGrow: 1,
-      padding: '20px',
+      padding: '16px',
+      paddingBottom: '30px',
       marginTop: '88px',
-      marginRight: '20px',
-      borderRadius: `${theme?.customization?.borderRadius}px`
+      marginRight: '0',
+      marginBottom: '20px',
+      borderRadius: '0'
     },
     menuCaption: {
       fontSize: '0.875rem',
@@ -106,32 +128,32 @@ export default function themeTypography(theme) {
       color: theme.heading,
       padding: '6px',
       textTransform: 'capitalize',
-      marginTop: '10px'
+      marginTop: '8px'
     },
     subMenuCaption: {
-      fontSize: '0.6875rem',
+      fontSize: '0.75rem',
       fontWeight: 500,
       color: theme.darkTextSecondary,
       textTransform: 'capitalize'
     },
     commonAvatar: {
       cursor: 'pointer',
-      borderRadius: '8px'
+      borderRadius: '4px'
     },
     smallAvatar: {
-      width: '22px',
-      height: '22px',
-      fontSize: '1rem'
+      width: '24px',
+      height: '24px',
+      fontSize: '0.875rem'
     },
     mediumAvatar: {
-      width: '34px',
-      height: '34px',
-      fontSize: '1.2rem'
+      width: '32px',
+      height: '32px',
+      fontSize: '1rem'
     },
     largeAvatar: {
-      width: '44px',
-      height: '44px',
-      fontSize: '1.5rem'
+      width: '40px',
+      height: '40px',
+      fontSize: '1.25rem'
     },
     menuButton: {
       color: theme.menuButtonColor,
diff --git a/web/src/themes/utils.js b/web/src/themes/utils.js
index ac310c69..492c4375 100644
--- a/web/src/themes/utils.js
+++ b/web/src/themes/utils.js
@@ -21,3 +21,8 @@ export function varAlpha(color, opacity = 1) {
   // 返回带有透明度的RGBA颜色字符串
   return `rgba(${r}, ${g}, ${b}, ${opacity})`;
 }
+
+// 创建渐变色样式
+export function createGradient(color1, color2) {
+  return `linear-gradient(to right, ${color1}, ${color2})`;
+}
diff --git a/web/src/ui-component/AdminContainer.js b/web/src/ui-component/AdminContainer.js
index 9da3166b..080bc217 100644
--- a/web/src/ui-component/AdminContainer.js
+++ b/web/src/ui-component/AdminContainer.js
@@ -4,6 +4,8 @@ import { Container } from '@mui/material';
 const AdminContainer = styled(Container)(({ theme }) => ({
   paddingLeft: '0px !important',
   paddingRight: '0px !important',
+  paddingBottom: '30px !important',
+  marginBottom: '30px !important',
   [theme.breakpoints.up('md')]: {
     maxWidth: '1400px'
   }
diff --git a/web/src/ui-component/TableHead.jsx b/web/src/ui-component/TableHead.jsx
index db2994d8..1af7f914 100644
--- a/web/src/ui-component/TableHead.jsx
+++ b/web/src/ui-component/TableHead.jsx
@@ -33,7 +33,11 @@ const KeywordTableHead = ({ order, orderBy, headLabel, onRequestSort }) => {
               key={headCell.id}
               align={headCell.align || 'right'}
               // sortDirection={orderBy === headCell.id ? order : false}
-              sx={{ width: headCell.width, minWidth: headCell.minWidth }}
+              sx={{
+                width: headCell.width,
+                minWidth: headCell.minWidth,
+                p: '10px 8px'
+              }}
             >
               {headCell.disableSort ? (
                 label(headCell)
diff --git a/web/src/ui-component/confirm-dialog.jsx b/web/src/ui-component/confirm-dialog.jsx
index b8563646..a7e4577e 100644
--- a/web/src/ui-component/confirm-dialog.jsx
+++ b/web/src/ui-component/confirm-dialog.jsx
@@ -19,7 +19,7 @@ export default function ConfirmDialog({ open, title, action, content, onClose, .
       <DialogActions>
         {action}
 
-        <Button variant="contained" color="error" onClick={onClose}>
+        <Button variant="contained" color="inherit" onClick={onClose}>
           {t('common.cancel')}
         </Button>
       </DialogActions>
diff --git a/web/src/ui-component/iconify/iconify.jsx b/web/src/ui-component/iconify/iconify.jsx
new file mode 100644
index 00000000..6cf12a1d
--- /dev/null
+++ b/web/src/ui-component/iconify/iconify.jsx
@@ -0,0 +1,26 @@
+import { forwardRef } from 'react';
+import { Icon, disableCache } from '@iconify/react';
+
+import Box from '@mui/material/Box';
+
+// ----------------------------------------------------------------------
+
+const Iconify = forwardRef(({ width = 20, sx, ...other }, ref) => (
+  <Box
+    ssr
+    ref={ref}
+    component={Icon}
+    sx={{
+      width,
+      height: width,
+      flexShrink: 0,
+      display: 'inline-flex',
+      ...sx
+    }}
+    {...other}
+  />
+));
+
+disableCache('local');
+
+export default Iconify;
diff --git a/web/src/views/Channel/ChannelList.jsx b/web/src/views/Channel/ChannelList.jsx
deleted file mode 100644
index 06d037b5..00000000
--- a/web/src/views/Channel/ChannelList.jsx
+++ /dev/null
@@ -1,496 +0,0 @@
-import { useState, useEffect, useCallback } from 'react';
-import { showError, showSuccess, showInfo, trims } from 'utils/common';
-
-import { useTheme } from '@mui/material/styles';
-import Table from '@mui/material/Table';
-import TableBody from '@mui/material/TableBody';
-import TableContainer from '@mui/material/TableContainer';
-import TablePagination from '@mui/material/TablePagination';
-import LinearProgress from '@mui/material/LinearProgress';
-import ButtonGroup from '@mui/material/ButtonGroup';
-import Toolbar from '@mui/material/Toolbar';
-import useMediaQuery from '@mui/material/useMediaQuery';
-import Alert from '@mui/material/Alert';
-
-import { Button, IconButton, Card, Box, Stack, Container, Typography, Divider } from '@mui/material';
-import ChannelTableRow from './component/TableRow';
-import KeywordTableHead from 'ui-component/TableHead';
-import { API } from 'utils/api';
-import EditeModal from './component/EditModal';
-import { ITEMS_PER_PAGE, PAGE_SIZE_OPTIONS } from 'constants';
-import TableToolBar from './component/TableToolBar';
-import BatchModal from './component/BatchModal';
-import { useTranslation } from 'react-i18next';
-
-import { useBoolean } from 'hooks/use-boolean';
-import ConfirmDialog from 'ui-component/confirm-dialog';
-import { Icon } from '@iconify/react';
-
-const originalKeyword = {
-  type: 0,
-  status: 0,
-  name: '',
-  group: '',
-  models: '',
-  key: '',
-  test_model: '',
-  other: '',
-  filter_tag: false,
-  tag: ''
-};
-
-export async function fetchChannelData(page, rowsPerPage, keyword, order, orderBy) {
-  try {
-    if (orderBy) {
-      orderBy = order === 'desc' ? '-' + orderBy : orderBy;
-    }
-    const res = await API.get(`/api/channel/`, {
-      params: {
-        page: page + 1,
-        size: rowsPerPage,
-        order: orderBy,
-        ...keyword
-      }
-    });
-    const { success, message, data } = res.data;
-    if (success) {
-      return data;
-    } else {
-      showError(message);
-    }
-  } catch (error) {
-    console.error(error);
-  }
-
-  return false;
-}
-
-// ----------------------------------------------------------------------
-// CHANNEL_OPTIONS,
-export default function ChannelList() {
-  const { t } = useTranslation();
-  const [page, setPage] = useState(0);
-  const [order, setOrder] = useState('desc');
-  const [orderBy, setOrderBy] = useState('id');
-  const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE);
-  const [listCount, setListCount] = useState(0);
-  const [searching, setSearching] = useState(false);
-  const [channels, setChannels] = useState([]);
-  const [refreshFlag, setRefreshFlag] = useState(false);
-  const [tags, setTags] = useState([]);
-
-  const confirm = useBoolean();
-  const [confirmTitle, setConfirmTitle] = useState('');
-  const [confirmConfirm, setConfirmConfirm] = useState(() => {});
-
-  const [groupOptions, setGroupOptions] = useState([]);
-  const [toolBarValue, setToolBarValue] = useState(originalKeyword);
-  const [searchKeyword, setSearchKeyword] = useState(originalKeyword);
-
-  const theme = useTheme();
-  const matchUpMd = useMediaQuery(theme.breakpoints.up('sm'));
-  const [openModal, setOpenModal] = useState(false);
-  const [editChannelId, setEditChannelId] = useState(0);
-  const [openBatchModal, setOpenBatchModal] = useState(false);
-
-  const handleSort = (event, id) => {
-    const isAsc = orderBy === id && order === 'asc';
-    if (id !== '') {
-      setOrder(isAsc ? 'desc' : 'asc');
-      setOrderBy(id);
-    }
-  };
-
-  const handleChangePage = (event, newPage) => {
-    setPage(newPage);
-  };
-
-  const handleChangeRowsPerPage = (event) => {
-    setPage(0);
-    setRowsPerPage(parseInt(event.target.value, 10));
-  };
-
-  const searchChannels = async () => {
-    // event.preventDefault();
-    // const formData = new FormData(event.target);
-    setPage(0);
-    setSearchKeyword(toolBarValue);
-  };
-
-  const handleToolBarValue = (event) => {
-    setToolBarValue({ ...toolBarValue, [event.target.name]: event.target.value });
-  };
-
-  const manageChannel = async (id, action, value) => {
-    const url = '/api/channel/';
-    let data = { id };
-    let res;
-
-    try {
-      switch (action) {
-        case 'copy': {
-          let oldRes = await API.get(`/api/channel/${id}`);
-          const { success, message, data } = oldRes.data;
-          if (!success) {
-            showError(message);
-            return;
-          }
-          // 删除 data.id
-          delete data.id;
-          data.name = data.name + '_copy';
-          res = await API.post(`/api/channel/`, { ...data });
-          break;
-        }
-        case 'delete':
-          res = await API.delete(url + id);
-          break;
-        case 'delete_tag':
-          res = await API.delete(url + id + '/tag');
-          break;
-        case 'status':
-          res = await API.put(url, {
-            ...data,
-            status: value
-          });
-          break;
-        case 'priority':
-          if (value === '') {
-            return;
-          }
-          res = await API.put(url, {
-            ...data,
-            priority: parseInt(value)
-          });
-          break;
-        case 'weight':
-          if (value === '') {
-            return;
-          }
-          res = await API.put(url, {
-            ...data,
-            weight: parseInt(value)
-          });
-          break;
-        case 'test':
-          res = await API.get(url + `test/${id}`, {
-            params: { model: value }
-          });
-          break;
-      }
-      const { success, message } = res.data;
-      if (success) {
-        showSuccess(t('userPage.operationSuccess'));
-        if (action === 'delete' || action === 'copy' || action == 'delete_tag') {
-          await handleRefresh(false);
-        }
-      } else {
-        showError(message);
-      }
-
-      return res.data;
-    } catch (error) {
-      return;
-    }
-  };
-
-  // 处理刷新
-  const handleRefresh = async (reset) => {
-    if (reset) {
-      setOrderBy('id');
-      setOrder('desc');
-      setToolBarValue(originalKeyword);
-      setSearchKeyword(originalKeyword);
-    }
-    setRefreshFlag(!refreshFlag);
-  };
-
-  const handlePopoverOpen = useCallback(
-    (title, onConfirm) => {
-      setConfirmTitle(title);
-      setConfirmConfirm(() => onConfirm);
-      confirm.onTrue();
-    },
-    [confirm]
-  );
-
-  // 处理测试所有启用渠道
-  const testAllChannels = async () => {
-    try {
-      const res = await API.get(`/api/channel/test`);
-      const { success, message } = res.data;
-      if (success) {
-        showInfo(t('channel_row.testAllChannel'));
-      } else {
-        showError(message);
-      }
-    } catch (error) {
-      return;
-    }
-  };
-
-  // 处理删除所有禁用渠道
-  const deleteAllDisabledChannels = async () => {
-    try {
-      const res = await API.delete(`/api/channel/disabled`);
-      const { success, message, data } = res.data;
-      if (success) {
-        showSuccess(t('channel_row.delChannelCount', { count: data }));
-        await handleRefresh();
-      } else {
-        showError(message);
-      }
-    } catch (error) {
-      return;
-    }
-  };
-
-  // 处理更新所有启用渠道余额
-  const updateAllChannelsBalance = async () => {
-    setSearching(true);
-    try {
-      const res = await API.get(`/api/channel/update_balance`);
-      const { success, message } = res.data;
-      if (success) {
-        showInfo(t('channel_row.updateChannelBalance'));
-      } else {
-        showError(message);
-      }
-    } catch (error) {
-      console.log(error);
-    }
-
-    setSearching(false);
-  };
-
-  const handleOpenModal = (channelId) => {
-    setEditChannelId(channelId);
-    setOpenModal(true);
-  };
-
-  const handleCloseModal = () => {
-    setOpenModal(false);
-    setEditChannelId(0);
-  };
-
-  const handleOkModal = (status) => {
-    if (status === true) {
-      handleCloseModal();
-      handleRefresh(false);
-    }
-  };
-
-  const fetchData = async (page, rowsPerPage, keyword, order, orderBy) => {
-    setSearching(true);
-    keyword = trims(keyword);
-    const data = await fetchChannelData(page, rowsPerPage, keyword, order, orderBy);
-
-    if (data) {
-      setListCount(data.total_count);
-      setChannels(data.data);
-    }
-    setSearching(false);
-  };
-
-  const fetchGroups = async () => {
-    try {
-      let res = await API.get(`/api/group/`);
-      setGroupOptions(res.data.data);
-    } catch (error) {
-      showError(error.message);
-    }
-  };
-
-  const fetchTags = async () => {
-    try {
-      let res = await API.get(`/api/channel_tag/_all`);
-      const { success, data } = res.data;
-      if (success) {
-        setTags(data);
-      }
-    } catch (error) {
-      showError(error.message);
-    }
-  };
-
-  useEffect(() => {
-    fetchData(page, rowsPerPage, searchKeyword, order, orderBy);
-  }, [page, rowsPerPage, searchKeyword, order, orderBy, refreshFlag]);
-
-  useEffect(() => {
-    fetchGroups().then();
-    fetchTags().then();
-  }, []);
-
-  return (
-    <>
-      <Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
-        <Typography variant="h4">{t('channel_index.channel')}</Typography>
-
-        <ButtonGroup variant="contained" aria-label="outlined small primary button group">
-          <Button color="primary" startIcon={<Icon icon="solar:add-circle-line-duotone" />} onClick={() => handleOpenModal(0)}>
-            {t('channel_index.newChannel')}
-          </Button>
-          <Button color="primary" startIcon={<Icon icon="solar:menu-dots-bold-duotone" />} onClick={() => setOpenBatchModal(true)}>
-            {t('channel_index.batchProcessing')}
-          </Button>
-        </ButtonGroup>
-      </Stack>
-      <Stack mb={5}>
-        <Alert severity="info">
-          {t('channel_index.priorityWeightExplanation')}
-          <br />
-          {t('channel_index.description1')}
-          <br />
-          {t('channel_index.description2')}
-          <br />
-          {t('channel_index.description3')}
-          <br />
-          {t('channel_index.description4')}
-        </Alert>
-      </Stack>
-      <Card>
-        <Box component="form" noValidate>
-          <TableToolBar filterName={toolBarValue} handleFilterName={handleToolBarValue} groupOptions={groupOptions} tags={tags} />
-        </Box>
-
-        <Toolbar
-          sx={{
-            textAlign: 'right',
-            height: 50,
-            display: 'flex',
-            justifyContent: 'space-between',
-            p: (theme) => theme.spacing(0, 1, 0, 3)
-          }}
-        >
-          <Container>
-            {matchUpMd ? (
-              <ButtonGroup variant="outlined" aria-label="outlined small primary button group">
-                <Button onClick={() => handleRefresh(true)} startIcon={<Icon icon="solar:refresh-circle-bold-duotone" width={18} />}>
-                  {t('channel_index.refreshClearSearchConditions')}
-                </Button>
-                <Button onClick={searchChannels} startIcon={<Icon icon="solar:magnifer-bold-duotone" width={18} />}>
-                  {t('channel_index.search')}
-                </Button>
-                <Button
-                  onClick={() => handlePopoverOpen(t('channel_index.testAllChannels'), testAllChannels)}
-                  startIcon={<Icon icon="solar:test-tube-bold-duotone" width={18} />}
-                >
-                  {t('channel_index.testAllChannels')}
-                </Button>
-                <Button
-                  onClick={() => handlePopoverOpen(t('channel_index.updateEnabledBalance'), updateAllChannelsBalance)}
-                  startIcon={<Icon icon="solar:dollar-minimalistic-bold-duotone" width={18} />}
-                >
-                  {t('channel_index.updateEnabledBalance')}
-                </Button>
-                <Button
-                  onClick={() => handlePopoverOpen(t('channel_index.deleteDisabledChannels'), deleteAllDisabledChannels)}
-                  startIcon={<Icon icon="solar:trash-bin-trash-bold-duotone" width={18} />}
-                >
-                  {t('channel_index.deleteDisabledChannels')}
-                </Button>
-              </ButtonGroup>
-            ) : (
-              <Stack
-                direction="row"
-                spacing={0.5}
-                divider={<Divider orientation="vertical" flexItem />}
-                justifyContent="space-around"
-                alignItems="center"
-              >
-                <IconButton onClick={() => handleRefresh(true)} size="large">
-                  <Icon width={20} icon="solar:refresh-circle-bold-duotone" />
-                </IconButton>
-                <IconButton onClick={searchChannels} size="large">
-                  <Icon width={20} icon="solar:magnifer-bold-duotone" />
-                </IconButton>
-                <IconButton onClick={() => handlePopoverOpen(t('channel_index.testAllChannels'), testAllChannels)} size="large">
-                  <Icon width={20} icon="solar:test-tube-bold-duotone" />
-                </IconButton>
-                <IconButton
-                  onClick={() => handlePopoverOpen(t('channel_index.updateEnabledBalance'), updateAllChannelsBalance)}
-                  size="large"
-                >
-                  <Icon width={20} icon="solar:dollar-minimalistic-bold-duotone" />
-                </IconButton>
-                <IconButton
-                  onClick={() => handlePopoverOpen(t('channel_index.deleteDisabledChannels'), deleteAllDisabledChannels)}
-                  size="large"
-                >
-                  <Icon width={20} icon="solar:trash-bin-trash-bold-duotone" />
-                </IconButton>
-              </Stack>
-            )}
-          </Container>
-        </Toolbar>
-        {searching && <LinearProgress />}
-        <TableContainer>
-          <Table sx={{ minWidth: 800 }}>
-            <KeywordTableHead
-              order={order}
-              orderBy={orderBy}
-              onRequestSort={handleSort}
-              headLabel={[
-                { id: 'collapse', label: '', disableSort: true, width: '50px' },
-                { id: 'id', label: 'ID', disableSort: false, width: '80px' },
-                { id: 'name', label: t('channel_index.name'), disableSort: false },
-                { id: 'group', label: t('channel_index.group'), disableSort: true },
-                { id: 'tag', label: t('channel_index.tags'), disableSort: true },
-                { id: 'type', label: t('channel_index.type'), disableSort: false },
-                { id: 'status', label: t('channel_index.status'), disableSort: false },
-                { id: 'response_time', label: t('channel_index.responseTime'), disableSort: false },
-                // { id: 'balance', label: '余额', disableSort: false },
-                { id: 'used', label: t('channel_index.usedBalance'), disableSort: true },
-                { id: 'priority', label: t('channel_index.priority'), disableSort: false, width: '80px' },
-                { id: 'weight', label: t('channel_index.weight'), disableSort: false, width: '80px' },
-                { id: 'action', label: t('channel_index.actions'), disableSort: true }
-              ]}
-            />
-            <TableBody>
-              {channels.map((row) => (
-                <ChannelTableRow
-                  item={row}
-                  manageChannel={manageChannel}
-                  key={row.id}
-                  handleOpenModal={handleOpenModal}
-                  setModalChannelId={setEditChannelId}
-                />
-              ))}
-            </TableBody>
-          </Table>
-        </TableContainer>
-        <TablePagination
-          page={page}
-          component="div"
-          count={listCount}
-          rowsPerPage={rowsPerPage}
-          onPageChange={handleChangePage}
-          rowsPerPageOptions={PAGE_SIZE_OPTIONS}
-          onRowsPerPageChange={handleChangeRowsPerPage}
-          showFirstButton
-          showLastButton
-        />
-      </Card>
-      <EditeModal open={openModal} onCancel={handleCloseModal} onOk={handleOkModal} channelId={editChannelId} groupOptions={groupOptions} />
-      <BatchModal open={openBatchModal} setOpen={setOpenBatchModal} />
-
-      <ConfirmDialog
-        open={confirm.value}
-        onClose={confirm.onFalse}
-        title={confirmTitle}
-        content={t('common.execute', { title: confirmTitle })}
-        action={
-          <Button
-            variant="contained"
-            onClick={() => {
-              confirmConfirm();
-              confirm.onFalse();
-            }}
-          >
-            {t('common.executeConfirm')}
-          </Button>
-        }
-      />
-    </>
-  );
-}
diff --git a/web/src/views/Channel/ChannelTag.jsx b/web/src/views/Channel/ChannelTag.jsx
deleted file mode 100644
index f55360a0..00000000
--- a/web/src/views/Channel/ChannelTag.jsx
+++ /dev/null
@@ -1,240 +0,0 @@
-import { useState, useEffect } from 'react'; //
-import { showError, trims, showSuccess } from 'utils/common';
-
-import Table from '@mui/material/Table';
-import TableBody from '@mui/material/TableBody';
-import TableContainer from '@mui/material/TableContainer';
-import TablePagination from '@mui/material/TablePagination';
-import LinearProgress from '@mui/material/LinearProgress';
-import Toolbar from '@mui/material/Toolbar';
-
-import EditeModal from './component/EditModal';
-import { Card, Stack, Typography } from '@mui/material';
-import TagTableRow from './component/TagTableRow';
-import KeywordTableHead from 'ui-component/TableHead';
-import { API } from 'utils/api';
-import { ITEMS_PER_PAGE, PAGE_SIZE_OPTIONS } from 'constants';
-import { useTranslation } from 'react-i18next';
-// import TableToolBar from 'ui-component/TableToolBar';
-
-const originalKeyword = {
-  tag: ''
-};
-
-export default function ChannelTag() {
-  const { t } = useTranslation();
-  const [page, setPage] = useState(0);
-  const [order, setOrder] = useState('desc');
-  const [orderBy, setOrderBy] = useState('id');
-  const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE);
-  const [listCount, setListCount] = useState(0);
-  const [channelTags, setChannelTags] = useState([]);
-  const [refreshFlag, setRefreshFlag] = useState(false);
-  const [searching, setSearching] = useState(false);
-  const [searchKeyword, setSearchKeyword] = useState(originalKeyword);
-  const [groupOptions, setGroupOptions] = useState([]);
-
-  const [openModal, setOpenModal] = useState(false);
-  const [editChannelId, setEditChannelId] = useState('');
-
-  const handleSort = (event, id) => {
-    const isAsc = orderBy === id && order === 'asc';
-    if (id !== '') {
-      setOrder(isAsc ? 'desc' : 'asc');
-      setOrderBy(id);
-    }
-  };
-
-  const handleChangePage = (event, newPage) => {
-    setPage(newPage);
-  };
-
-  const handleChangeRowsPerPage = (event) => {
-    setPage(0);
-    setRowsPerPage(parseInt(event.target.value, 10));
-  };
-
-  // const searchRedemptions = async (event) => {
-  //   event.preventDefault();
-  //   const formData = new FormData(event.target);
-  //   setPage(0);
-  //   setSearchKeyword(formData.get('keyword'));
-  // };
-
-  const handleRefresh = async () => {
-    setOrderBy('id');
-    setOrder('desc');
-    // setToolBarValue(originalKeyword);
-    setSearchKeyword(originalKeyword);
-    setRefreshFlag(!refreshFlag);
-  };
-
-  const handleCloseModal = () => {
-    setOpenModal(false);
-    setEditChannelId(0);
-  };
-
-  const handleOkModal = (status) => {
-    if (status === true) {
-      handleCloseModal();
-      handleRefresh();
-    }
-  };
-
-  const handleOpenModal = (channelId) => {
-    setEditChannelId(channelId);
-    setOpenModal(true);
-  };
-
-  const manageChannel = async (id, action, value) => {
-    const url = '/api/channel_tag/';
-    let res;
-
-    try {
-      switch (action) {
-        case 'delete':
-          res = await API.delete(url + encodeURIComponent(id));
-          break;
-        case 'priority':
-          res = await API.put(`${url + encodeURIComponent(id)}/priority`, {
-            type: 'priority',
-            value
-          });
-          break;
-        default:
-          return null;
-      }
-      const { success, message } = res.data;
-      if (success) {
-        showSuccess(t('userPage.operationSuccess'));
-        if (action === 'delete' || action === 'copy') {
-          await handleRefresh();
-        }
-      } else {
-        showError(message);
-      }
-
-      return res.data;
-    } catch (error) {
-      return;
-    }
-  };
-
-  const fetchGroups = async () => {
-    try {
-      let res = await API.get(`/api/group/`);
-      setGroupOptions(res.data.data);
-    } catch (error) {
-      showError(error.message);
-    }
-  };
-
-  const fetchData = async (page, rowsPerPage, keyword, order, orderBy) => {
-    setSearching(true);
-    keyword = trims(keyword);
-    try {
-      if (orderBy) {
-        orderBy = order === 'desc' ? '-' + orderBy : orderBy;
-      }
-      const res = await API.get(`/api/channel_tag/`, {
-        params: {
-          page: page + 1,
-          size: rowsPerPage,
-          order: orderBy,
-          ...keyword
-        }
-      });
-      const { success, message, data } = res.data;
-      if (success) {
-        setChannelTags(data.data);
-        setListCount(data.total_count);
-      } else {
-        showError(message);
-      }
-    } catch (error) {
-      console.error(error);
-    }
-
-    setSearching(false);
-  };
-
-  useEffect(() => {
-    fetchData(page, rowsPerPage, searchKeyword, order, orderBy);
-  }, [page, rowsPerPage, searchKeyword, order, orderBy, refreshFlag]); //
-
-  useEffect(() => {
-    fetchGroups().then();
-  }, []);
-
-  return (
-    <>
-      <Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
-        <Typography variant="h4">{t('channel_index.channelTags')}</Typography>
-      </Stack>
-      <Card>
-        {/* <Box component="form" onSubmit={searchRedemptions} noValidate>
-          <TableToolBar placeholder={'搜索标签名称...'} />
-        </Box> */}
-
-        <Toolbar
-          sx={{
-            textAlign: 'right',
-            height: 50,
-            display: 'flex',
-            justifyContent: 'space-between',
-            p: (theme) => theme.spacing(0, 1, 0, 3)
-          }}
-        ></Toolbar>
-        {searching && <LinearProgress />}
-        <TableContainer>
-          <Table sx={{ minWidth: 800 }}>
-            <KeywordTableHead
-              order={order}
-              orderBy={orderBy}
-              onRequestSort={handleSort}
-              headLabel={[
-                { id: 'collapse', label: '', disableSort: true, width: '50px' },
-                { id: 'Tag', label: t('channel_index.tags'), disableSort: false },
-                { id: 'type', label: t('channel_index.supplier'), disableSort: false },
-                { id: 'group', label: t('channel_index.group'), disableSort: true, width: '80px' },
-                { id: 'models', label: t('channel_index.model'), disableSort: false },
-                { id: 'priority', label: t('channel_index.priority'), disableSort: false },
-                { id: 'action', label: t('channel_index.actions'), disableSort: true }
-              ]}
-            />
-            <TableBody>
-              {channelTags.map((row) => (
-                <TagTableRow
-                  item={row}
-                  manageChannel={manageChannel}
-                  key={'TagTableRow-' + row.tag}
-                  handleOpenModal={handleOpenModal}
-                  setModalChannelId={setEditChannelId}
-                />
-              ))}
-            </TableBody>
-          </Table>
-        </TableContainer>
-        <TablePagination
-          page={page}
-          component="div"
-          count={listCount}
-          rowsPerPage={rowsPerPage}
-          onPageChange={handleChangePage}
-          rowsPerPageOptions={PAGE_SIZE_OPTIONS}
-          onRowsPerPageChange={handleChangeRowsPerPage}
-          showFirstButton
-          showLastButton
-        />
-      </Card>
-      <EditeModal
-        open={openModal}
-        onCancel={handleCloseModal}
-        onOk={handleOkModal}
-        channelId={editChannelId}
-        groupOptions={groupOptions}
-        isTag={true}
-      />
-    </>
-  );
-}
diff --git a/web/src/views/Channel/component/BatchAzureAPI.jsx b/web/src/views/Channel/component/BatchAzureAPI.jsx
index 0bfd0749..b6a22323 100644
--- a/web/src/views/Channel/component/BatchAzureAPI.jsx
+++ b/web/src/views/Channel/component/BatchAzureAPI.jsx
@@ -2,7 +2,7 @@ import { useState } from 'react';
 import { Grid, TextField, InputAdornment, Checkbox, Button, FormControlLabel, IconButton } from '@mui/material';
 import { gridSpacing } from 'store/constant';
 import { IconSearch, IconSend } from '@tabler/icons-react';
-import { fetchChannelData } from '../ChannelList';
+import { fetchChannelData } from '../index';
 import { API } from 'utils/api';
 import { showError, showSuccess } from 'utils/common';
 import { useTranslation } from 'react-i18next';
diff --git a/web/src/views/Channel/component/BatchDelModel.jsx b/web/src/views/Channel/component/BatchDelModel.jsx
index 84f5cd7e..d8bb249a 100644
--- a/web/src/views/Channel/component/BatchDelModel.jsx
+++ b/web/src/views/Channel/component/BatchDelModel.jsx
@@ -2,7 +2,7 @@ import { useState } from 'react';
 import { Grid, TextField, InputAdornment, Checkbox, Button, FormControlLabel, IconButton, Alert } from '@mui/material';
 import { gridSpacing } from 'store/constant';
 import { IconSearch, IconTrash } from '@tabler/icons-react';
-import { fetchChannelData } from '../ChannelList';
+import { fetchChannelData } from '../index';
 import { API } from 'utils/api';
 import { showError, showSuccess } from 'utils/common';
 import { useTranslation } from 'react-i18next';
diff --git a/web/src/views/Channel/component/EditModal.jsx b/web/src/views/Channel/component/EditModal.jsx
index 30871b4f..3f9381ea 100644
--- a/web/src/views/Channel/component/EditModal.jsx
+++ b/web/src/views/Channel/component/EditModal.jsx
@@ -73,7 +73,7 @@ const getValidationSchema = (t) =>
     model_headers: Yup.array()
   });
 
-const EditModal = ({ open, channelId, onCancel, onOk, groupOptions, isTag }) => {
+const EditModal = ({ open, channelId, onCancel, onOk, groupOptions, isTag, modelOptions }) => {
   const { t } = useTranslation();
   const { t: customizeT } = useCustomizeT();
   const theme = useTheme();
@@ -81,7 +81,6 @@ const EditModal = ({ open, channelId, onCancel, onOk, groupOptions, isTag }) =>
   const [initialInput, setInitialInput] = useState(defaultConfig.input);
   const [inputLabel, setInputLabel] = useState(defaultConfig.inputLabel); //
   const [inputPrompt, setInputPrompt] = useState(defaultConfig.prompt);
-  const [modelOptions, setModelOptions] = useState([]);
   const [batchAdd, setBatchAdd] = useState(false);
   const [providerModelsLoad, setProviderModelsLoad] = useState(false);
   const [hasTag, setHasTag] = useState(false);
@@ -177,31 +176,6 @@ const EditModal = ({ open, channelId, onCancel, onOk, groupOptions, isTag }) =>
     setProviderModelsLoad(false);
   };
 
-  const fetchModels = async () => {
-    try {
-      let res = await API.get(`/api/channel/models`);
-      const { data } = res.data;
-      // 先对data排序
-      data.sort((a, b) => {
-        const ownedByComparison = a.owned_by.localeCompare(b.owned_by);
-        if (ownedByComparison === 0) {
-          return a.id.localeCompare(b.id);
-        }
-        return ownedByComparison;
-      });
-      setModelOptions(
-        data.map((model) => {
-          return {
-            id: model.id,
-            group: model.owned_by
-          };
-        })
-      );
-    } catch (error) {
-      showError(error.message);
-    }
-  };
-
   const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
     setSubmitting(true);
     values = trims(values);
@@ -394,20 +368,19 @@ const EditModal = ({ open, channelId, onCancel, onOk, groupOptions, isTag }) =>
   };
 
   useEffect(() => {
-    fetchModels().then();
-  }, []);
-
-  useEffect(() => {
-    setBatchAdd(isTag);
-    if (channelId) {
-      loadChannel().then();
-    } else {
-      setHasTag(false);
-      initChannel(1);
-      setInitialInput({ ...defaultConfig.input, is_edit: false });
+    if (open) {
+      setBatchAdd(isTag);
+      if (channelId) {
+        loadChannel().then();
+      } else {
+        setHasTag(false);
+        initChannel(1);
+        setInitialInput({ ...defaultConfig.input, is_edit: false });
+      }
     }
+
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [channelId]);
+  }, [channelId, open]);
 
   return (
     <Dialog open={open} onClose={onCancel} fullWidth maxWidth={'md'}>
@@ -515,37 +488,20 @@ const EditModal = ({ open, channelId, onCancel, onOk, groupOptions, isTag }) =>
                 </Container>
               )}
 
-              {!isTag && inputPrompt.base_url && (
+              {inputPrompt.base_url && (
                 <FormControl fullWidth error={Boolean(touched.base_url && errors.base_url)} sx={{ ...theme.typography.otherInput }}>
-                  {!batchAdd ? (
-                    <>
-                      <InputLabel htmlFor="channel-base_url-label">{customizeT(inputLabel.base_url)}</InputLabel>
-                      <OutlinedInput
-                        id="channel-base_url-label"
-                        label={customizeT(inputLabel.base_url)}
-                        type="text"
-                        value={values.base_url}
-                        name="base_url"
-                        onBlur={handleBlur}
-                        onChange={handleChange}
-                        inputProps={{}}
-                        aria-describedby="helper-text-channel-base_url-label"
-                      />
-                    </>
-                  ) : (
-                    <TextField
-                      multiline
-                      id="channel-base_url-label"
-                      label={customizeT(inputLabel.base_url)}
-                      value={values.base_url}
-                      name="base_url"
-                      onBlur={handleBlur}
-                      onChange={handleChange}
-                      aria-describedby="helper-text-channel-base_url-label"
-                      minRows={5}
-                      placeholder={customizeT(inputPrompt.base_url) + t('channel_edit.batchBaseurlTip')}
-                    />
-                  )}
+                  <InputLabel htmlFor="channel-base_url-label">{customizeT(inputLabel.base_url)}</InputLabel>
+                  <OutlinedInput
+                    id="channel-base_url-label"
+                    label={customizeT(inputLabel.base_url)}
+                    type="text"
+                    value={values.base_url}
+                    name="base_url"
+                    onBlur={handleBlur}
+                    onChange={handleChange}
+                    inputProps={{}}
+                    aria-describedby="helper-text-channel-base_url-label"
+                  />
 
                   {touched.base_url && errors.base_url ? (
                     <FormHelperText error id="helper-tex-channel-base_url-label">
@@ -722,6 +678,7 @@ const EditModal = ({ open, channelId, onCancel, onOk, groupOptions, isTag }) =>
               >
                 <ButtonGroup variant="outlined" aria-label="small outlined primary button group">
                   <Button
+                    size="small"
                     onClick={() => {
                       const modelString = values.models.map((model) => model.id).join(',');
                       copy(modelString);
@@ -731,6 +688,7 @@ const EditModal = ({ open, channelId, onCancel, onOk, groupOptions, isTag }) =>
                   </Button>
                   <Button
                     disabled={hasTag}
+                    size="small"
                     onClick={() => {
                       setFieldValue('models', basicModels(values.type));
                     }}
@@ -739,6 +697,7 @@ const EditModal = ({ open, channelId, onCancel, onOk, groupOptions, isTag }) =>
                   </Button>
                   <Button
                     disabled={hasTag}
+                    size="small"
                     onClick={() => {
                       setFieldValue('models', modelOptions);
                     }}
@@ -750,6 +709,7 @@ const EditModal = ({ open, channelId, onCancel, onOk, groupOptions, isTag }) =>
                       <LoadingButton
                         loading={providerModelsLoad}
                         disabled={hasTag}
+                        size="small"
                         onClick={() => {
                           getProviderModels(values, setFieldValue);
                         }}
@@ -1052,5 +1012,6 @@ EditModal.propTypes = {
   onCancel: PropTypes.func,
   onOk: PropTypes.func,
   groupOptions: PropTypes.array,
-  isTag: PropTypes.bool
+  isTag: PropTypes.bool,
+  modelOptions: PropTypes.array
 };
diff --git a/web/src/views/Channel/component/TableRow.jsx b/web/src/views/Channel/component/TableRow.jsx
index d5bbc213..d1cdc385 100644
--- a/web/src/views/Channel/component/TableRow.jsx
+++ b/web/src/views/Channel/component/TableRow.jsx
@@ -1,10 +1,15 @@
 import PropTypes from 'prop-types';
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
 
-import { showInfo, showError } from 'utils/common';
+import { showInfo, showError, showSuccess } from 'utils/common';
 import { API } from 'utils/api';
 import { CHANNEL_OPTIONS } from 'constants/ChannelConstants';
 import { useTranslation } from 'react-i18next';
+import { useBoolean } from 'src/hooks/use-boolean';
+import ConfirmDialog from 'ui-component/confirm-dialog';
+import EditeModal from './EditModal';
+import PerfectScrollbar from 'react-perfect-scrollbar';
+import { usePopover } from 'hooks/use-popover';
 
 import {
   Popover,
@@ -25,11 +30,20 @@ import {
   TextField,
   Stack,
   Menu,
-  Box
+  Box,
+  Switch,
+  InputAdornment,
+  CircularProgress,
+  Table,
+  TableBody,
+  TableHead,
+  Checkbox,
+  TablePagination,
+  MenuList
 } from '@mui/material';
 
 import Label from 'ui-component/Label';
-import TableSwitch from 'ui-component/Switch';
+// import TableSwitch from 'ui-component/Switch';
 
 import ResponseTimeLabel from './ResponseTimeLabel';
 import GroupLabel from './GroupLabel';
@@ -91,15 +105,36 @@ function statusInfo(t, status) {
   }
 }
 
-export default function ChannelTableRow({ item, manageChannel, handleOpenModal, setModalChannelId, hideEdit }) {
+export default function ChannelTableRow({ item, manageChannel, onRefresh, groupOptions, modelOptions }) {
   const { t } = useTranslation();
-  const [open, setOpen] = useState(null);
+  const popover = usePopover();
+  const confirmDelete = useBoolean();
+  const check = useBoolean();
+  const updateBalanceOption = useBoolean();
+
   const [openTest, setOpenTest] = useState(false);
-  const [openDelete, setOpenDelete] = useState(false);
+  // const [openDelete, setOpenDelete] = useState(false);
   const [openCheck, setOpenCheck] = useState(false);
   const [statusSwitch, setStatusSwitch] = useState(item.status);
-  const [priorityValve, setPriority] = useState(item.priority);
-  const [weightValve, setWeight] = useState(item.weight);
+
+  const [priority, setPriority] = useState(item.priority);
+  const [weight, setWeight] = useState(item.weight);
+  const tagDeleteConfirm = useBoolean();
+  const quickEdit = useBoolean();
+  const [totalTagChannels, setTotalTagChannels] = useState(0);
+  const [isTagChannelsLoading, setIsTagChannelsLoading] = useState(false);
+  const [tagChannels, setTagChannels] = useState([]);
+  const [selectedChannels, setSelectedChannels] = useState([]);
+  const [currentTestingChannel, setCurrentTestingChannel] = useState(null);
+  const [tagPage, setTagPage] = useState(0);
+  const [tagRowsPerPage, setTagRowsPerPage] = useState(10);
+  const tagModelPopover = usePopover();
+
+  const batchConfirm = useBoolean();
+
+  const tagStatusConfirm = useBoolean();
+  const [statusChangeAction, setStatusChangeAction] = useState('');
+
   const [responseTimeData, setResponseTimeData] = useState({ test_time: item.test_time, response_time: item.response_time });
   const [itemBalance, setItemBalance] = useState(item.balance);
 
@@ -108,63 +143,136 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
   modelMap = item.models.split(',');
   modelMap.sort();
 
-  const handleDeleteOpen = () => {
-    handleCloseMenu();
-    setOpenDelete(true);
-  };
+  const fetchTagChannels = useCallback(async () => {
+    if (!item.tag) return;
 
-  const handleDeleteClose = () => {
-    setOpenDelete(false);
+    try {
+      setIsTagChannelsLoading(true);
+      const response = await API.get(`/api/channel_tag/${item.tag}/list`);
+      if (response.data.success) {
+        const data = response.data.data || [];
+        setTagChannels(data);
+        setTotalTagChannels(data.length);
+      } else {
+        showError(t('channel_row.getTagChannelsError', { message: response.data.message }));
+      }
+    } catch (error) {
+      showError(t('channel_row.getTagChannelsErrorTip', { message: error.message }));
+    } finally {
+      setIsTagChannelsLoading(false);
+    }
+  }, [item.tag, t]);
+
+  const handleChangeTagPage = (event, newPage) => {
+    setTagPage(newPage);
   };
 
-  const handleOpenMenu = (event) => {
-    setOpen(event.currentTarget);
+  const handleChangeTagRowsPerPage = (event) => {
+    setTagRowsPerPage(parseInt(event.target.value, 10));
+    setTagPage(0);
   };
 
-  const handleTestModel = (event) => {
-    setOpenTest(event.currentTarget);
+  const handleToggleChannel = (channelId) => {
+    setSelectedChannels((prev) => (prev.includes(channelId) ? prev.filter((id) => id !== channelId) : [...prev, channelId]));
   };
 
-  const handleCloseMenu = () => {
-    setOpen(null);
+  const handleToggleAll = () => {
+    if (selectedChannels.length === tagChannels.length) {
+      setSelectedChannels([]);
+    } else {
+      setSelectedChannels(tagChannels.map((channel) => channel.id));
+    }
   };
 
-  const handleStatus = async () => {
-    const switchVlue = statusSwitch === 1 ? 2 : 1;
-    const { success } = await manageChannel(item.id, 'status', switchVlue);
+  const handleTagChannelStatus = async (channelId, currentStatus) => {
+    const newStatus = currentStatus === 1 ? 2 : 1;
+    const { success } = await manageChannel(channelId, 'status', newStatus);
     if (success) {
-      setStatusSwitch(switchVlue);
+      // 更新本地状态
+      setTagChannels((prev) => prev.map((channel) => (channel.id === channelId ? { ...channel, status: newStatus } : channel)));
     }
   };
 
-  const handlePriority = async (event) => {
-    const currentValue = parseInt(event.target.value);
-    if (isNaN(currentValue) || currentValue === priorityValve) {
-      return;
+  // 处理子渠道的优先级变更
+  const handleTagChannelPriorityChange = (channelId, value) => {
+    // 更新本地UI状态
+    setTagChannels((prev) => prev.map((c) => (c.id === channelId ? { ...c, priority: value } : c)));
+  };
+
+  const handleTagChannelTest = async (channel) => {
+    const models = channel.models.split(',');
+    if (models.length === 1) {
+      // 如果只有一个模型，直接测速
+      const testModel = models[0];
+      const { success, time } = await manageChannel(channel.id, 'test', testModel);
+      if (success) {
+        showInfo(t('channel_row.modelTestSuccess', { channel: channel.name, model: testModel, time: time.toFixed(2) }));
+        // 更新本地状态
+        setTagChannels((prev) =>
+          prev.map((c) =>
+            c.id === channel.id
+              ? {
+                  ...c,
+                  test_time: Date.now() / 1000,
+                  response_time: time * 1000
+                }
+              : c
+          )
+        );
+      }
+    } else {
+      // 多个模型，显示模型列表
+      setCurrentTestingChannel(channel);
+      tagModelPopover.onOpen();
     }
+  };
 
-    if (currentValue < 0) {
-      showError(t('channel_row.priorityTip'));
+  const handleBatchDelete = async () => {
+    if (!selectedChannels.length) {
+      showError(t('channel_row.batchAddIDRequired'));
       return;
     }
 
-    await manageChannel(item.id, 'priority', currentValue);
-    setPriority(currentValue);
+    batchConfirm.onTrue();
   };
 
-  const handleWeight = async (event) => {
-    const currentValue = parseInt(event.target.value);
-    if (isNaN(currentValue) || currentValue === weightValve) {
-      return;
+  const executeBatchDelete = async () => {
+    try {
+      // 这里需要实现批量删除的API调用
+      const { success, message } = await manageChannel(0, 'batch_delete', selectedChannels, false);
+      if (success) {
+        showInfo(t('channel_row.batchDeleteSuccess'));
+        setSelectedChannels([]);
+        fetchTagChannels(); // 重新获取数据
+        onRefresh(false); // 刷新父组件数据
+      } else {
+        showError(t('channel_row.batchDeleteError', { message }));
+      }
+    } catch (error) {
+      showError(t('channel_row.batchDeleteErrorTip', { message: error.message }));
     }
+  };
 
-    if (currentValue < 1) {
-      showError(t('channel_row.weightTip'));
-      return;
+  useEffect(() => {
+    if (openRow && item.tag) {
+      fetchTagChannels();
     }
+  }, [openRow, item.tag, fetchTagChannels]);
 
-    await manageChannel(item.id, 'weight', currentValue);
-    setWeight(currentValue);
+  const handleTestModel = (event) => {
+    setOpenTest(event.currentTarget);
+  };
+
+  const handleDeleteRow = useCallback(async () => {
+    await manageChannel(item.id, 'delete', '');
+  }, [manageChannel, item.id]);
+
+  const handleStatus = async () => {
+    const switchVlue = statusSwitch === 1 ? 2 : 1;
+    const { success } = await manageChannel(item.id, 'status', switchVlue);
+    if (success) {
+      setStatusSwitch(switchVlue);
+    }
   };
 
   const handleResponseTime = async (modelName) => {
@@ -185,11 +293,6 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
     }
   };
 
-  const handleDeleteTag = async () => {
-    handleCloseMenu();
-    await manageChannel(item.id, 'delete_tag', '');
-  };
-
   const updateChannelBalance = async () => {
     try {
       const res = await API.get(`/api/channel/update_balance/${item.id}`);
@@ -206,11 +309,6 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
     }
   };
 
-  const handleDelete = async () => {
-    handleCloseMenu();
-    await manageChannel(item.id, 'delete', '');
-  };
-
   useEffect(() => {
     setStatusSwitch(item.status);
     setPriority(item.priority);
@@ -222,21 +320,49 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
   return (
     <>
       <TableRow tabIndex={item.id}>
-        <TableCell>
-          <IconButton aria-label="expand row" size="small" onClick={() => setOpenRow(!openRow)}>
-            {openRow ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
-          </IconButton>
+        <TableCell sx={{ minWidth: 50, textAlign: 'center' }}>
+          <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5 }}>
+            <IconButton aria-label="expand row" size="small" onClick={() => setOpenRow(!openRow)}>
+              {openRow ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
+            </IconButton>
+            {item.tag ? (
+              <Label color="primary" variant="soft" sx={{ fontSize: '0.75rem', fontWeight: 600 }}>
+                {t('channel_row.tag')}
+              </Label>
+            ) : (
+              item.id
+            )}
+          </Box>
+        </TableCell>
+        <TableCell sx={{ minWidth: 100, maxWidth: 220, overflow: 'hidden' }}>
+          {item.tag ? (
+            <Typography
+              variant="subtitle1"
+              sx={{
+                color: 'primary.main',
+                maxWidth: 100,
+                display: 'block'
+              }}
+            >
+              {item.tag}
+            </Typography>
+          ) : (
+            <Typography
+              variant="subtitle1"
+              sx={{
+                maxWidth: 100,
+                lineHeight: 1.4
+              }}
+            >
+              {item.name}
+            </Typography>
+          )}
         </TableCell>
-
-        <TableCell>{item.id}</TableCell>
-        <TableCell>{item.name}</TableCell>
 
         <TableCell>
           <GroupLabel group={item.group} />
         </TableCell>
 
-        <TableCell>{item.tag && <Label key={item.tag}>{item.tag}</Label>}</TableCell>
-
         <TableCell>
           {!CHANNEL_OPTIONS[item.type] ? (
             <Label color="error" variant="outlined">
@@ -248,115 +374,255 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
             </Label>
           )}
         </TableCell>
-        <TableCell>
-          <TableSwitch id={`switch-${item.id}`} checked={statusSwitch === 1} onChange={handleStatus} />
-          {statusInfo(t, statusSwitch)}
+
+        <TableCell align="center" sx={{ minWidth: 90 }}>
+          {!item.tag && (
+            <Stack direction="column" alignItems="center" spacing={0.5}>
+              <Switch checked={statusSwitch === 1} onChange={handleStatus} size="small" />
+              <Typography
+                variant="caption"
+                sx={{
+                  fontWeight: statusSwitch === 1 ? 600 : 400,
+                  color: statusSwitch === 1 ? 'success.main' : 'text.secondary'
+                }}
+              >
+                {statusInfo(t, statusSwitch)}
+              </Typography>
+            </Stack>
+          )}
+          {item.tag && (
+            <Stack direction="row" spacing={1} justifyContent="center">
+              <Tooltip title={t('channel_row.enableAllChannels')} placement="top">
+                <IconButton
+                  size="small"
+                  onClick={() => {
+                    setStatusChangeAction('enable');
+                    tagStatusConfirm.onTrue();
+                  }}
+                  sx={{ color: 'success.main' }}
+                >
+                  <Icon icon="mdi:power" />
+                </IconButton>
+              </Tooltip>
+              <Tooltip title={t('channel_row.disableAllChannels')} placement="top">
+                <IconButton
+                  size="small"
+                  onClick={() => {
+                    setStatusChangeAction('disable');
+                    tagStatusConfirm.onTrue();
+                  }}
+                  sx={{ color: 'error.main' }}
+                >
+                  <Icon icon="mdi:power-off" />
+                </IconButton>
+              </Tooltip>
+            </Stack>
+          )}
         </TableCell>
 
-        <TableCell>
-          <ResponseTimeLabel
-            test_time={responseTimeData.test_time}
-            response_time={responseTimeData.response_time}
-            handle_action={handleResponseTime}
-          />
+        <TableCell sx={{ minWidth: 90, textAlign: 'center' }}>
+          {!item.tag && (
+            <ResponseTimeLabel
+              test_time={responseTimeData.test_time}
+              response_time={responseTimeData.response_time}
+              handle_action={handleResponseTime}
+            />
+          )}
         </TableCell>
         {/* <TableCell>
           
         </TableCell> */}
         <TableCell>
-          {renderQuota(item.used_quota)}
-          <Tooltip title={t('channel_row.clickUpdateQuota')} placement="top" onClick={updateChannelBalance}>
-            {renderBalance(item.type, itemBalance)}
-          </Tooltip>
-        </TableCell>
-        <TableCell>
-          <TextField
-            id={`priority-${item.id}`}
-            onBlur={handlePriority}
-            type="number"
-            label={t('channel_index.priority')}
-            variant="standard"
-            defaultValue={item.priority}
-            inputProps={{ min: '0' }}
-          />
+          {!item.tag && (
+            <Stack spacing={0.5} alignItems="center">
+              <Typography variant="body1">{renderQuota(item.used_quota)}</Typography>
+              <Typography
+                variant="caption"
+                sx={{
+                  color: 'success.main',
+                  fontWeight: 600,
+                  cursor: 'pointer',
+                  '&:hover': { textDecoration: 'underline' }
+                }}
+                onClick={updateChannelBalance}
+              >
+                {renderBalance(item.type, itemBalance)}
+              </Typography>
+            </Stack>
+          )}
         </TableCell>
-        <TableCell>
-          <TextField
-            id={`weight-${item.id}`}
-            onBlur={handleWeight}
-            type="number"
-            label={t('channel_index.weight')}
-            variant="standard"
-            defaultValue={item.weight}
-            inputProps={{ min: '1' }}
-          />
+
+        <TableCell colSpan={item.tag ? 2 : 1}>
+          <Box sx={{ display: 'flex' }}>
+            <TextField
+              id={`priority-${item.id}`}
+              type="number"
+              label={t('channel_index.priority')}
+              variant="outlined"
+              size="small"
+              value={priority}
+              onChange={(e) => setPriority(Number(e.target.value))}
+              inputProps={{
+                min: '0'
+              }}
+              sx={{ width: '90px' }}
+              InputProps={{
+                endAdornment: (
+                  <InputAdornment position="end">
+                    <IconButton
+                      size="small"
+                      color="primary"
+                      onClick={() => {
+                        // 确保在提交时检查是否有变化
+                        if (priority !== item.priority) {
+                          const isTag = !!item.tag;
+                          const channelId = isTag ? item.tag : item.id;
+                          manageChannel(channelId, 'priority', priority, isTag)
+                            .then(({ success }) => {
+                              if (success) {
+                                item.priority = priority;
+                                showInfo(t('channel_row.priorityUpdateSuccess'));
+                              }
+                            })
+                            .catch((error) => {
+                              showError(t('channel_row.priorityUpdateError', { message: error.message }));
+                            });
+                        }
+                      }}
+                    >
+                      <Icon icon="mdi:check" />
+                    </IconButton>
+                  </InputAdornment>
+                )
+              }}
+            />
+          </Box>
         </TableCell>
 
+        {!item.tag && (
+          <TableCell>
+            <Box sx={{ display: 'flex' }}>
+              <TextField
+                id={`weight-${item.id}`}
+                type="number"
+                label={t('channel_index.weight')}
+                variant="outlined"
+                size="small"
+                value={weight}
+                onChange={(e) => setWeight(Number(e.target.value))}
+                inputProps={{
+                  min: '1'
+                }}
+                sx={{ width: '90px' }}
+                InputProps={{
+                  endAdornment: (
+                    <InputAdornment position="end">
+                      <IconButton
+                        size="small"
+                        color="primary"
+                        onClick={() => {
+                          // 确保在提交时检查是否有变化
+                          if (weight !== item.weight) {
+                            manageChannel(item.id, 'weight', weight)
+                              .then(({ success }) => {
+                                if (success) {
+                                  item.weight = weight;
+                                  showInfo(t('channel_row.weightUpdateSuccess'));
+                                }
+                              })
+                              .catch((error) => {
+                                showError(t('channel_row.weightUpdateError', { message: error.message }));
+                              });
+                          }
+                        }}
+                      >
+                        <Icon icon="mdi:check" />
+                      </IconButton>
+                    </InputAdornment>
+                  )
+                }}
+              />
+            </Box>
+          </TableCell>
+        )}
+
         <TableCell>
-          <Stack direction="row" justifyContent="center" alignItems="center" spacing={1}>
-            <Tooltip title={t('channel_row.onlyChat')} placement="top">
-              <Button
-                id="test-model-button"
+          <Stack direction="row" justifyContent="right" alignItems="right" spacing={1}>
+            {!item.tag && (
+              <IconButton
+                size="small"
+                onClick={handleTestModel}
                 aria-controls={openTest ? 'test-model-menu' : undefined}
                 aria-haspopup="true"
                 aria-expanded={openTest ? 'true' : undefined}
-                variant="outlined"
-                disableElevation
-                onClick={handleTestModel}
-                endIcon={<KeyboardArrowDownIcon />}
-                size="small"
+                sx={{ color: 'info.main' }}
               >
-                {t('channel_row.test')}
-              </Button>
+                <Icon icon="mdi:speedometer" />
+              </IconButton>
+            )}
+
+            <Tooltip title={t('common.edit')} placement="top" arrow>
+              <IconButton onClick={quickEdit.onTrue} size="small">
+                <Icon icon="solar:pen-bold" />
+              </IconButton>
             </Tooltip>
-            <IconButton onClick={handleOpenMenu} sx={{ color: 'rgb(99, 115, 129)' }}>
-              <Icon icon="solar:menu-dots-circle-bold-duotone" />
-            </IconButton>
+            {!item.tag && (
+              <IconButton onClick={popover.onOpen} size="small">
+                <Icon icon="eva:more-vertical-fill" />
+              </IconButton>
+            )}
+
+            {item.tag && (
+              <Tooltip title={t('channel_row.deleteTagAndChannels')} placement="top">
+                <IconButton sx={{ color: 'error.main' }} onClick={tagDeleteConfirm.onTrue} size="small">
+                  <Icon icon="solar:trash-bin-trash-bold" />
+                </IconButton>
+              </Tooltip>
+            )}
           </Stack>
         </TableCell>
       </TableRow>
 
       <Popover
-        open={!!open}
-        anchorEl={open}
-        onClose={handleCloseMenu}
+        open={popover.open}
+        anchorEl={popover.anchorEl}
+        onClose={() => {
+          popover.onClose();
+          // 如果在关闭后没有进一步操作，重置当前渠道
+          if (!check.value && !confirmDelete.value && !updateBalanceOption.value) {
+            setCurrentTestingChannel(null);
+          }
+        }}
         anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
         transformOrigin={{ vertical: 'top', horizontal: 'right' }}
         PaperProps={{
           sx: { minWidth: 140 }
         }}
       >
-        {!hideEdit && (
-          <MenuItem
-            onClick={() => {
-              handleCloseMenu();
-              handleOpenModal();
-              setModalChannelId(item.id);
-            }}
-          >
-            <Icon icon="solar:pen-bold-duotone" style={{ marginRight: '16px' }} />
-            {t('common.edit')}
-          </MenuItem>
-        )}
-
         <MenuItem
           onClick={() => {
-            handleCloseMenu();
-            manageChannel(item.id, 'copy');
+            popover.onClose();
+            manageChannel(currentTestingChannel ? currentTestingChannel.id : item.id, 'copy');
           }}
         >
           <Icon icon="solar:copy-bold-duotone" style={{ marginRight: '16px' }} />
           {t('token_index.copy')}
         </MenuItem>
-        <MenuItem onClick={() => setOpenCheck(true)}>
+        <MenuItem
+          onClick={() => {
+            setOpenCheck(true);
+            popover.onClose();
+          }}
+        >
           <Icon icon="solar:checklist-minimalistic-bold" style={{ marginRight: '16px' }} />
           {t('channel_row.check')}
         </MenuItem>
-        {CHANNEL_OPTIONS[item.type]?.url && (
+
+        {CHANNEL_OPTIONS[currentTestingChannel ? currentTestingChannel?.type : item.type]?.url && (
           <MenuItem
             onClick={() => {
-              handleCloseMenu();
-              window.open(CHANNEL_OPTIONS[item.type].url);
+              popover.onClose();
+              window.open(CHANNEL_OPTIONS[currentTestingChannel ? currentTestingChannel?.type : item.type].url);
             }}
           >
             <Icon icon="solar:global-line-duotone" style={{ marginRight: '16px' }} />
@@ -364,13 +630,25 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
           </MenuItem>
         )}
 
-        {item.tag && (
-          <MenuItem onClick={handleDeleteTag} sx={{ color: 'error.main' }}>
+        {currentTestingChannel && (
+          <MenuItem
+            onClick={() => {
+              popover.onClose();
+              manageChannel(currentTestingChannel.id, 'delete_tag', '');
+            }}
+            sx={{ color: 'error.main' }}
+          >
             <Icon icon="solar:trash-bin-trash-bold-duotone" style={{ marginRight: '16px' }} />
             {t('channel_row.delTag')}
           </MenuItem>
         )}
-        <MenuItem onClick={handleDeleteOpen} sx={{ color: 'error.main' }}>
+        <MenuItem
+          onClick={() => {
+            popover.onClose();
+            confirmDelete.onTrue();
+          }}
+          sx={{ color: 'error.main' }}
+        >
           <Icon icon="solar:trash-bin-trash-bold-duotone" style={{ marginRight: '16px' }} />
           {t('common.delete')}
         </MenuItem>
@@ -398,20 +676,57 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
           </MenuItem>
         ))}
       </StyledMenu>
-      <TableRow>
+      <TableRow
+        sx={{
+          '&:hover': {
+            backgroundColor: 'transparent !important'
+          },
+          '&.MuiTableRow-hover:hover': {
+            backgroundColor: 'transparent !important'
+          }
+        }}
+      >
         <TableCell style={{ paddingBottom: 0, paddingTop: 0, textAlign: 'left' }} colSpan={20}>
           <Collapse in={openRow} timeout="auto" unmountOnExit>
-            <Grid container spacing={1}>
+            <Grid container spacing={1} sx={{ py: 2 }}>
               <Grid item xs={12}>
-                <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '10px', margin: 1 }}>
-                  <Typography variant="h6" gutterBottom component="div">
-                    {t('channel_row.canModels')}
+                <Box
+                  sx={{
+                    display: 'flex',
+                    flexWrap: 'wrap',
+                    gap: '10px',
+                    m: 1,
+                    p: 0,
+                    bgcolor: 'background.neutral',
+                    borderRadius: 1,
+                    alignItems: 'center'
+                  }}
+                >
+                  <Typography
+                    variant="body1"
+                    component="div"
+                    sx={{
+                      fontWeight: 600,
+                      color: 'text.secondary',
+                      // mr: 1,
+                      display: 'flex',
+                      alignItems: 'center'
+                    }}
+                  >
+                    <Icon icon="mdi:cube-outline" sx={{ mr: 0.5 }} /> {t('channel_row.canModels')}
                   </Typography>
                   {modelMap.map((model) => (
                     <Label
-                      variant="outlined"
+                      variant="soft"
                       color="primary"
                       key={model}
+                      sx={{
+                        // py: 0.75,
+                        // px: 1.5,
+                        fontSize: '0.75rem',
+                        cursor: 'pointer',
+                        // '&:hover': { opacity: 0.8 }
+                      }}
                       onClick={() => {
                         copy(model, t('channel_index.modelName'));
                       }}
@@ -421,16 +736,39 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
                   ))}
                 </Box>
               </Grid>
+
               {item.test_model && (
-                <Grid item xs={12}>
-                  <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '10px', margin: 1 }}>
-                    <Typography variant="h6" gutterBottom component="div">
-                      {t('channel_row.testModels') + ':'}
+                <Grid xs={12}>
+                  <Box
+                    sx={{
+                      display: 'flex',
+                      flexWrap: 'wrap',
+                      gap: '10px',
+                      m: 1,
+                      px: 1,
+                      py: 0.5,
+                      bgcolor: 'background.neutral',
+                      borderRadius: 1,
+                      alignItems: 'center'
+                    }}
+                  >
+                    <Typography
+                      variant="body1"
+                      component="div"
+                      sx={{
+                        fontWeight: 600,
+                        color: 'text.secondary',
+                        display: 'flex',
+                        alignItems: 'center'
+                      }}
+                    >
+                      <Icon icon="mdi:speedometer" sx={{ mr: 0.5 }} /> {t('channel_row.testModels') + ':'}
                     </Typography>
                     <Label
-                      variant="outlined"
-                      color="default"
+                      variant="soft"
+                      color="info"
                       key={item.test_model}
+                      sx={{ fontSize: '0.75rem', cursor: 'pointer' }}
                       onClick={() => {
                         copy(item.test_model, t('channel_row.testModels'));
                       }}
@@ -440,26 +778,71 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
                   </Box>
                 </Grid>
               )}
+
               {item.proxy && (
-                <Grid item xs={12}>
-                  <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '10px', margin: 1 }}>
-                    <Typography variant="h6" gutterBottom component="div">
-                      {t('channel_row.proxy')}
+                <Grid xs={12}>
+                  <Box
+                    sx={{
+                      display: 'flex',
+                      flexWrap: 'wrap',
+                      gap: '10px',
+                      m: 1,
+                      px: 1,
+                      py: 0.5,
+                      bgcolor: 'background.neutral',
+                      borderRadius: 1,
+                      alignItems: 'center'
+                    }}
+                  >
+                    <Typography
+                      variant="body1"
+                      component="div"
+                      sx={{
+                        fontWeight: 600,
+                        color: 'text.secondary',
+                        display: 'flex',
+                        alignItems: 'center'
+                      }}
+                    >
+                      <Icon icon="mdi:web" sx={{ mr: 0.5 }} /> {t('channel_row.proxy')}
                     </Typography>
-                    {item.proxy}
+                    <Typography variant="body2">{item.proxy}</Typography>
                   </Box>
                 </Grid>
               )}
+
               {item.other && (
-                <Grid item xs={12}>
-                  <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '10px', margin: 1 }}>
-                    <Typography variant="h6" gutterBottom component="div">
-                      {t('channel_row.otherArg') + ':'}
+                <Grid xs={12}>
+                  <Box
+                    sx={{
+                      display: 'flex',
+                      flexWrap: 'wrap',
+                      gap: '10px',
+                      m: 1,
+                      px: 1,
+                      py: 0.5,
+                      bgcolor: 'background.neutral',
+                      borderRadius: 1,
+                      alignItems: 'center'
+                    }}
+                  >
+                    <Typography
+                      variant="body1"
+                      component="div"
+                      sx={{
+                        fontWeight: 600,
+                        color: 'text.secondary',
+                        display: 'flex',
+                        alignItems: 'center'
+                      }}
+                    >
+                      <Icon icon="mdi:cog-outline" sx={{ mr: 0.5 }} /> {t('channel_row.otherArg')}
                     </Typography>
                     <Label
-                      variant="outlined"
+                      variant="soft"
                       color="default"
                       key={item.other}
+                      sx={{ fontSize: '0.75rem', cursor: 'pointer' }}
                       onClick={() => {
                         copy(item.other, t('channel_row.otherArg'));
                       }}
@@ -469,25 +852,487 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
                   </Box>
                 </Grid>
               )}
+              {item.tag && (
+                <Grid xs={12}>
+                  <Box sx={{ m: 1, mt: 2 }}>
+                    <Stack direction="row" justifyContent="space-between" alignItems="center" mb={2}>
+                      <Stack direction="row" alignItems="center" spacing={1}>
+                        <Typography
+                          variant="subtitle1"
+                          fontWeight="bold"
+                          sx={{
+                            borderLeft: '3px solid',
+                            borderColor: 'primary.main',
+                            pl: 1.5,
+                            py: 0.5
+                          }}
+                        >
+                          {t('channel_row.tagChannelList')} ({totalTagChannels})
+                        </Typography>
+                        <Tooltip title={t('channel_row.refreshList')} placement="top">
+                          <IconButton
+                            size="small"
+                            color="primary"
+                            onClick={() => {
+                              setIsTagChannelsLoading(true);
+                              fetchTagChannels().finally(() => {
+                                setIsTagChannelsLoading(false);
+                              });
+                            }}
+                          >
+                            <Icon icon="mdi:refresh" width={18} height={18} />
+                          </IconButton>
+                        </Tooltip>
+                      </Stack>
+
+                      {selectedChannels.length > 0 && (
+                        <Button
+                          variant="contained"
+                          color="error"
+                          startIcon={<Icon icon="solar:trash-bin-trash-bold" />}
+                          onClick={handleBatchDelete}
+                          size="small"
+                        >
+                          {t('channel_row.batchDelete')} ({selectedChannels.length})
+                        </Button>
+                      )}
+                    </Stack>
+
+                    {isTagChannelsLoading ? (
+                      <Box sx={{ display: 'flex', justifyContent: 'center', py: 3 }}>
+                        <CircularProgress size={24} />
+                      </Box>
+                    ) : tagChannels.length === 0 ? (
+                      <Typography variant="body2" sx={{ py: 2, textAlign: 'center', color: 'text.secondary' }}>
+                        {t('channel_row.noTagChannels')}
+                      </Typography>
+                    ) : (
+                      <Box>
+                        <Box
+                          sx={{
+                            border: '1px solid',
+                            borderColor: 'divider',
+                            borderRadius: 1,
+                            overflow: 'hidden',
+                            boxShadow: '0 0 8px rgba(0,0,0,0.05)'
+                          }}
+                        >
+                          <PerfectScrollbar sx={{ maxHeight: 400 }}>
+                            <Table size="small" sx={{ '& .MuiTableCell-root': { py: 1, px: 1.5 } }}>
+                              <TableHead sx={{ bgcolor: 'background.neutral' }}>
+                                <TableRow>
+                                  <TableCell padding="checkbox" sx={{ pl: 1, width: '40px', textAlign: 'center' }}>
+                                    <Checkbox
+                                      indeterminate={selectedChannels.length > 0 && selectedChannels.length < tagChannels.length}
+                                      checked={tagChannels.length > 0 && selectedChannels.length === tagChannels.length}
+                                      onChange={handleToggleAll}
+                                      size="small"
+                                    />
+                                  </TableCell>
+                                  <TableCell sx={{ width: '20%', textAlign: 'center', fontWeight: 600 }}>
+                                    {t('channel_index.name')}
+                                  </TableCell>
+                                  <TableCell sx={{ width: '15%', textAlign: 'center', fontWeight: 600 }}>
+                                    {t('channel_index.status')}
+                                  </TableCell>
+                                  <TableCell sx={{ width: '15%', textAlign: 'center', fontWeight: 600 }}>
+                                    {t('channel_index.usedBalance')}
+                                  </TableCell>
+                                  <TableCell sx={{ width: '15%', textAlign: 'center', fontWeight: 600 }}>
+                                    {t('channel_index.responseTime')}
+                                  </TableCell>
+                                  <TableCell sx={{ width: '25%', textAlign: 'center', fontWeight: 600 }}>
+                                    {t('channel_index.priority')}
+                                  </TableCell>
+                                  <TableCell sx={{ width: '10%', textAlign: 'center', fontWeight: 600 }}>
+                                    {t('channel_index.actions')}
+                                  </TableCell>
+                                </TableRow>
+                              </TableHead>
+                              <TableBody>
+                                {tagChannels.slice(tagPage * tagRowsPerPage, tagPage * tagRowsPerPage + tagRowsPerPage).map((channel) => (
+                                  <TableRow key={channel.id} hover>
+                                    <TableCell padding="checkbox" sx={{ pl: 1, textAlign: 'center' }}>
+                                      <Checkbox
+                                        checked={selectedChannels.includes(channel.id)}
+                                        onChange={() => handleToggleChannel(channel.id)}
+                                        size="small"
+                                      />
+                                    </TableCell>
+                                    <TableCell sx={{ textAlign: 'center' }}>
+                                      <Typography variant="body2" noWrap title={channel.name} sx={{ fontWeight: 500 }}>
+                                        {channel.id} - {channel.name}
+                                      </Typography>
+                                    </TableCell>
+                                    <TableCell sx={{ textAlign: 'center' }}>
+                                      <Stack direction="row" alignItems="center" spacing={0.5} justifyContent="center">
+                                        <Switch
+                                          checked={channel.status === 1}
+                                          onChange={() => handleTagChannelStatus(channel.id, channel.status)}
+                                          size="small"
+                                        />
+                                        <Typography
+                                          variant="caption"
+                                          sx={{
+                                            fontWeight: channel.status === 1 ? 600 : 400,
+                                            color: channel.status === 1 ? 'success.main' : 'text.secondary'
+                                          }}
+                                        >
+                                          {statusInfo(t, channel.status)}
+                                          {/* {CHANNEL_STATUS_MAP[channel.status]?.label || '未知'} */}
+                                        </Typography>
+                                      </Stack>
+                                    </TableCell>
+                                    <TableCell sx={{ textAlign: 'center' }}>
+                                      <Tooltip title={t('channel_row.clickUpdateQuota')} placement="top">
+                                        <Box sx={{ cursor: 'pointer' }} onClick={() => manageChannel(channel.id, 'update_balance')}>
+                                          <Stack direction="column" spacing={0.5} alignItems="center" justifyContent="center">
+                                            <Typography
+                                              variant="body2"
+                                              sx={{
+                                                fontSize: '0.8rem',
+                                                fontWeight: 500,
+                                                '&:hover': { textDecoration: 'underline' }
+                                              }}
+                                            >
+                                              {renderQuota(channel.used_quota)}
+                                            </Typography>
+                                            <Typography
+                                              variant="caption"
+                                              sx={{
+                                                color: 'success.main',
+                                                fontWeight: 600
+                                              }}
+                                            >
+                                              ${channel.balance}
+                                            </Typography>
+                                          </Stack>
+                                        </Box>
+                                      </Tooltip>
+                                    </TableCell>
+                                    <TableCell sx={{ textAlign: 'center' }}>
+                                      <ResponseTimeLabel test_time={channel.test_time} response_time={channel.response_time} />
+                                    </TableCell>
+
+                                    <TableCell sx={{ textAlign: 'center' }}>
+                                      <Box sx={{ display: 'flex', justifyContent: 'center' }}>
+                                        <TextField
+                                          id={`priority-${channel.id}`}
+                                          type="number"
+                                          label="优先级"
+                                          variant="standard"
+                                          value={channel.priority}
+                                          onChange={(e) => handleTagChannelPriorityChange(channel.id, Number(e.target.value))}
+                                          inputProps={{
+                                            min: '0',
+                                            style: { padding: '4px', fontSize: '0.85rem' }
+                                          }}
+                                          sx={{
+                                            width: '90px',
+                                            '.MuiInputBase-root': {
+                                              minHeight: '30px'
+                                            },
+                                            '.MuiInputLabel-root': {
+                                              fontSize: '0.75rem',
+                                              fontWeight: 500
+                                            }
+                                          }}
+                                          InputProps={{
+                                            endAdornment: (
+                                              <InputAdornment position="end">
+                                                <IconButton
+                                                  size="small"
+                                                  sx={{ p: 0.5 }}
+                                                  color="primary"
+                                                  edge="end"
+                                                  onClick={() => {
+                                                    try {
+                                                      // 直接使用当前渠道对象和UI状态中的优先级值
+                                                      const channelId = channel.id;
+                                                      const newPriority = channel.priority;
+
+                                                      // 直接发送请求更新优先级，不再进行二次检查
+                                                      manageChannel(channelId, 'priority', newPriority)
+                                                        .then(({ success }) => {
+                                                          if (success) {
+                                                            // 成功后更新本地状态（虽然没必要，因为UI状态已经是新值了）
+                                                            showSuccess(t('channel_row.priorityUpdateSuccess'));
+                                                          }
+                                                        })
+                                                        .catch((error) => {
+                                                          showError(t('channel_row.priorityUpdateError', { message: error.message }));
+                                                        });
+                                                    } catch (error) {
+                                                      showError(t('channel_row.priorityUpdateError', { message: error.message }));
+                                                    }
+                                                  }}
+                                                >
+                                                  <Icon icon="mdi:check" width={16} height={16} />
+                                                </IconButton>
+                                              </InputAdornment>
+                                            )
+                                          }}
+                                        />
+                                      </Box>
+                                    </TableCell>
+                                    <TableCell align="center">
+                                      <Stack direction="row" spacing={1} justifyContent="center">
+                                        <Tooltip title={t('channel_row.testModels')} placement="top">
+                                          <IconButton
+                                            size="small"
+                                            sx={{ p: 0.5, color: 'info.main' }}
+                                            onClick={(event) => {
+                                              handleTagChannelTest(channel);
+                                              // 记录点击位置用于弹出模型列表
+                                              if (channel.models.split(',').length > 1) {
+                                                tagModelPopover.onOpen(event);
+                                              }
+                                            }}
+                                          >
+                                            <Icon icon="mdi:speedometer" width={18} height={18} />
+                                          </IconButton>
+                                        </Tooltip>
+
+                                        <Tooltip title={t('channel_index.actions')} placement="top">
+                                          <IconButton
+                                            size="small"
+                                            sx={{ p: 0.5 }}
+                                            onClick={(event) => {
+                                              // 设置当前操作的渠道
+                                              setCurrentTestingChannel(channel);
+                                              // 打开更多操作菜单
+                                              popover.onOpen(event);
+                                            }}
+                                          >
+                                            <Icon icon="eva:more-vertical-fill" width={18} height={18} />
+                                          </IconButton>
+                                        </Tooltip>
+                                      </Stack>
+                                    </TableCell>
+                                  </TableRow>
+                                ))}
+                              </TableBody>
+                            </Table>
+                          </PerfectScrollbar>
+                        </Box>
+                        <Box sx={{ display: 'flex', justifyContent: 'flex-end', pt: 1 }}>
+                          <TablePagination
+                            component="div"
+                            count={tagChannels.length}
+                            page={tagPage}
+                            onPageChange={handleChangeTagPage}
+                            rowsPerPage={tagRowsPerPage}
+                            onRowsPerPageChange={handleChangeTagRowsPerPage}
+                            rowsPerPageOptions={[10, 25, 50, 100]}
+                            labelRowsPerPage="每页行数:"
+                            labelDisplayedRows={({ from, to, count }) => `${from}-${to} 共 ${count}`}
+                            sx={{
+                              '.MuiTablePagination-toolbar': {
+                                minHeight: '40px',
+                                pl: 1
+                              },
+                              '.MuiTablePagination-selectLabel, .MuiTablePagination-displayedRows': {
+                                fontSize: '0.75rem'
+                              },
+                              '.MuiTablePagination-select': {
+                                padding: '0 8px'
+                              }
+                            }}
+                          />
+                        </Box>
+                      </Box>
+                    )}
+                  </Box>
+                </Grid>
+              )}
             </Grid>
           </Collapse>
         </TableCell>
       </TableRow>
-      <Dialog open={openDelete} onClose={handleDeleteClose}>
+      <Dialog open={confirmDelete.value} onClose={confirmDelete.onFalse}>
         <DialogTitle>{t('channel_row.delChannel')}</DialogTitle>
         <DialogContent>
           <DialogContentText>
-            {t('channel_row.delChannelInfo')} {item.name}？
+            {t('common.deleteConfirm', { title: currentTestingChannel ? currentTestingChannel.name : item.name })}
           </DialogContentText>
         </DialogContent>
         <DialogActions>
-          <Button onClick={handleDeleteClose}>{t('token_index.close')}</Button>
-          <Button onClick={handleDelete} sx={{ color: 'error.main' }} autoFocus>
+          <Button onClick={confirmDelete.onFalse}>{t('token_index.close')}</Button>
+          <Button
+            onClick={() => {
+              if (currentTestingChannel) {
+                // 处理子渠道删除
+                manageChannel(currentTestingChannel.id, 'delete', '')
+                  .then(({ success }) => {
+                    if (success) {
+                      // 从本地列表中移除
+                      setTagChannels((prev) => prev.filter((c) => c.id !== currentTestingChannel.id));
+                      // 减少总数
+                      setTotalTagChannels((prev) => prev - 1);
+                      // 重置当前选中的渠道
+                      setCurrentTestingChannel(null);
+                      showSuccess(t('common.deleteSuccess'));
+                    }
+                  })
+                  .catch((error) => {
+                    showError(t('common.deleteError', { message: error.message }));
+                  });
+              } else {
+                // 处理主渠道删除
+                handleDeleteRow();
+              }
+              confirmDelete.onFalse();
+            }}
+            sx={{ color: 'error.main' }}
+            autoFocus
+          >
             {t('common.delete')}
           </Button>
         </DialogActions>
       </Dialog>
-      <ChannelCheck item={item} open={openCheck} onClose={() => setOpenCheck(false)} />
+      <ChannelCheck item={currentTestingChannel || item} open={openCheck} onClose={() => setOpenCheck(false)} />
+
+      <ConfirmDialog
+        open={tagDeleteConfirm.value}
+        onClose={tagDeleteConfirm.onFalse}
+        title={t('channel_row.deleteTag')}
+        content={t('channel_row.deleteTagConfirm', { tag: item.tag })}
+        action={
+          <Button
+            variant="contained"
+            color="error"
+            onClick={() => {
+              manageChannel(item.tag, 'delete', '', true)
+                .then(({ success }) => {
+                  if (success) {
+                    showInfo(t('channel_row.deleteTagSuccess', { tag: item.tag }));
+                    onRefresh(false); // 刷新父组件数据
+                  }
+                })
+                .catch((error) => {
+                  showError(t('channel_row.deleteTagError', { message: error.message }));
+                });
+              tagDeleteConfirm.onFalse();
+            }}
+          >
+            {t('common.delete')}
+          </Button>
+        }
+      />
+
+      <ConfirmDialog
+        open={tagStatusConfirm.value}
+        onClose={tagStatusConfirm.onFalse}
+        title={statusChangeAction === 'enable' ? t('channel_row.enableTagChannels') : t('channel_row.disableTagChannels')}
+        content={t('channel_row.tagChannelsConfirm', {
+          action: statusChangeAction === 'enable' ? t('channel_row.enable') : t('channel_row.disable'),
+          tag: item.tag
+        })}
+        action={
+          <Button
+            variant="contained"
+            color={statusChangeAction === 'enable' ? 'success' : 'error'}
+            onClick={() => {
+              manageChannel(item.tag, 'tag_change_status', statusChangeAction, false)
+                .then(({ success }) => {
+                  if (success) {
+                    showInfo(
+                      t('channel_row.tagChannelsSuccess', {
+                        action: statusChangeAction === 'enable' ? t('channel_row.enable') : t('channel_row.disable')
+                      })
+                    );
+                    onRefresh(false); // 刷新父组件数据
+                  }
+                })
+                .catch((error) => {
+                  showError(
+                    t('channel_row.tagChannelsError', {
+                      action: statusChangeAction === 'enable' ? t('channel_row.enable') : t('channel_row.disable'),
+                      message: error.message
+                    })
+                  );
+                });
+              tagStatusConfirm.onFalse();
+            }}
+          >
+            {t('common.submit')}
+          </Button>
+        }
+      />
+
+      <EditeModal
+        open={quickEdit.value}
+        onCancel={quickEdit.onFalse}
+        onOk={() => {
+          onRefresh(false);
+          quickEdit.onFalse();
+        }}
+        channelId={item.tag ? item.tag : item.id}
+        groupOptions={groupOptions}
+        isTag={item.tag}
+        modelOptions={modelOptions}
+      />
+
+      <Popover
+        open={tagModelPopover.open}
+        anchorEl={tagModelPopover.anchorEl}
+        onClose={tagModelPopover.onClose}
+        anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
+        transformOrigin={{ vertical: 'top', horizontal: 'right' }}
+        PaperProps={{
+          sx: { minWidth: 140 }
+        }}
+      >
+        <MenuList>
+          {currentTestingChannel &&
+            currentTestingChannel.models.split(',').map((model) => (
+              <MenuItem
+                key={`tag-test-model-${model}`}
+                onClick={() => {
+                  manageChannel(currentTestingChannel.id, 'test', model).then(({ success, time }) => {
+                    if (success) {
+                      showSuccess(t('channel_row.modelTestSuccess', { channel: currentTestingChannel.name, model, time: time.toFixed(2) }));
+                      // 更新本地状态
+                      setTagChannels((prev) =>
+                        prev.map((c) =>
+                          c.id === currentTestingChannel.id
+                            ? {
+                                ...c,
+                                test_time: Date.now() / 1000,
+                                response_time: time * 1000
+                              }
+                            : c
+                        )
+                      );
+                    }
+                  });
+                  tagModelPopover.onClose();
+                }}
+              >
+                {model}
+              </MenuItem>
+            ))}
+        </MenuList>
+      </Popover>
+
+      <ConfirmDialog
+        open={batchConfirm.value}
+        onClose={batchConfirm.onFalse}
+        title={t('channel_row.batchDelete')}
+        content={t('channel_row.batchDeleteConfirm', { count: selectedChannels.length })}
+        action={
+          <Button
+            variant="contained"
+            color="error"
+            onClick={() => {
+              executeBatchDelete();
+              batchConfirm.onFalse();
+            }}
+          >
+            {t('common.delete')}
+          </Button>
+        }
+      />
     </>
   );
 }
@@ -495,30 +1340,18 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
 ChannelTableRow.propTypes = {
   item: PropTypes.object,
   manageChannel: PropTypes.func,
-  handleOpenModal: PropTypes.func,
-  setModalChannelId: PropTypes.func,
-  hideEdit: PropTypes.bool
+  onRefresh: PropTypes.func,
+  groupOptions: PropTypes.array,
+  modelOptions: PropTypes.array
 };
 
 function renderBalance(type, balance) {
   switch (type) {
     case 28: // Deepseek
-      return (
-        <span>
-          <br />¥{balance}
-        </span>
-      );
+      return <>¥{balance}</>;
     case 45: // Deepseek
-      return (
-        <span>
-          <br />¥{balance}
-        </span>
-      );
+      return <>¥{balance}</>;
     default:
-      return (
-        <span>
-          <br />${balance.toFixed(2)}
-        </span>
-      );
+      return <>${balance.toFixed(2)}</>;
   }
 }
diff --git a/web/src/views/Channel/component/TableToolBar.jsx b/web/src/views/Channel/component/TableToolBar.jsx
index 9b5ab348..7fa1f3d7 100644
--- a/web/src/views/Channel/component/TableToolBar.jsx
+++ b/web/src/views/Channel/component/TableToolBar.jsx
@@ -225,12 +225,15 @@ export default function TableToolBar({ filterName, handleFilterName, groupOption
               }
             }}
           >
-            <MenuItem key={1} value={false}>
-              {t('channel_index.showAll')}
+            <MenuItem key={0} value={0}>
+              {t('channel_index.all')}
             </MenuItem>
-            <MenuItem key={2} value={true}>
+            <MenuItem key={1} value={1}>
               {t('channel_index.filterTags')}
             </MenuItem>
+            <MenuItem key={2} value={2}>
+              {t('channel_index.onlyTags')}
+            </MenuItem>
           </Select>
         </FormControl>
         <FormControl sx={{ minWidth: '17.5%' }}>
diff --git a/web/src/views/Channel/index.jsx b/web/src/views/Channel/index.jsx
index 58488781..29a64c2e 100644
--- a/web/src/views/Channel/index.jsx
+++ b/web/src/views/Channel/index.jsx
@@ -1,99 +1,559 @@
-import { useState, useEffect, useMemo } from 'react';
-import PropTypes from 'prop-types';
-import { Tabs, Tab, Box, Card } from '@mui/material';
-import { Icon } from '@iconify/react';
-import ChannelList from './ChannelList';
-import ChannelTag from './ChannelTag';
+import { useState, useEffect, useCallback } from 'react';
+import { showError, showSuccess, showInfo, trims } from 'utils/common';
 import AdminContainer from 'ui-component/AdminContainer';
-import { useLocation, useNavigate } from 'react-router-dom';
-import { useTranslation } from 'react-i18next';
 
-function CustomTabPanel(props) {
-  const { children, value, index, ...other } = props;
+import { useTheme } from '@mui/material/styles';
+import Table from '@mui/material/Table';
+import TableBody from '@mui/material/TableBody';
+import TableContainer from '@mui/material/TableContainer';
+import TablePagination from '@mui/material/TablePagination';
+import LinearProgress from '@mui/material/LinearProgress';
+import ButtonGroup from '@mui/material/ButtonGroup';
+import Toolbar from '@mui/material/Toolbar';
+import useMediaQuery from '@mui/material/useMediaQuery';
+import Alert from '@mui/material/Alert';
 
-  return (
-    <div role="tabpanel" hidden={value !== index} id={`channel-tabpanel-${index}`} aria-labelledby={`channel-tab-${index}`} {...other}>
-      {value === index && <Box sx={{ p: 3 }}>{children}</Box>}
-    </div>
-  );
-}
+import { Button, IconButton, Card, Box, Stack, Container, Typography, Divider } from '@mui/material';
+import ChannelTableRow from './component/TableRow';
+import KeywordTableHead from 'ui-component/TableHead';
+import { API } from 'utils/api';
+import EditeModal from './component/EditModal';
+import { ITEMS_PER_PAGE, PAGE_SIZE_OPTIONS } from 'constants';
+import TableToolBar from './component/TableToolBar';
+import BatchModal from './component/BatchModal';
+import { useTranslation } from 'react-i18next';
+
+import { useBoolean } from 'hooks/use-boolean';
+import ConfirmDialog from 'ui-component/confirm-dialog';
+import { Icon } from '@iconify/react';
 
-CustomTabPanel.propTypes = {
-  children: PropTypes.node,
-  index: PropTypes.number.isRequired,
-  value: PropTypes.number.isRequired
+const originalKeyword = {
+  type: 0,
+  status: 0,
+  name: '',
+  group: '',
+  models: '',
+  key: '',
+  test_model: '',
+  other: '',
+  filter_tag: 0,
+  tag: ''
 };
 
-function a11yProps(index) {
-  return {
-    id: `channel-tab-${index}`,
-    'aria-controls': `channel-tabpanel-${index}`
-  };
+export async function fetchChannelData(page, rowsPerPage, keyword, order, orderBy) {
+  try {
+    if (orderBy) {
+      orderBy = order === 'desc' ? '-' + orderBy : orderBy;
+    }
+    const res = await API.get(`/api/channel/`, {
+      params: {
+        page: page + 1,
+        size: rowsPerPage,
+        order: orderBy,
+        ...keyword
+      }
+    });
+    const { success, message, data } = res.data;
+    if (success) {
+      return data;
+    } else {
+      showError(message);
+    }
+  } catch (error) {
+    console.error(error);
+  }
+
+  return false;
 }
 
-const ChannelTab = () => {
+// ----------------------------------------------------------------------
+// CHANNEL_OPTIONS,
+export default function ChannelList() {
   const { t } = useTranslation();
-  const location = useLocation();
-  const navigate = useNavigate();
-  const hash = location.hash.replace('#', '');
-  const tabMap = useMemo(
-    () => ({
-      channel: 0,
-      channel_tag: 1
-    }),
-    []
+  const [page, setPage] = useState(0);
+  const [order, setOrder] = useState('desc');
+  const [orderBy, setOrderBy] = useState('id');
+  const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE);
+  const [listCount, setListCount] = useState(0);
+  const [searching, setSearching] = useState(false);
+  const [channels, setChannels] = useState([]);
+  const [refreshFlag, setRefreshFlag] = useState(false);
+  const [tags, setTags] = useState([]);
+  const [modelOptions, setModelOptions] = useState([]);
+
+  const confirm = useBoolean();
+  const [confirmTitle, setConfirmTitle] = useState('');
+  const [confirmConfirm, setConfirmConfirm] = useState(() => {});
+
+  const [groupOptions, setGroupOptions] = useState([]);
+  const [toolBarValue, setToolBarValue] = useState(originalKeyword);
+  const [searchKeyword, setSearchKeyword] = useState(originalKeyword);
+
+  const theme = useTheme();
+  const matchUpMd = useMediaQuery(theme.breakpoints.up('sm'));
+  const [openModal, setOpenModal] = useState(false);
+  const [editChannelId, setEditChannelId] = useState(0);
+  const [openBatchModal, setOpenBatchModal] = useState(false);
+
+  const handleSort = (event, id) => {
+    const isAsc = orderBy === id && order === 'asc';
+    if (id !== '') {
+      setOrder(isAsc ? 'desc' : 'asc');
+      setOrderBy(id);
+    }
+  };
+
+  const handleChangePage = (event, newPage) => {
+    setPage(newPage);
+  };
+
+  const handleChangeRowsPerPage = (event) => {
+    setPage(0);
+    setRowsPerPage(parseInt(event.target.value, 10));
+  };
+
+  const searchChannels = async () => {
+    // event.preventDefault();
+    // const formData = new FormData(event.target);
+    setPage(0);
+    setSearchKeyword(toolBarValue);
+  };
+
+  const handleToolBarValue = (event) => {
+    setToolBarValue({ ...toolBarValue, [event.target.name]: event.target.value });
+  };
+
+  const manageChannel = async (id, action, value, tag = false) => {
+    let url = '/api/channel/';
+    if (tag) {
+      url = '/api/channel_tag/';
+    }
+
+    let data = { id };
+    let res;
+
+    try {
+      switch (action) {
+        case 'copy': {
+          let oldRes = await API.get(`/api/channel/${id}`);
+          const { success, message, data } = oldRes.data;
+          if (!success) {
+            showError(message);
+            return { success: false, message };
+          }
+          // 删除 data.id
+          delete data.id;
+          delete data.test_time;
+          delete data.balance_updated_time;
+          delete data.used_quota;
+          delete data.response_time;
+          data.name = data.name + '_copy';
+          res = await API.post(`/api/channel/`, { ...data });
+          break;
+        }
+        case 'delete':
+          if (tag) {
+            res = await API.delete(url + encodeURIComponent(id));
+          } else {
+            res = await API.delete(`${url}${id}`);
+          }
+          break;
+        case 'delete_tag':
+          res = await API.delete(url + id + '/tag');
+          break;
+        case 'status':
+          res = await API.put(url, {
+            ...data,
+            status: value
+          });
+          break;
+        case 'priority':
+        case 'weight':
+          if (value === '') {
+            return { success: false, message: '值不能为空' };
+          }
+
+          if (!tag) {
+            res = await API.put(url, {
+              ...data,
+              [action]: Number(value)
+            });
+          } else {
+            res = await API.put(`${url + encodeURIComponent(id)}/priority`, {
+              type: 'priority',
+              value
+            });
+          }
+          break;
+        case 'test':
+          res = await API.get(url + `test/${id}`, {
+            params: { model: value }
+          });
+          break;
+        case 'batch_delete':
+          res = await API.delete('/api/channel/batch', {
+            data: {
+              value: 'batch_delete',
+              ids: value
+            }
+          });
+          break;
+        case 'tag_change_status':
+          res = await API.put(`/api/channel_tag/${id}/status/${value}`);
+          break;
+        default:
+          showError('无效操作');
+          return { success: false, message: '无效操作' };
+      }
+      const { success, message } = res.data;
+      if (success) {
+        showSuccess(t('userPage.operationSuccess'));
+        if (action === 'delete' || action === 'copy' || action == 'delete_tag') {
+          await handleRefresh(false);
+        }
+      } else {
+        showError(message);
+      }
+
+      return res.data;
+    } catch (error) {
+      return { success: false, message: error.message };
+    }
+  };
+
+  // 处理刷新
+  const handleRefresh = async (reset) => {
+    if (reset) {
+      setOrderBy('id');
+      setOrder('desc');
+      setToolBarValue(originalKeyword);
+      setSearchKeyword(originalKeyword);
+    }
+    setRefreshFlag(!refreshFlag);
+  };
+
+  const handlePopoverOpen = useCallback(
+    (title, onConfirm) => {
+      setConfirmTitle(title);
+      setConfirmConfirm(() => onConfirm);
+      confirm.onTrue();
+    },
+    [confirm]
   );
-  const [value, setValue] = useState(tabMap[hash] || 0);
 
-  const handleChange = (event, newValue) => {
-    setValue(newValue);
-    const hashArray = Object.keys(tabMap);
-    navigate(`#${hashArray[newValue]}`);
+  // 处理测试所有启用渠道
+  const testAllChannels = async () => {
+    try {
+      const res = await API.get(`/api/channel/test`);
+      const { success, message } = res.data;
+      if (success) {
+        showInfo(t('channel_row.testAllChannel'));
+      } else {
+        showError(message);
+      }
+    } catch (error) {
+      return;
+    }
+  };
+
+  // 处理删除所有禁用渠道
+  const deleteAllDisabledChannels = async () => {
+    try {
+      const res = await API.delete(`/api/channel/disabled`);
+      const { success, message, data } = res.data;
+      if (success) {
+        showSuccess(t('channel_row.delChannelCount', { count: data }));
+        await handleRefresh();
+      } else {
+        showError(message);
+      }
+    } catch (error) {
+      return;
+    }
+  };
+
+  // 处理更新所有启用渠道余额
+  const updateAllChannelsBalance = async () => {
+    setSearching(true);
+    try {
+      const res = await API.get(`/api/channel/update_balance`);
+      const { success, message } = res.data;
+      if (success) {
+        showInfo(t('channel_row.updateChannelBalance'));
+      } else {
+        showError(message);
+      }
+    } catch (error) {
+      console.log(error);
+    }
+
+    setSearching(false);
+  };
+
+  const handleOpenModal = (channelId) => {
+    setEditChannelId(channelId);
+    setOpenModal(true);
+  };
+
+  const handleCloseModal = () => {
+    setOpenModal(false);
+    setEditChannelId(0);
+  };
+
+  const handleOkModal = (status) => {
+    if (status === true) {
+      handleCloseModal();
+      handleRefresh(false);
+    }
+  };
+
+  const fetchData = async (page, rowsPerPage, keyword, order, orderBy) => {
+    setSearching(true);
+    keyword = trims(keyword);
+    const data = await fetchChannelData(page, rowsPerPage, keyword, order, orderBy);
+
+    if (data) {
+      setListCount(data.total_count);
+      setChannels(data.data);
+    }
+    setSearching(false);
   };
 
+  const fetchGroups = async () => {
+    try {
+      let res = await API.get(`/api/group/`);
+      setGroupOptions(res.data.data);
+    } catch (error) {
+      showError(error.message);
+    }
+  };
+
+  const fetchTags = async () => {
+    try {
+      let res = await API.get(`/api/channel_tag/_all`);
+      const { success, data } = res.data;
+      if (success) {
+        setTags(data);
+      }
+    } catch (error) {
+      showError(error.message);
+    }
+  };
+
+  const fetchModels = async () => {
+    try {
+      let res = await API.get(`/api/channel/models`);
+      const { data } = res.data;
+      // 先对data排序
+      data.sort((a, b) => {
+        const ownedByComparison = a.owned_by.localeCompare(b.owned_by);
+        if (ownedByComparison === 0) {
+          return a.id.localeCompare(b.id);
+        }
+        return ownedByComparison;
+      });
+      setModelOptions(
+        data.map((model) => {
+          return {
+            id: model.id,
+            group: model.owned_by
+          };
+        })
+      );
+    } catch (error) {
+      showError(error.message);
+    }
+  };
+
+  useEffect(() => {
+    fetchData(page, rowsPerPage, searchKeyword, order, orderBy);
+  }, [page, rowsPerPage, searchKeyword, order, orderBy, refreshFlag]);
+
   useEffect(() => {
-    const handleHashChange = () => {
-      const hash = location.hash.replace('#', '');
-      setValue(tabMap[hash] || 0);
-    };
-    window.addEventListener('hashchange', handleHashChange);
-    return () => {
-      window.removeEventListener('hashchange', handleHashChange);
-    };
-  }, [location, tabMap]);
+    fetchGroups().then();
+    fetchTags().then();
+    fetchModels().then();
+  }, []);
 
   return (
-    <>
+    <AdminContainer>
+      <Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
+        <Typography variant="h4">{t('channel_index.channel')}</Typography>
+
+        <ButtonGroup variant="contained" aria-label="outlined small primary button group">
+          <Button color="primary" startIcon={<Icon icon="solar:add-circle-line-duotone" />} onClick={() => handleOpenModal(0)}>
+            {t('channel_index.newChannel')}
+          </Button>
+          <Button color="primary" startIcon={<Icon icon="solar:menu-dots-bold-duotone" />} onClick={() => setOpenBatchModal(true)}>
+            {t('channel_index.batchProcessing')}
+          </Button>
+        </ButtonGroup>
+      </Stack>
+      <Stack mb={5}>
+        <Alert severity="info">
+          {t('channel_index.priorityWeightExplanation')}
+          <br />
+          {t('channel_index.description1')}
+          <br />
+          {t('channel_index.description2')}
+          <br />
+          {t('channel_index.description3')}
+          <br />
+          {t('channel_index.description4')}
+        </Alert>
+      </Stack>
       <Card>
-        <AdminContainer>
-          <Box sx={{ width: '100%' }}>
-            <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
-              <Tabs value={value} onChange={handleChange} variant="scrollable" scrollButtons="auto">
-                <Tab
-                  label={t('channel_index.channelList')}
-                  {...a11yProps(0)}
-                  icon={<Icon icon="solar:menu-dots-bold-duotone" />}
-                  iconPosition="start"
-                />
-                <Tab
-                  label={t('channel_index.channelTags')}
-                  {...a11yProps(1)}
-                  icon={<Icon icon="solar:tag-horizontal-bold-duotone" />}
-                  iconPosition="start"
+        <Box component="form" noValidate>
+          <TableToolBar filterName={toolBarValue} handleFilterName={handleToolBarValue} groupOptions={groupOptions} tags={tags} />
+        </Box>
+
+        <Toolbar
+          sx={{
+            textAlign: 'right',
+            height: 50,
+            display: 'flex',
+            justifyContent: 'space-between',
+            p: (theme) => theme.spacing(0, 1, 0, 3)
+          }}
+        >
+          <Container>
+            {matchUpMd ? (
+              <ButtonGroup variant="outlined" aria-label="outlined small primary button group">
+                <Button onClick={() => handleRefresh(true)} startIcon={<Icon icon="solar:refresh-circle-bold-duotone" width={18} />}>
+                  {t('channel_index.refreshClearSearchConditions')}
+                </Button>
+                <Button onClick={searchChannels} startIcon={<Icon icon="solar:magnifer-bold-duotone" width={18} />}>
+                  {t('channel_index.search')}
+                </Button>
+                <Button
+                  onClick={() => handlePopoverOpen(t('channel_index.testAllChannels'), testAllChannels)}
+                  startIcon={<Icon icon="solar:test-tube-bold-duotone" width={18} />}
+                >
+                  {t('channel_index.testAllChannels')}
+                </Button>
+                <Button
+                  onClick={() => handlePopoverOpen(t('channel_index.updateEnabledBalance'), updateAllChannelsBalance)}
+                  startIcon={<Icon icon="solar:dollar-minimalistic-bold-duotone" width={18} />}
+                >
+                  {t('channel_index.updateEnabledBalance')}
+                </Button>
+                <Button
+                  onClick={() => handlePopoverOpen(t('channel_index.deleteDisabledChannels'), deleteAllDisabledChannels)}
+                  startIcon={<Icon icon="solar:trash-bin-trash-bold-duotone" width={18} />}
+                >
+                  {t('channel_index.deleteDisabledChannels')}
+                </Button>
+              </ButtonGroup>
+            ) : (
+              <Stack
+                direction="row"
+                spacing={0.5}
+                divider={<Divider orientation="vertical" flexItem />}
+                justifyContent="space-around"
+                alignItems="center"
+              >
+                <IconButton onClick={() => handleRefresh(true)} size="large">
+                  <Icon width={20} icon="solar:refresh-circle-bold-duotone" />
+                </IconButton>
+                <IconButton onClick={searchChannels} size="large">
+                  <Icon width={20} icon="solar:magnifer-bold-duotone" />
+                </IconButton>
+                <IconButton onClick={() => handlePopoverOpen(t('channel_index.testAllChannels'), testAllChannels)} size="large">
+                  <Icon width={20} icon="solar:test-tube-bold-duotone" />
+                </IconButton>
+                <IconButton
+                  onClick={() => handlePopoverOpen(t('channel_index.updateEnabledBalance'), updateAllChannelsBalance)}
+                  size="large"
+                >
+                  <Icon width={20} icon="solar:dollar-minimalistic-bold-duotone" />
+                </IconButton>
+                <IconButton
+                  onClick={() => handlePopoverOpen(t('channel_index.deleteDisabledChannels'), deleteAllDisabledChannels)}
+                  size="large"
+                >
+                  <Icon width={20} icon="solar:trash-bin-trash-bold-duotone" />
+                </IconButton>
+              </Stack>
+            )}
+          </Container>
+        </Toolbar>
+        {searching && <LinearProgress />}
+        <TableContainer>
+          <Table sx={{ minWidth: 800 }}>
+            <KeywordTableHead
+              order={order}
+              orderBy={orderBy}
+              onRequestSort={handleSort}
+              headLabel={[
+                // { id: 'collapse', label: '', disableSort: true, width: '50px' },
+                { id: 'id', label: 'ID', disableSort: false, width: '80px' },
+                { id: 'name', label: t('channel_index.name'), disableSort: false },
+                { id: 'group', label: t('channel_index.group'), disableSort: true },
+                { id: 'type', label: t('channel_index.type'), disableSort: false },
+                { id: 'status', label: t('channel_index.status'), disableSort: false },
+                { id: 'response_time', label: t('channel_index.responseTime'), disableSort: false },
+                // { id: 'balance', label: '余额', disableSort: false },
+                { id: 'used', label: t('channel_index.usedBalance'), disableSort: true },
+                { id: 'priority', label: t('channel_index.priority'), disableSort: false, width: '80px' },
+                { id: 'weight', label: t('channel_index.weight'), disableSort: false, width: '80px' },
+                { id: 'action', label: t('channel_index.actions'), disableSort: true }
+              ]}
+            />
+            <TableBody>
+              {channels.map((row) => (
+                <ChannelTableRow
+                  item={row}
+                  manageChannel={manageChannel}
+                  key={row.id}
+                  // handleOpenModal={handleOpenModal}
+                  // setModalChannelId={setEditChannelId}
+                  groupOptions={groupOptions}
+                  onRefresh={handleRefresh}
+                  modelOptions={modelOptions}
                 />
-              </Tabs>
-            </Box>
-            <CustomTabPanel value={value} index={0}>
-              <ChannelList />
-            </CustomTabPanel>
-            <CustomTabPanel value={value} index={1}>
-              <ChannelTag />
-            </CustomTabPanel>
-          </Box>
-        </AdminContainer>
+              ))}
+            </TableBody>
+          </Table>
+        </TableContainer>
+        <TablePagination
+          page={page}
+          component="div"
+          count={listCount}
+          rowsPerPage={rowsPerPage}
+          onPageChange={handleChangePage}
+          rowsPerPageOptions={PAGE_SIZE_OPTIONS}
+          onRowsPerPageChange={handleChangeRowsPerPage}
+          showFirstButton
+          showLastButton
+        />
       </Card>
-    </>
-  );
-};
+      <EditeModal
+        open={openModal}
+        onCancel={handleCloseModal}
+        onOk={handleOkModal}
+        channelId={editChannelId}
+        groupOptions={groupOptions}
+        modelOptions={modelOptions}
+      />
+      <BatchModal open={openBatchModal} setOpen={setOpenBatchModal} />
 
-export default ChannelTab;
+      <ConfirmDialog
+        open={confirm.value}
+        onClose={confirm.onFalse}
+        title={confirmTitle}
+        content={t('common.execute', { title: confirmTitle })}
+        action={
+          <Button
+            variant="contained"
+            onClick={() => {
+              confirmConfirm();
+              confirm.onFalse();
+            }}
+          >
+            {t('common.executeConfirm')}
+          </Button>
+        }
+      />
+    </AdminContainer>
+  );
+}
diff --git a/web/src/views/Channel/type/Config.js b/web/src/views/Channel/type/Config.js
index cce12b58..c57fbe47 100644
--- a/web/src/views/Channel/type/Config.js
+++ b/web/src/views/Channel/type/Config.js
@@ -37,7 +37,8 @@ const defaultConfig = {
     base_url: '可空，请输入中转API地址，例如通过cloudflare中转',
     key: '请输入渠道对应的鉴权密钥',
     other: '',
-    proxy: '单独设置代理地址，支持http和socks5，例如：http://127.0.0.1:1080',
+    proxy:
+      '单独设置代理地址，支持http和socks5，例如：http://127.0.0.1:1080,代理地址中可以通过 `%s` 作为会话标识占位符，程序中检测到有占位符会根据Key生成唯一会话标识符进行替换',
     test_model: '用于测试使用的模型，为空时无法测速,如：gpt-3.5-turbo，仅支持chat模型',
     models:
       '请选择该渠道所支持的模型,你也可以输入通配符*来匹配模型，例如：gpt-3.5*，表示支持所有gpt-3.5开头的模型，*号只能在最后一位使用，前面必须有字符，例如：gpt-3.5*是正确的，*gpt-3.5是错误的',
diff --git a/web/src/views/Log/component/TableRow.jsx b/web/src/views/Log/component/TableRow.jsx
index 65956f55..8d01a11e 100644
--- a/web/src/views/Log/component/TableRow.jsx
+++ b/web/src/views/Log/component/TableRow.jsx
@@ -61,7 +61,7 @@ function requestTSLabelOptions(request_ts) {
   return color;
 }
 
-export default function LogTableRow({ item, userIsAdmin, userGroup }) {
+export default function LogTableRow({ item, userIsAdmin, userGroup, columnVisibility }) {
   const { t } = useTranslation();
   let request_time = item.request_time / 1000;
   let request_time_str = request_time.toFixed(2) + ' S';
@@ -83,50 +83,64 @@ export default function LogTableRow({ item, userIsAdmin, userGroup }) {
   return (
     <>
       <TableRow tabIndex={item.id}>
-        <TableCell>{timestamp2string(item.created_at)}</TableCell>
+        {columnVisibility.created_at && <TableCell sx={{ p: '10px 8px' }}>{timestamp2string(item.created_at)}</TableCell>}
 
-        {userIsAdmin && <TableCell>{(item.channel_id || '') + ' ' + (item.channel?.name ? '(' + item.channel.name + ')' : '')}</TableCell>}
-        {userIsAdmin && (
-          <TableCell>
+        {userIsAdmin && columnVisibility.channel_id && (
+          <TableCell sx={{ p: '10px 8px' }}>
+            {(item.channel_id || '') + ' ' + (item.channel?.name ? '(' + item.channel.name + ')' : '')}
+          </TableCell>
+        )}
+        {userIsAdmin && columnVisibility.user_id && (
+          <TableCell sx={{ p: '10px 8px' }}>
             <Label color="default" variant="outlined" copyText={item.username}>
               {item.username}
             </Label>
           </TableCell>
         )}
 
-        <TableCell>
-          {item?.metadata?.group_name ? (
-            <Label color="default" variant="soft">
-              {userGroup[item.metadata.group_name]?.name || '跟随用户'}
-            </Label>
-          ) : (
-            ''
-          )}
-        </TableCell>
-        <TableCell>
-          {item.token_name && (
-            <Label color="default" variant="soft" copyText={item.token_name}>
-              {item.token_name}
-            </Label>
-          )}
-        </TableCell>
-        <TableCell>{renderType(item.type)}</TableCell>
-        <TableCell>{viewModelName(item.model_name, item.is_stream)}</TableCell>
-
-        <TableCell>
-          <Stack direction="column" spacing={0.5}>
-            <Label color={requestTimeLabelOptions(request_time)}>
-              {item.request_time == 0 ? '无' : request_time_str} {first_time_str ? ' / ' + first_time_str : ''}
-            </Label>
-
-            {request_ts_str && <Label color={requestTSLabelOptions(request_ts)}>{request_ts_str}</Label>}
-          </Stack>
-        </TableCell>
-        <TableCell>{viewInput(item, t, totalInputTokens, totalOutputTokens, show, tokenDetails)}</TableCell>
-        <TableCell>{item.completion_tokens || ''}</TableCell>
-        <TableCell>{item.quota ? renderQuota(item.quota, 6) : '$0'}</TableCell>
-        <TableCell>{item.source_ip || ''}</TableCell>
-        <TableCell>{viewLogContent(item, t, totalInputTokens, totalOutputTokens)}</TableCell>
+        {columnVisibility.group && (
+          <TableCell sx={{ p: '10px 8px' }}>
+            {item?.metadata?.group_name ? (
+              <Label color="default" variant="soft">
+                {userGroup[item.metadata.group_name]?.name || '跟随用户'}
+              </Label>
+            ) : (
+              ''
+            )}
+          </TableCell>
+        )}
+        {columnVisibility.token_name && (
+          <TableCell sx={{ p: '10px 8px' }}>
+            {item.token_name && (
+              <Label color="default" variant="soft" copyText={item.token_name}>
+                {item.token_name}
+              </Label>
+            )}
+          </TableCell>
+        )}
+        {columnVisibility.type && <TableCell sx={{ p: '10px 8px' }}>{renderType(item.type)}</TableCell>}
+        {columnVisibility.model_name && <TableCell sx={{ p: '10px 8px' }}>{viewModelName(item.model_name, item.is_stream)}</TableCell>}
+
+        {columnVisibility.duration && (
+          <TableCell sx={{ p: '10px 8px' }}>
+            <Stack direction="column" spacing={0.5}>
+              <Label color={requestTimeLabelOptions(request_time)}>
+                {item.request_time == 0 ? '无' : request_time_str} {first_time_str ? ' / ' + first_time_str : ''}
+              </Label>
+
+              {request_ts_str && <Label color={requestTSLabelOptions(request_ts)}>{request_ts_str}</Label>}
+            </Stack>
+          </TableCell>
+        )}
+        {columnVisibility.message && (
+          <TableCell sx={{ p: '10px 8px' }}>{viewInput(item, t, totalInputTokens, totalOutputTokens, show, tokenDetails)}</TableCell>
+        )}
+        {columnVisibility.completion && <TableCell sx={{ p: '10px 8px' }}>{item.completion_tokens || ''}</TableCell>}
+        {columnVisibility.quota && <TableCell sx={{ p: '10px 8px' }}>{item.quota ? renderQuota(item.quota, 6) : '$0'}</TableCell>}
+        {columnVisibility.source_ip && <TableCell sx={{ p: '10px 8px' }}>{item.source_ip || ''}</TableCell>}
+        {columnVisibility.detail && (
+          <TableCell sx={{ p: '10px 8px' }}>{viewLogContent(item, t, totalInputTokens, totalOutputTokens)}</TableCell>
+        )}
       </TableRow>
     </>
   );
@@ -135,7 +149,8 @@ export default function LogTableRow({ item, userIsAdmin, userGroup }) {
 LogTableRow.propTypes = {
   item: PropTypes.object,
   userIsAdmin: PropTypes.bool,
-  userGroup: PropTypes.object
+  userGroup: PropTypes.object,
+  columnVisibility: PropTypes.object
 };
 
 function viewModelName(model_name, isStream) {
diff --git a/web/src/views/Log/index.jsx b/web/src/views/Log/index.jsx
index 0c0864f9..2c92b188 100644
--- a/web/src/views/Log/index.jsx
+++ b/web/src/views/Log/index.jsx
@@ -9,8 +9,9 @@ import TablePagination from '@mui/material/TablePagination';
 import LinearProgress from '@mui/material/LinearProgress';
 import ButtonGroup from '@mui/material/ButtonGroup';
 import Toolbar from '@mui/material/Toolbar';
-
-import { Button, Card, Stack, Container, Typography, Box } from '@mui/material';
+import IconButton from '@mui/material/IconButton';
+import Divider from '@mui/material/Divider';
+import { Button, Card, Stack, Container, Typography, Box, Menu, MenuItem, Checkbox, ListItemText } from '@mui/material';
 import LogTableRow from './component/TableRow';
 import KeywordTableHead from 'ui-component/TableHead';
 import TableToolBar from './component/TableToolBar';
@@ -21,6 +22,8 @@ import { Icon } from '@iconify/react';
 import dayjs from 'dayjs';
 import { useTranslation } from 'react-i18next';
 import { UserContext } from 'contexts/UserContext';
+import useMediaQuery from '@mui/material/useMediaQuery';
+import { useTheme } from '@mui/material/styles';
 
 export default function Log() {
   const { t } = useTranslation();
@@ -46,10 +49,60 @@ export default function Log() {
   const [searchKeyword, setSearchKeyword] = useState(originalKeyword);
   const [refreshFlag, setRefreshFlag] = useState(false);
   const { userGroup } = useContext(UserContext);
+  const theme = useTheme();
+  const matchUpMd = useMediaQuery(theme.breakpoints.up('sm'));
 
   const [logs, setLogs] = useState([]);
   const userIsAdmin = isAdmin();
 
+  // 添加列显示设置相关状态
+  const [columnVisibility, setColumnVisibility] = useState({
+    created_at: true,
+    channel_id: true,
+    user_id: true,
+    group: true,
+    token_name: true,
+    type: true,
+    model_name: true,
+    duration: true,
+    message: true,
+    completion: true,
+    quota: true,
+    source_ip: true,
+    detail: true
+  });
+  const [columnMenuAnchor, setColumnMenuAnchor] = useState(null);
+
+  // 处理列显示菜单打开和关闭
+  const handleColumnMenuOpen = (event) => {
+    setColumnMenuAnchor(event.currentTarget);
+  };
+
+  const handleColumnMenuClose = () => {
+    setColumnMenuAnchor(null);
+  };
+
+  // 处理列显示状态变更
+  const handleColumnVisibilityChange = (columnId) => {
+    setColumnVisibility({
+      ...columnVisibility,
+      [columnId]: !columnVisibility[columnId]
+    });
+  };
+
+  // 处理全选/取消全选列显示
+  const handleSelectAllColumns = () => {
+    const allColumns = Object.keys(columnVisibility);
+    const areAllVisible = allColumns.every((column) => columnVisibility[column]);
+
+    const newColumnVisibility = {};
+    allColumns.forEach((column) => {
+      newColumnVisibility[column] = !areAllVisible;
+    });
+
+    setColumnVisibility(newColumnVisibility);
+  };
+
   const handleSort = (event, id) => {
     const isAsc = orderBy === id && order === 'asc';
     if (id !== '') {
@@ -145,15 +198,89 @@ export default function Log() {
           }}
         >
           <Container>
-            <ButtonGroup variant="outlined" aria-label="outlined small primary button group">
-              <Button onClick={handleRefresh} startIcon={<Icon icon="solar:refresh-bold-duotone" width={18} />}>
-                {t('logPage.refreshButton')}
-              </Button>
+            {matchUpMd ? (
+              <ButtonGroup variant="outlined" aria-label="outlined small primary button group">
+                <Button onClick={handleRefresh} size="small" startIcon={<Icon icon="solar:refresh-bold-duotone" width={18} />}>
+                  {t('logPage.refreshButton')}
+                </Button>
+
+                <Button onClick={searchLogs} size="small" startIcon={<Icon icon="solar:minimalistic-magnifer-line-duotone" width={18} />}>
+                  {t('logPage.searchButton')}
+                </Button>
 
-              <Button onClick={searchLogs} startIcon={<Icon icon="solar:minimalistic-magnifer-line-duotone" width={18} />}>
-                {t('logPage.searchButton')}
-              </Button>
-            </ButtonGroup>
+                <Button onClick={handleColumnMenuOpen} size="small" startIcon={<Icon icon="solar:settings-bold-duotone" width={18} />}>
+                  {t('logPage.columnSettings')}
+                </Button>
+              </ButtonGroup>
+            ) : (
+              <Stack
+                direction="row"
+                spacing={1}
+                divider={<Divider orientation="vertical" flexItem />}
+                justifyContent="space-around"
+                alignItems="center"
+              >
+                <IconButton onClick={handleRefresh} size="small">
+                  <Icon icon="solar:refresh-bold-duotone" width={18} />
+                </IconButton>
+                <IconButton onClick={searchLogs} size="small">
+                  <Icon icon="solar:minimalistic-magnifer-line-duotone" width={18} />
+                </IconButton>
+                <IconButton onClick={handleColumnMenuOpen} size="small">
+                  <Icon icon="solar:settings-bold-duotone" width={18} />
+                </IconButton>
+              </Stack>
+            )}
+
+            <Menu
+              anchorEl={columnMenuAnchor}
+              open={Boolean(columnMenuAnchor)}
+              onClose={handleColumnMenuClose}
+              PaperProps={{
+                style: {
+                  maxHeight: 300,
+                  width: 200
+                }
+              }}
+            >
+              <MenuItem disabled>
+                <Typography variant="subtitle2">{t('logPage.selectColumns')}</Typography>
+              </MenuItem>
+              <MenuItem onClick={handleSelectAllColumns} dense>
+                <Checkbox
+                  checked={Object.values(columnVisibility).every((visible) => visible)}
+                  indeterminate={
+                    !Object.values(columnVisibility).every((visible) => visible) &&
+                    Object.values(columnVisibility).some((visible) => visible)
+                  }
+                  size="small"
+                />
+                <ListItemText primary={t('logPage.columnSelectAll')} />
+              </MenuItem>
+              {[
+                { id: 'created_at', label: t('logPage.timeLabel') },
+                { id: 'channel_id', label: t('logPage.channelLabel'), adminOnly: true },
+                { id: 'user_id', label: t('logPage.userLabel'), adminOnly: true },
+                { id: 'group', label: t('logPage.groupLabel') },
+                { id: 'token_name', label: t('logPage.tokenLabel') },
+                { id: 'type', label: t('logPage.typeLabel') },
+                { id: 'model_name', label: t('logPage.modelLabel') },
+                { id: 'duration', label: t('logPage.durationLabel') },
+                { id: 'message', label: t('logPage.inputLabel') },
+                { id: 'completion', label: t('logPage.outputLabel') },
+                { id: 'quota', label: t('logPage.quotaLabel') },
+                { id: 'source_ip', label: t('logPage.sourceIp') },
+                { id: 'detail', label: t('logPage.detailLabel') }
+              ].map(
+                (column) =>
+                  (!column.adminOnly || userIsAdmin) && (
+                    <MenuItem key={column.id} onClick={() => handleColumnVisibilityChange(column.id)} dense>
+                      <Checkbox checked={columnVisibility[column.id] || false} size="small" />
+                      <ListItemText primary={column.label} />
+                    </MenuItem>
+                  )
+              )}
+            </Menu>
           </Container>
         </Toolbar>
         {searching && <LinearProgress />}
@@ -168,76 +295,93 @@ export default function Log() {
                   {
                     id: 'created_at',
                     label: t('logPage.timeLabel'),
-                    disableSort: false
+                    disableSort: false,
+                    hide: !columnVisibility.created_at
                   },
                   {
                     id: 'channel_id',
                     label: t('logPage.channelLabel'),
                     disableSort: false,
-                    hide: !userIsAdmin
+                    hide: !columnVisibility.channel_id || !userIsAdmin
                   },
                   {
                     id: 'user_id',
                     label: t('logPage.userLabel'),
                     disableSort: false,
-                    hide: !userIsAdmin
+                    hide: !columnVisibility.user_id || !userIsAdmin
                   },
                   {
                     id: 'group',
                     label: t('logPage.groupLabel'),
-                    disableSort: false
+                    disableSort: false,
+                    hide: !columnVisibility.group
                   },
                   {
                     id: 'token_name',
                     label: t('logPage.tokenLabel'),
-                    disableSort: false
+                    disableSort: false,
+                    hide: !columnVisibility.token_name
                   },
                   {
                     id: 'type',
                     label: t('logPage.typeLabel'),
-                    disableSort: false
+                    disableSort: false,
+                    hide: !columnVisibility.type
                   },
                   {
                     id: 'model_name',
                     label: t('logPage.modelLabel'),
-                    disableSort: false
+                    disableSort: false,
+                    hide: !columnVisibility.model_name
                   },
                   {
                     id: 'duration',
                     label: t('logPage.durationLabel'),
                     tooltip: t('logPage.durationTooltip'),
-                    disableSort: true
+                    disableSort: true,
+                    hide: !columnVisibility.duration
                   },
                   {
                     id: 'message',
                     label: t('logPage.inputLabel'),
-                    disableSort: true
+                    disableSort: true,
+                    hide: !columnVisibility.message
                   },
                   {
                     id: 'completion',
                     label: t('logPage.outputLabel'),
-                    disableSort: true
+                    disableSort: true,
+                    hide: !columnVisibility.completion
                   },
                   {
                     id: 'quota',
                     label: t('logPage.quotaLabel'),
-                    disableSort: true
+                    disableSort: true,
+                    hide: !columnVisibility.quota
                   },
                   {
                     id: 'source_ip',
                     label: t('logPage.sourceIp'),
-                    disableSort: true
+                    disableSort: true,
+                    hide: !columnVisibility.source_ip
                   },
                   {
                     id: 'detail',
                     label: t('logPage.detailLabel'),
-                    disableSort: true
+                    disableSort: true,
+                    hide: !columnVisibility.detail
                   }
                 ]}
               />
               <TableBody>
                 {logs.map((row, index) => (
-                  <LogTableRow item={row} key={`${row.id}_${index}`} userIsAdmin={userIsAdmin} userGroup={userGroup} />
+                  <LogTableRow
+                    item={row}
+                    key={`${row.id}_${index}`}
+                    userIsAdmin={userIsAdmin}
+                    userGroup={userGroup}
+                    columnVisibility={columnVisibility}
+                  />
                 ))}
               </TableBody>
             </Table>
diff --git a/web/src/views/Token/component/TableRow.jsx b/web/src/views/Token/component/TableRow.jsx
index 25abc28d..7ad0266e 100644
--- a/web/src/views/Token/component/TableRow.jsx
+++ b/web/src/views/Token/component/TableRow.jsx
@@ -26,6 +26,9 @@ import Label from 'ui-component/Label';
 import { Icon } from '@iconify/react';
 import { IconCaretDownFilled } from '@tabler/icons-react';
 import { useTranslation } from 'react-i18next';
+import useMediaQuery from '@mui/material/useMediaQuery';
+import { useTheme } from '@mui/material/styles';
+
 function createMenu(menuItems) {
   return (
     <>
@@ -62,6 +65,8 @@ export default function TokensTableRow({ item, manageToken, handleOpenModal, set
   const [statusSwitch, setStatusSwitch] = useState(item.status);
   const siteInfo = useSelector((state) => state.siteInfo);
   const chatLinks = getChatLinks();
+  const theme = useTheme();
+  const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
 
   const handleDeleteOpen = () => {
     handleCloseMenu();
@@ -207,14 +212,16 @@ export default function TokensTableRow({ item, manageToken, handleOpenModal, set
                   copy(`sk-${item.key}`, t('token_index.token'));
                 }}
               >
-                {t('token_index.copy')}
+                {isMobile ? <Icon icon="mdi:content-copy" /> : t('token_index.copy')}
               </Button>
               <Button size="small" onClick={(e) => handleOpenMenu(e, 'copy')}>
                 <IconCaretDownFilled size={'16px'} />
               </Button>
             </ButtonGroup>
             <ButtonGroup size="small" onClick={(e) => handleOpenMenu(e, 'link')} aria-label="split button">
-              <Button color="primary">{t('token_index.chat')}</Button>
+              <Button size="small" color="primary">
+                {isMobile ? <Icon icon="mdi:chat" /> : t('token_index.chat')}
+              </Button>
               <Button size="small">
                 <IconCaretDownFilled size={'16px'} />
               </Button>



Test patch:
diff --git a/controller/channel-test.go b/controller/channel-test.go
index 658fdb17..1f6afcef 100644
--- a/controller/channel-test.go
+++ b/controller/channel-test.go
@@ -38,6 +38,7 @@ func testChannel(channel *model.Channel, testModel string) (openaiErr *types.Ope
 	}
 
 	channelType := getModelType(testModel)
+	channel.SetProxy()
 
 	var url string
 	switch channelType {
@@ -123,7 +124,9 @@ func testChannel(channel *model.Channel, testModel string) (openaiErr *types.Ope
 		if strings.HasPrefix(newModelName, "o1") || strings.HasPrefix(newModelName, "o3") {
 			testRequest.MaxCompletionTokens = 10
 		} else {
-			testRequest.MaxTokens = 10
+			if !strings.Contains(newModelName, "gemini-2") {
+				testRequest.MaxTokens = 10
+			}
 		}
 		response, openAIErrorWithStatusCode = chatProvider.CreateChatCompletion(testRequest)
 	default: