Repository: SwanHubX/SwanLab (language: python)

Issue:
[BUG] logdir参数出现错误
<!-- Describe your bugs below ^_^ -->

## 🐛 Bug description [Please make everyone to understand it]

如果指定了logdir，sqlite还是出现在了默认文件夹下

## 🧑‍💻 Step to reproduce

编写单测时发现此问题

## 👾 Expected result

将在与dashboard解藕时同时修复此bug

## 🚑 Any additional [like screenshots]

- **SwanLab Version**: v0.3.10




Discussion / hints:

Fix patch:
diff --git a/.config/copy_frontend.py b/.config/copy_frontend.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index a73ce502..00000000
--- a/.editorconfig
+++ /dev/null
@@ -1,21 +0,0 @@
-root = true
-
-[*]
-indent_style = space
-end_of_line = lf
-charset = utf-8
-trim_trailing_whitespace = true
-insert_final_newline = true
-
-[*.md]
-trim_trailing_whitespace = false
-
-# 配置前端文件缩进
-[*.{ts,js,cjs,vue,css}]
-indent_style = space
-indent_size = 2
-
-# 配置python文件缩进
-[*.py]
-indent_style = space
-indent_size = 4
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index fbb2af69..00000000
--- a/.eslintignore
+++ /dev/null
@@ -1,19 +0,0 @@
-node_modules/
-
-**/*.spec.*
-
-**/style/
-
-*.html
-
-es/
-
-lib/
-
-_site/
-
-dist/
-
-package.json
-
-swanlab/
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
deleted file mode 100644
index 3664d5bb..00000000
--- a/.eslintrc.cjs
+++ /dev/null
@@ -1,21 +0,0 @@
-module.exports = {
-  env: {
-    browser: true,
-    es2021: true,
-    node: true
-  },
-  extends: [
-    'plugin:vue/base',
-    'eslint:recommended',
-    'plugin:vue/vue3-essential',
-    './.eslintrc-auto-import.json'
-    // "plugin:vue/vue3-strongly-recommended"
-  ],
-  overrides: [],
-  parserOptions: {
-    ecmaVersion: 'latest',
-    sourceType: 'module'
-  },
-  plugins: ['vue'],
-  rules: {}
-}
diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml
index 9f37636e..79f9bbae 100644
--- a/.github/workflows/publish-to-pypi.yml
+++ b/.github/workflows/publish-to-pypi.yml
@@ -3,7 +3,7 @@ name: Publish to PyPI
 on:
   push:
     tags:
-      - 'v*.*.*'
+      - "v*.*.*"
 
 jobs:
   publish:
@@ -18,14 +18,8 @@ jobs:
         with:
           python-version: "3.10"
 
-      - name: Set Nodejs
-        uses: actions/setup-node@v4
-        with:
-          node-version: 18.15.0
-
       - name: Install Dependencies
         run: |
-          npm install
           pip install -r requirements.txt
           pip install build
           pip install twine
@@ -36,7 +30,6 @@ jobs:
           TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
 
         run: |
-          npm run build.release
           python -m build
           python -m twine upload dist/*
 
diff --git a/.gitignore b/.gitignore
index 329295d0..4af844da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,14 +1,12 @@
 # frontend
-node_modules
-template
-package-lock.json
-pnpm-lock.yaml
-vue/components.d.ts
-vue/auto-imports.d.ts
 swanlog/
 swanlog.bak/
-.eslintrc-auto-import.json
+node_modules/
 dist/
+vue/
+swanlab/template/
+package-lock.json
+.eslintrc-auto-import.json
 
 # test
 temp/
diff --git a/.husky/pre-commit b/.husky/pre-commit
deleted file mode 100755
index 64778a1e..00000000
--- a/.husky/pre-commit
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env sh
-. "$(dirname -- "$0")/_/husky.sh"
-
diff --git a/.husky/pre-push b/.husky/pre-push
deleted file mode 100755
index fee07c69..00000000
--- a/.husky/pre-push
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env sh
-. "$(dirname -- "$0")/_/husky.sh"
-
-
-
-npm run fmt
diff --git a/.prettierignore b/.prettierignore
deleted file mode 100644
index a1a701d7..00000000
--- a/.prettierignore
+++ /dev/null
@@ -1,68 +0,0 @@
-
-**/*.svg
-
-node_modules/
-
-package.json
-
-lib/
-
-es/
-
-dist/
-
-_site/
-
-coverage/
-
-CNAME
-
-LICENlock
-
-netlifSE
-
-yarn.y.toml
-
-yarn-error.log
-
-*.sh
-
-*.snap
-
-.gitignore
-
-.npmignore
-
-.prettierignore
-
-.DS_Store
-
-.editorconfig
-
-.eslintignore
-
-**/*.yml
-
-components/style/color/*.less
-
-**/assets
-
-.gitattributes
-
-.stylelintrc
-
-.vcmrc
-
-.logo
-
-.npmrc.template
-
-.huskyrc
-
-doc/**
-
-*.md
-.github/**
-
-swanlab/
-
diff --git a/.prettierrc b/.prettierrc
deleted file mode 100644
index f800bee3..00000000
--- a/.prettierrc
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "bracketSpacing": true,
-  "printWidth": 120,
-  "proseWrap": "always",
-  "semi": false,
-  "singleQuote": true,
-  "trailingComma": "none"
-}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index f5d136b0..a7599bde 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,15 +1,5 @@
 {
   "recommendations": [
-    /**
-     * 前端相关插件
-     */
-    "Vue.volar",
-    "esbenp.prettier-vscode",
-    "bradlc.vscode-tailwindcss",
-    "dbaeumer.vscode-eslint",
-    "esbenp.prettier-vscode",
-    "lokalise.i18n-ally",
-    "mrmlnc.vscode-json5",
     /**
      * python插件
      */
@@ -23,7 +13,7 @@
      */
     // 代码风格统一
     "editorconfig.editorconfig",
-    // 代码标记,TODO FIXME等
+    // 代码标记,比如TODO FIXME等
     "gruntfuggly.todo-tree",
     // 代码协作，git协作
     "eamodio.gitlens",
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 0e426304..edc067f8 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -4,27 +4,6 @@
   // 欲了解更多信息，请访问: https://go.microsoft.com/fwlink/?linkid=830387
   "version": "0.2.0",
   "configurations": [
-    // 启动前端开发服务
-    {
-      "name": "前端开发",
-      "request": "launch",
-      "cwd": "${workspaceRoot}",
-      "type": "node",
-      "runtimeExecutable": "npm",
-      "runtimeArgs": ["run", "dev"]
-    },
-    // 启动后端开发服务
-    {
-      "name": "后端开发",
-      "type": "debugpy",
-      "request": "launch",
-      "program": "${workspaceFolder}/test/start_server.py",
-      "console": "integratedTerminal",
-      "justMyCode": true,
-      "cwd": "${workspaceFolder}",
-      //sys.path 会加入顶层目录，影响模块导入查询路径
-      "env": { "PYTHONPATH": "${workspaceFolder}" }
-    },
     // 模拟实验开启
     {
       "name": "开启一个实验",
@@ -35,7 +14,9 @@
       "justMyCode": true,
       "cwd": "${workspaceFolder}",
       //sys.path 会加入顶层目录，影响模块导入查询路径
-      "env": { "PYTHONPATH": "${workspaceFolder}" }
+      "env": {
+        "PYTHONPATH": "${workspaceFolder}"
+      }
     },
     // python运行当前文件
     {
@@ -47,14 +28,18 @@
       "justMyCode": true,
       "cwd": "${workspaceFolder}",
       //sys.path 会加入顶层目录，影响模块导入查询路径
-      "env": { "PYTHONPATH": "${workspaceFolder}" }
+      "env": {
+        "PYTHONPATH": "${workspaceFolder}"
+      }
     },
     {
       "name": "测试当前文件",
       "type": "debugpy",
       "request": "launch",
       "module": "pytest",
-      "args": ["${file}"],
+      "args": [
+        "${file}"
+      ],
       "console": "integratedTerminal"
     },
     {
@@ -62,7 +47,9 @@
       "type": "debugpy",
       "request": "launch",
       "module": "pytest",
-      "args": ["test/unit"],
+      "args": [
+        "test/unit"
+      ],
       "console": "integratedTerminal"
     },
     // 打包命令
diff --git a/.vscode/swanlab.code-snippets b/.vscode/swanlab.code-snippets
index 5e08eb10..856a2192 100644
--- a/.vscode/swanlab.code-snippets
+++ b/.vscode/swanlab.code-snippets
@@ -20,123 +20,5 @@
     "scope": "python",
     "description": "python文件块注释",
     "body": ["# ---------------------------------- $1 ----------------------------------", "$2"]
-  },
-  "frontend-comment": {
-    "prefix": ["comment", "comm"],
-    "description": "前端文件块注释",
-    "scope": "vue, javascript,typescript",
-    "body": ["", "// ---------------------------------- $1 ----------------------------------", "", "$2"]
-  },
-  "vue-template": {
-    "scope": "vue",
-    "prefix": "vue",
-    "description": "vue文件模板",
-    "body": [
-      "<template> </template>",
-      "",
-      "<script setup>",
-      "/**",
-      " * @description: $1",
-      " * @file: ${TM_FILENAME} ",
-      " * @since: ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} ${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND} ",
-      "**/",
-      "$2",
-      "</script>",
-      "",
-      "<style lang=\"scss\" scoped></style>",
-      ""
-    ]
-  },
-  "vue-chart": {
-    "prefix": "vue-chart",
-    "body": [
-      "<template>",
-      "  <!-- 图表标题 -->",
-      "  <p class=\"text-center font-semibold\">{{ title }}</p>",
-      "  <!-- 如果图表数据错误 -->",
-      "  <div class=\"flex flex-col justify-center grow text-dimmer gap-2\" v-if=\"error\">",
-      "    <SLIcon class=\"mx-auto h-5 w-5\" icon=\"error\" />",
-      "    <p class=\"text-center text-xs\">",
-      "      <!-- 在此处显示错误信息 -->",
-      "    </p>",
-      "  </div>",
-      "  <!-- 如果图表数据正确 -->",
-      "  <template v-else>",
-      "    <!-- 在此处完成图表主体定义 -->",
-      "    <!-- 放大效果弹窗 -->",
-      "    <SLModal class=\"p-10 pt-0 overflow-hidden\" max-w=\"-1\" v-model=\"isZoom\"> </SLModal>",
-      "  </template>",
-      "</template>",
-      "",
-      "<script setup>",
-      "/**",
-      " * @description: $1",
-      " * @file: ${TM_FILENAME} ",
-      " * @since: ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} ${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND} ",
-      "**/",
-      "import SLModal from '@swanlab-vue/components/SLModal.vue'",
-      "import SLIcon from '@swanlab-vue/components/SLIcon.vue'",
-      "import { ref, inject } from 'vue'",
-      "import { addTaskToBrowserMainThread } from '@swanlab-vue/utils/browser'",
-      "import * as UTILS from './utils'",
-      "",
-      "// ---------------------------------- 配置 ----------------------------------",
-      "const props = defineProps({",
-      "  title: {",
-      "    type: String,",
-      "    required: true",
-      "  },",
-      "  chart: {",
-      "    type: Object,",
-      "    required: true",
-      "  },",
-      "  index: {",
-      "    type: Number,",
-      "    required: true",
-      "  }",
-      "})",
-      "",
-      "// ---------------------------------- 错误处理，如果chart.error存在，则下面的api都将不应该被执行 ----------------------------------",
-      "",
-      "const error = ref(props.chart.error)",
-      "",
-      "// ---------------------------------- 图表颜色配置 ----------------------------------",
-      "// 后续需要适配不同的颜色，但是Line不支持css变量，考虑自定义主题或者js获取css变量完成计算",
-      "const colors = inject('colors')",
-      "if (!colors) throw new Error('colors is not defined, please provide colors in parent component')",
-      "// ---------------------------------- 组件渲染逻辑 ----------------------------------",
-      "",
-      "// ---------------------------------- 数据格式化 ----------------------------------",
-      "",
-      "// ---------------------------------- 渲染、重渲染功能 ----------------------------------",
-      "",
-      "// 渲染",
-      "const render = (data) => {}",
-      "// 重渲染",
-      "const change = (data) => {}",
-      "",
-      "// ---------------------------------- 放大功能 ----------------------------------",
-      "// 是否放大",
-      "const isZoom = ref(false)",
-      "// 放大数据",
-      "const zoom = (data) => {",
-      "  isZoom.value = true",
-      "  // 放大后图表的高度",
-      "  const height = window.innerHeight * 0.6",
-      "  addTaskToBrowserMainThread(() => {})",
-      "}",
-      "",
-      "// ---------------------------------- 暴露api ----------------------------------",
-      "defineExpose({",
-      "  render,",
-      "  change,",
-      "  zoom",
-      "})",
-      "</script>",
-      "",
-      "<style lang=\"scss\" scoped></style>",
-      ""
-    ],
-    "description": "vue-chart"
   }
 }
diff --git a/.vscode/tailwind.json b/.vscode/tailwind.json
deleted file mode 100644
index 96a1f579..00000000
--- a/.vscode/tailwind.json
+++ /dev/null
@@ -1,55 +0,0 @@
-{
-  "version": 1.1,
-  "atDirectives": [
-    {
-      "name": "@tailwind",
-      "description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
-      "references": [
-        {
-          "name": "Tailwind Documentation",
-          "url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
-        }
-      ]
-    },
-    {
-      "name": "@apply",
-      "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
-      "references": [
-        {
-          "name": "Tailwind Documentation",
-          "url": "https://tailwindcss.com/docs/functions-and-directives#apply"
-        }
-      ]
-    },
-    {
-      "name": "@responsive",
-      "description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n  .alert {\n    background-color: #E53E3E;\n  }\n}\n```\n",
-      "references": [
-        {
-          "name": "Tailwind Documentation",
-          "url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
-        }
-      ]
-    },
-    {
-      "name": "@screen",
-      "description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n  /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n  /* ... */\n}\n```\n",
-      "references": [
-        {
-          "name": "Tailwind Documentation",
-          "url": "https://tailwindcss.com/docs/functions-and-directives#screen"
-        }
-      ]
-    },
-    {
-      "name": "@variants",
-      "description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n   .btn-brand {\n    background-color: #3182CE;\n  }\n}\n```\n",
-      "references": [
-        {
-          "name": "Tailwind Documentation",
-          "url": "https://tailwindcss.com/docs/functions-and-directives#variants"
-        }
-      ]
-    }
-  ]
-}
diff --git a/README.md b/README.md
index 4c4a0ad7..e733e164 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,7 @@
 [![Overview](readme_files/swanlab-overview-new.png)](https://swanlab.cn/)
 
 <p align="center">
-<a href="https://swanlab.cn">SwanLab Cloud</a> · <a href="https://docs.swanlab.cn">Document</a> · <a href="https://geektechstudio.feishu.cn/wiki/NIZ9wp5LRiSqQykizbGcVzUKnic">WeChat</a> · <a href="https://github.com/swanhubx/swanlab/issues">Report Issue</a> · <a href="https://geektechstudio.feishu.cn/share/base/form/shrcnyBlK8OMD0eweoFcc2SvWKc">Feedback</a> · <a href="https://docs.swanlab.cn/zh/guide_cloud/general/changelog.html">Changelog</a>
-
+<a href="https://swanlab.cn">🔥SwanLab Cloud</a> · <a href="https://docs.swanlab.cn">📃Document</a> · <a href="https://geektechstudio.feishu.cn/wiki/NIZ9wp5LRiSqQykizbGcVzUKnic">WeChat</a> · <a href="https://github.com/swanhubx/swanlab/issues">Report Issue</a> · <a href="https://geektechstudio.feishu.cn/share/base/form/shrcnyBlK8OMD0eweoFcc2SvWKc">Feedback</a> · <a href="https://docs.swanlab.cn/zh/guide_cloud/general/changelog.html">Changelog</a>
 
 </p>
 
@@ -32,22 +31,22 @@
 - [👋🏻 Introduction](#-Introduction)
 - [📃 Demo](#-demo)
 - [🏁 Quick Start](#🏁-quick-start)
-    - [1.Installation](#1installation)
-    - [2.Log In and Get the API Key](#2log-in-and-get-the-api-key)
-    - [3.Integrate SwanLab with Your Code](#3-integrate-swanlab-with-your-code)
+  - [1.Installation](#1installation)
+  - [2.Log In and Get the API Key](#2log-in-and-get-the-api-key)
+  - [3.Integrate SwanLab with Your Code](#3-integrate-swanlab-with-your-code)
 - [💻 Self-hosted](#-self-hosted)
-    - [Offline Experiment Tracking](#offline-experiment-tracking)
-    - [Open Offline Dashboard](#open-offline-board)
+  - [Offline Experiment Tracking](#offline-experiment-tracking)
+  - [Open Offline Dashboard](#open-offline-board)
 - [🚗 Integration](#-integration)
 - [🆚 Comparison with Familiar Tools](#-comparison-with-familiar-tools)
-    - [Tensorboard vs SwanLab](#tensorboard-vs-swanlab)
-    - [Weights & Biases vs SwanLab](#weights-and-biases-vs-swanlab)
+  - [Tensorboard vs SwanLab](#tensorboard-vs-swanlab)
+  - [Weights & Biases vs SwanLab](#weights-and-biases-vs-swanlab)
 - [👥 Community](#-community)
-    - [Community and Support](#community-and-support)
-    - [SwanLab README Badge](#swanlab-readme-badge)
-    - [Citing SwanLab in the Paper](#citing-swanlab-in-the-paper)
-    - [Contribute to SwanLab](#contribute-to-swanlab)
-    - [Download Icon](#download-icon)
+  - [Community and Support](#community-and-support)
+  - [SwanLab README Badge](#swanlab-readme-badge)
+  - [Citing SwanLab in the Paper](#citing-swanlab-in-the-paper)
+  - [Contribute to SwanLab](#contribute-to-swanlab)
+  - [Download Icon](#download-icon)
 - [📃 License](#-license)
 
 <br/>
@@ -95,8 +94,6 @@ viewing and experiment management.
 >
 > **Star Us**, You will receive all release notifications from GitHub without any delay ~ ⭐️
 
-
-
 ![star-us](readme_files/star-us.png)
 
 <br>
@@ -105,15 +102,15 @@ viewing and experiment management.
 
 Check out SwanLab's online demo：
 
-| [ResNet50 Cats vs Dogs](https://swanlab.cn/@ZeyiLin/Cats_Dogs_Classification/runs/jzo93k112f15pmx14vtxf/chart) | [Yolov8-COCO128](https://swanlab.cn/@ZeyiLin/ultratest/runs/yux7vclmsmmsar9ear7u5/chart)|
-|:---:|:---:|
+|                  [ResNet50 Cats vs Dogs](https://swanlab.cn/@ZeyiLin/Cats_Dogs_Classification/runs/jzo93k112f15pmx14vtxf/chart)                  |                     [Yolov8-COCO128](https://swanlab.cn/@ZeyiLin/ultratest/runs/yux7vclmsmmsar9ear7u5/chart)                     |
+| :----------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------: |
 | <a href="https://swanlab.cn/@ZeyiLin/Cats_Dogs_Classification/runs/jzo93k112f15pmx14vtxf/chart"> <img src="readme_files/example-mnist.png"> </a> | <a href="https://swanlab.cn/@ZeyiLin/ultratest/runs/yux7vclmsmmsar9ear7u5/chart"> <img src="readme_files/example-yolo.png"> </a> |
-| Track the image classification task of training a simple ResNet50 model on the cats and dogs dataset. | Perform object detection tasks using Yolov8 on the COCO128 dataset, tracking training hyperparameters and metrics. |
+|                      Track the image classification task of training a simple ResNet50 model on the cats and dogs dataset.                       |        Perform object detection tasks using Yolov8 on the COCO128 dataset, tracking training hyperparameters and metrics.        |
 
-| [Qwen2 Instruction Finetune](https://swanlab.cn/@ZeyiLin/Qwen2-fintune/runs/cfg5f8dzkp6vouxzaxlx6/chart)| [LSTM Google Stock Prediction](https://swanlab.cn/@ZeyiLin/Google-Stock-Prediction/charts) |
-|:---:|:---:|
+|               [Qwen2 Instruction Finetune](https://swanlab.cn/@ZeyiLin/Qwen2-fintune/runs/cfg5f8dzkp6vouxzaxlx6/chart)                |              [LSTM Google Stock Prediction](https://swanlab.cn/@ZeyiLin/Google-Stock-Prediction/charts)              |
+| :-----------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------: |
 | <a href="https://swanlab.cn/@ZeyiLin/Qwen2-fintune/runs/cfg5f8dzkp6vouxzaxlx6/chart"> <img src="readme_files/example-qwen2.png"> </a> | <a href="https://swanlab.cn/@ZeyiLin/Google-Stock-Prediction/charts"> <img src="readme_files/example-lstm.png"> </a> |
-| Track the instruction fine-tuning training of the Qwen2 large language model, completing simple instruction following. | Train a simple LSTM model on the Google stock price dataset to predict future stock prices. |
+|        Track the instruction fine-tuning training of the Qwen2 large language model, completing simple instruction following.         |             Train a simple LSTM model on the Google stock price dataset to predict future stock prices.              |
 
 [More examples](https://docs.swanlab.cn/zh/examples/mnist.html)
 
@@ -163,7 +160,6 @@ for i in range(10):
 
 <br>
 
-
 ## 💻 Self-hosted
 
 The community edition supports offline viewing of SwanLab dashboards.
@@ -198,7 +194,7 @@ Other parts are completely consistent with cloud usage.
 Open the terminal and use the following command to open a SwanLab dashboard:
 
 ```bash
-swanlab watch -l ./logs
+swanlab watch ./logs
 ```
 
 After the operation is completed, SwanLab will provide you with a local URL link (default
@@ -390,6 +386,7 @@ visualizer = dict(
     vis_backends=vis_backends,
 )
 ```
+
 </details>
 
 <details>
@@ -411,15 +408,13 @@ add_swanlab_callback(model)
 
 model.train(
     data="./coco.yaml",
-    epochs=50, 
+    epochs=50,
     imgsz=320,
 )
 ```
 
 </details>
 
-
-
 <br>
 
 ## 🆚 Comparison with familiar tools
diff --git a/README_cn.md b/README_cn.md
index a3ea5bb9..a5a9053b 100644
--- a/README_cn.md
+++ b/README_cn.md
@@ -1,7 +1,7 @@
 [![Overview](readme_files/swanlab-overview-new.png)](https://swanlab.cn/)
 
 <p align="center">
-<a href="https://swanlab.cn">🔥SwanLab在线版</a> · <a href="https://docs.swanlab.cn">文档</a> · <a href="https://geektechstudio.feishu.cn/wiki/NIZ9wp5LRiSqQykizbGcVzUKnic">微信</a> · <a href="https://github.com/swanhubx/swanlab/issues">报告问题</a> · <a href="https://geektechstudio.feishu.cn/share/base/form/shrcnyBlK8OMD0eweoFcc2SvWKc">建议反馈</a>  · <a href="https://docs.swanlab.cn/zh/guide_cloud/general/changelog.html">更新日志</a>
+<a href="https://swanlab.cn">🔥SwanLab在线版</a> · <a href="https://docs.swanlab.cn">📃文档</a> · <a href="https://geektechstudio.feishu.cn/wiki/NIZ9wp5LRiSqQykizbGcVzUKnic">微信</a> · <a href="https://github.com/swanhubx/swanlab/issues">报告问题</a> · <a href="https://geektechstudio.feishu.cn/share/base/form/shrcnyBlK8OMD0eweoFcc2SvWKc">建议反馈</a>  · <a href="https://docs.swanlab.cn/zh/guide_cloud/general/changelog.html">更新日志</a>
 </p>
 
 <p align="center">
@@ -27,64 +27,63 @@
 
 #### TOC
 
-- [👋🏻 什么是SwanLab](#-什么是swanlab)
+- [👋🏻 什么是 SwanLab](#-什么是swanlab)
 - [📃 在线演示](#-在线演示)
 - [🏁 快速开始](#-快速开始)
-    - [1.安装](#1安装)
-    - [2.登录并获取API Key](#2登录并获取api-key)
-    - [3.将SwanLab与你的代码集成](#3将SwanLab与你的代码集成)
+  - [1.安装](#1安装)
+  - [2.登录并获取 API Key](#2登录并获取api-key)
+  - [3.将 SwanLab 与你的代码集成](#3将SwanLab与你的代码集成)
 - [💻 自托管](#-自托管)
-    - [离线实验跟踪](#离线实验跟踪)
-    - [开启离线看板](#开启离线看板)
+  - [离线实验跟踪](#离线实验跟踪)
+  - [开启离线看板](#开启离线看板)
 - [🚗 框架集成](#-框架集成)
 - [🆚 与熟悉的工具的比较](#-与熟悉的工具的比较)
-    - [Tensorboard vs SwanLab](#tensorboard-vs-swanlab)
-    - [W&B vs SwanLab](#weights-and-biases-vs-swanlab)
+  - [Tensorboard vs SwanLab](#tensorboard-vs-swanlab)
+  - [W&B vs SwanLab](#weights-and-biases-vs-swanlab)
 - [👥 社区](#-社区)
-    - [社区与支持](#社区与支持)
-    - [SwanLab README徽章](#swanlab-readme徽章)
-    - [在论文中引用SwanLab](#在论文中引用swanlab)
-    - [为SwanLab做出贡献](#为swanlab做出贡献)
-    - [下载Icon](#下载icon)
+  - [社区与支持](#社区与支持)
+  - [SwanLab README 徽章](#swanlab-readme徽章)
+  - [在论文中引用 SwanLab](#在论文中引用swanlab)
+  - [为 SwanLab 做出贡献](#为swanlab做出贡献)
+  - [下载 Icon](#下载icon)
 - [📃 协议](#-协议)
 
 <br/>
 
 </details>
 
-## 👋🏻 什么是SwanLab
+## 👋🏻 什么是 SwanLab
 
-SwanLab是一款开源、轻量的AI实验跟踪工具，提供了一个跟踪、比较、和协作实验的平台。
+SwanLab 是一款开源、轻量的 AI 实验跟踪工具，提供了一个跟踪、比较、和协作实验的平台。
 
-SwanLab提供了友好的API和漂亮的界面，结合了超参数跟踪、指标记录、在线协作、实验链接分享等功能，让您可以快速跟踪AI实验、可视化过程、记录超参数，并分享给伙伴。
+SwanLab 提供了友好的 API 和漂亮的界面，结合了超参数跟踪、指标记录、在线协作、实验链接分享等功能，让您可以快速跟踪 AI 实验、可视化过程、记录超参数，并分享给伙伴。
 
 以下是其核心特性列表：
 
-**1. 📊实验指标与超参数跟踪**: 极简的代码嵌入您的机器学习pipeline，跟踪记录训练关键指标
+**1. 📊 实验指标与超参数跟踪**: 极简的代码嵌入您的机器学习 pipeline，跟踪记录训练关键指标
 
 - 自由的超参数与实验配置记录
 - 支持的元数据类型：标量指标、图像、音频、文本、...
 - 支持的图表类型：折线图、媒体图（图像、音频、文本）、...
-- 自动记录：控制台logging、GPU硬件、Git信息、Python解释器、Python库列表、代码目录
+- 自动记录：控制台 logging、GPU 硬件、Git 信息、Python 解释器、Python 库列表、代码目录
 
 ![](readme_files/overview-2.png)
 
+**2. ⚡️ 全面的框架集成**: PyTorch、Tensorflow、PyTorch Lightning、🤗HuggingFace、Transformers、MMEngine、Ultralytics、fastai、Tensorboard、OpenAI、ZhipuAI、Hydra、...
 
-**2. ⚡️全面的框架集成**: PyTorch、Tensorflow、PyTorch Lightning、🤗HuggingFace、Transformers、MMEngine、Ultralytics、fastai、Tensorboard、OpenAI、ZhipuAI、Hydra、...
+**3. 📦 组织实验**: 集中式仪表板，快速管理多个项目与实验，通过整体视图速览训练全局
 
-**3. 📦组织实验**: 集中式仪表板，快速管理多个项目与实验，通过整体视图速览训练全局
+**4. 🆚 比较结果**: 通过在线表格与对比图表比较不同实验的超参数和结果，挖掘迭代灵感
 
-**4. 🆚比较结果**: 通过在线表格与对比图表比较不同实验的超参数和结果，挖掘迭代灵感
+**5. 👥 在线协作**: 您可以与团队进行协作式训练，支持将实验实时同步在一个项目下，您可以在线查看团队的训练记录，基于结果发表看法与建议
 
-**5. 👥在线协作**: 您可以与团队进行协作式训练，支持将实验实时同步在一个项目下，您可以在线查看团队的训练记录，基于结果发表看法与建议
+**6. ✉️ 分享结果**: 复制和发送持久的 URL 来共享每个实验，方便地发送给伙伴，或嵌入到在线笔记中
 
-**6. ✉️分享结果**: 复制和发送持久的URL来共享每个实验，方便地发送给伙伴，或嵌入到在线笔记中
-
-**7. 💻支持自托管**: 支持不联网使用，自托管的社区版同样可以查看仪表盘与管理实验
+**7. 💻 支持自托管**: 支持不联网使用，自托管的社区版同样可以查看仪表盘与管理实验
 
 > \[!IMPORTANT]
 >
-> **收藏项目**，你将从 GitHub 上无延迟地接收所有发布通知～⭐️
+> **收藏项目**，你将从 GitHub 上无延迟地接收所有发布通知～ ⭐️
 
 ![star-us](readme_files/star-us.png)
 
@@ -92,17 +91,17 @@ SwanLab提供了友好的API和漂亮的界面，结合了超参数跟踪、指
 
 ## 📃 在线演示
 
-来看看SwanLab的在线演示：
+来看看 SwanLab 的在线演示：
 
-| [ResNet50猫狗分类](https://swanlab.cn/@ZeyiLin/Cats_Dogs_Classification/runs/jzo93k112f15pmx14vtxf/chart) | [Yolov8-COCO128目标检测](https://swanlab.cn/@ZeyiLin/ultratest/runs/yux7vclmsmmsar9ear7u5/chart)|
-|:---:|:---:|
+|                    [ResNet50 猫狗分类](https://swanlab.cn/@ZeyiLin/Cats_Dogs_Classification/runs/jzo93k112f15pmx14vtxf/chart)                    |                [Yolov8-COCO128 目标检测](https://swanlab.cn/@ZeyiLin/ultratest/runs/yux7vclmsmmsar9ear7u5/chart)                 |
+| :----------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------: |
 | <a href="https://swanlab.cn/@ZeyiLin/Cats_Dogs_Classification/runs/jzo93k112f15pmx14vtxf/chart"> <img src="readme_files/example-mnist.png"> </a> | <a href="https://swanlab.cn/@ZeyiLin/ultratest/runs/yux7vclmsmmsar9ear7u5/chart"> <img src="readme_files/example-yolo.png"> </a> |
-| 跟踪一个简单的ResNet50模型在猫狗数据集上训练的图像分类任务。 | 使用Yolov8在COCO128数据集上进行目标检测任务，跟踪训练超参数和指标。 |
+|                                          跟踪一个简单的 ResNet50 模型在猫狗数据集上训练的图像分类任务。                                          |                             使用 Yolov8 在 COCO128 数据集上进行目标检测任务，跟踪训练超参数和指标。                              |
 
-| [Qwen2指令微调](https://swanlab.cn/@ZeyiLin/Qwen2-fintune/runs/cfg5f8dzkp6vouxzaxlx6/chart)| [LSTM Google股票预测](https://swanlab.cn/@ZeyiLin/Google-Stock-Prediction/charts) |
-|:---:|:---:|
+|                     [Qwen2 指令微调](https://swanlab.cn/@ZeyiLin/Qwen2-fintune/runs/cfg5f8dzkp6vouxzaxlx6/chart)                      |                  [LSTM Google 股票预测](https://swanlab.cn/@ZeyiLin/Google-Stock-Prediction/charts)                  |
+| :-----------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------: |
 | <a href="https://swanlab.cn/@ZeyiLin/Qwen2-fintune/runs/cfg5f8dzkp6vouxzaxlx6/chart"> <img src="readme_files/example-qwen2.png"> </a> | <a href="https://swanlab.cn/@ZeyiLin/Google-Stock-Prediction/charts"> <img src="readme_files/example-lstm.png"> </a> |
-| 跟踪Qwen2大语言模型的指令微调训练，完成简单的指令遵循。 | 使用简单的LSTM模型在Google股价数据集上训练，实现对未来股价的预测。 |
+|                                       跟踪 Qwen2 大语言模型的指令微调训练，完成简单的指令遵循。                                       |                        使用简单的 LSTM 模型在 Google 股价数据集上训练，实现对未来股价的预测。                        |
 
 [更多案例](https://docs.swanlab.cn/zh/examples/mnist.html)
 
@@ -116,11 +115,11 @@ SwanLab提供了友好的API和漂亮的界面，结合了超参数跟踪、指
 pip install swanlab
 ```
 
-### 2.登录并获取API Key
+### 2.登录并获取 API Key
 
 1. 免费[注册账号](https://swanlab.cn)
 
-2. 登录账号，在用户设置 > [API Key](https://swanlab.cn/settings) 里复制您的API Key
+2. 登录账号，在用户设置 > [API Key](https://swanlab.cn/settings) 里复制您的 API Key
 
 3. 打开终端，输入：
 
@@ -128,9 +127,9 @@ pip install swanlab
 swanlab login
 ```
 
-出现提示时，输入您的API Key，按下回车，完成登陆。
+出现提示时，输入您的 API Key，按下回车，完成登陆。
 
-### 3.将SwanLab与你的代码集成
+### 3.将 SwanLab 与你的代码集成
 
 ```python
 import swanlab
@@ -146,21 +145,19 @@ for i in range(10):
     swanlab.log({"loss": i, "acc": i})
 ```
 
-大功告成！前往[SwanLab](https://swanlab.cn)查看你的第一个SwanLab实验。
+大功告成！前往[SwanLab](https://swanlab.cn)查看你的第一个 SwanLab 实验。
 
 ![MNIST](readme_files/readme-mnist.png)
 
 <br>
 
-
-
 ## 💻 自托管
 
-自托管社区版支持离线查看SwanLab仪表盘。
+自托管社区版支持离线查看 SwanLab 仪表盘。
 
 ### 离线实验跟踪
 
-在swanlab.init中设置`logir`和`mode`这两个参数，即可离线跟踪实验：
+在 swanlab.init 中设置`logir`和`mode`这两个参数，即可离线跟踪实验：
 
 ```python
 ...
@@ -175,21 +172,21 @@ swanlab.init(
 
 - 参数`mode`设置为`local`，关闭将实验同步到云端
 
-- 参数`logdir`的设置是可选的，它的作用是指定了SwanLab日志文件的保存位置（默认保存在`swanlog`文件夹下）
+- 参数`logdir`的设置是可选的，它的作用是指定了 SwanLab 日志文件的保存位置（默认保存在`swanlog`文件夹下）
 
-    - 日志文件会在跟踪实验的过程中被创建和更新，离线看板的启动也将基于这些日志文件
+  - 日志文件会在跟踪实验的过程中被创建和更新，离线看板的启动也将基于这些日志文件
 
 其他部分和云端使用完全一致。
 
 ### 开启离线看板
 
-打开终端，使用下面的指令，开启一个SwanLab仪表板:
+打开终端，使用下面的指令，开启一个 SwanLab 仪表板:
 
 ```bash
-swanlab watch -l ./logs
+swanlab watch ./logs
 ```
 
-运行完成后，SwanLab会给你1个本地的URL链接（默认是[http://127.0.0.1:5092](http://127.0.0.1:5092)）
+运行完成后，SwanLab 会给你 1 个本地的 URL 链接（默认是[http://127.0.0.1:5092](http://127.0.0.1:5092)）
 
 访问该链接，就可以在浏览器用离线看板查看实验了。
 
@@ -197,7 +194,7 @@ swanlab watch -l ./logs
 
 ## 🚗 框架集成
 
-将您最喜欢的框架与SwanLab结合使用，[更多集成](https://docs.swanlab.cn/zh/guide_cloud/integration/integration-pytorch-lightning.html)。
+将您最喜欢的框架与 SwanLab 结合使用，[更多集成](https://docs.swanlab.cn/zh/guide_cloud/integration/integration-pytorch-lightning.html)。
 
 <details>
   <summary>
@@ -205,7 +202,7 @@ swanlab watch -l ./logs
   </summary>
   <br>
 
-使用`SwanLabLogger`创建示例，并代入`Trainer`的`logger`参数中，即可实现SwanLab记录训练指标。
+使用`SwanLabLogger`创建示例，并代入`Trainer`的`logger`参数中，即可实现 SwanLab 记录训练指标。
 
 ```python
 from swanlab.integration.pytorch_lightning import SwanLabLogger
@@ -289,7 +286,7 @@ trainer.test(dataloaders=test_loader)
 
 <br>
 
-使用`SwanLabCallback`创建示例，并代入`Trainer`的`callbacks`参数中，即可实现SwanLab记录训练指标。
+使用`SwanLabCallback`创建示例，并代入`Trainer`的`callbacks`参数中，即可实现 SwanLab 记录训练指标。
 
 ```python
 import evaluate
@@ -352,9 +349,9 @@ trainer.train()
 </summary>
 <br>
 
-将SwanLab专为MMEngine设计的`SwanlabVisBackend`集成到MMEngine中，即可实现SwanLab自动记录训练指标。
+将 SwanLab 专为 MMEngine 设计的`SwanlabVisBackend`集成到 MMEngine 中，即可实现 SwanLab 自动记录训练指标。
 
-在你的MM配置文件中，加入下面的代码片段，开始训练即可。
+在你的 MM 配置文件中，加入下面的代码片段，开始训练即可。
 
 ```python
 custom_imports = dict(imports=["swanlab.integration.mmengine"], allow_failed_imports=False)
@@ -383,7 +380,7 @@ visualizer = dict(
 </summary>
 <br>
 
-将SwanLab集成到Ultralytics中非常简单，只需要用`add_swanlab_callback`函数即可实现:
+将 SwanLab 集成到 Ultralytics 中非常简单，只需要用`add_swanlab_callback`函数即可实现:
 
 ```python
 from ultralytics import YOLO
@@ -397,7 +394,7 @@ add_swanlab_callback(model)
 
 model.train(
     data="./coco.yaml",
-    epochs=50, 
+    epochs=50,
     imgsz=320,
 )
 ```
@@ -410,23 +407,23 @@ model.train(
 
 ### Tensorboard vs SwanLab
 
-- **☁️支持在线使用**：
-  通过SwanLab可以方便地将训练实验在云端在线同步与保存，便于远程查看训练进展、管理历史项目、分享实验链接、发送实时消息通知、多端看实验等。而Tensorboard是一个离线的实验跟踪工具。
+- **☁️ 支持在线使用**：
+  通过 SwanLab 可以方便地将训练实验在云端在线同步与保存，便于远程查看训练进展、管理历史项目、分享实验链接、发送实时消息通知、多端看实验等。而 Tensorboard 是一个离线的实验跟踪工具。
 
-- **👥多人协作**：
-  在进行多人、跨团队的机器学习协作时，通过SwanLab可以轻松管理多人的训练项目、分享实验链接、跨空间交流讨论。而Tensorboard主要为个人设计，难以进行多人协作和分享实验。
+- **👥 多人协作**：
+  在进行多人、跨团队的机器学习协作时，通过 SwanLab 可以轻松管理多人的训练项目、分享实验链接、跨空间交流讨论。而 Tensorboard 主要为个人设计，难以进行多人协作和分享实验。
 
-- **💻持久、集中的仪表板**：
-  无论你在何处训练模型，无论是在本地计算机上、在实验室集群还是在公有云的GPU实例中，你的结果都会记录到同一个集中式仪表板中。而使用TensorBoard需要花费时间从不同的机器复制和管理
-  TFEvent文件。
+- **💻 持久、集中的仪表板**：
+  无论你在何处训练模型，无论是在本地计算机上、在实验室集群还是在公有云的 GPU 实例中，你的结果都会记录到同一个集中式仪表板中。而使用 TensorBoard 需要花费时间从不同的机器复制和管理
+  TFEvent 文件。
 
-- **💪更强大的表格**：
-  通过SwanLab表格可以查看、搜索、过滤来自不同实验的结果，可以轻松查看数千个模型版本并找到适合不同任务的最佳性能模型。
+- **💪 更强大的表格**：
+  通过 SwanLab 表格可以查看、搜索、过滤来自不同实验的结果，可以轻松查看数千个模型版本并找到适合不同任务的最佳性能模型。
   TensorBoard 不适用于大型项目。
 
 ### Weights and Biases vs SwanLab
 
-- Weights and Biases 是一个必须联网使用的闭源MLOps平台
+- Weights and Biases 是一个必须联网使用的闭源 MLOps 平台
 
 - SwanLab 不仅支持联网使用，也支持开源、免费、自托管的版本
 
@@ -436,13 +433,13 @@ model.train(
 
 ### 社区与支持
 
-- [GitHub Issues](https://github.com/SwanHubX/SwanLab/issues)：使用SwanLab时遇到的错误和问题
-- [电子邮件支持](zeyi.lin@swanhub.co)：反馈关于使用SwanLab的问题
-- <a href="https://geektechstudio.feishu.cn/wiki/NIZ9wp5LRiSqQykizbGcVzUKnic">微信交流群</a>：交流使用SwanLab的问题、分享最新的AI技术
+- [GitHub Issues](https://github.com/SwanHubX/SwanLab/issues)：使用 SwanLab 时遇到的错误和问题
+- [电子邮件支持](zeyi.lin@swanhub.co)：反馈关于使用 SwanLab 的问题
+- <a href="https://geektechstudio.feishu.cn/wiki/NIZ9wp5LRiSqQykizbGcVzUKnic">微信交流群</a>：交流使用 SwanLab 的问题、分享最新的 AI 技术
 
-### SwanLab README徽章
+### SwanLab README 徽章
 
-如果你喜欢在工作中使用 SwanLab，请将 SwanLab 徽章添加到你的README中：
+如果你喜欢在工作中使用 SwanLab，请将 SwanLab 徽章添加到你的 README 中：
 
 [![swanlab](https://img.shields.io/badge/powered%20by-SwanLab-438440)](https://github.com/swanhubx/swanlab)
 
@@ -450,7 +447,7 @@ model.train(
 [![swanlab](https://img.shields.io/badge/powered%20by-SwanLab-438440)](https://github.com/swanhubx/swanlab)
 ```
 
-### 在论文中引用SwanLab
+### 在论文中引用 SwanLab
 
 如果您发现 SwanLab 对您的研究之旅有帮助，请考虑以下列格式引用：
 
@@ -465,13 +462,13 @@ model.train(
 }
 ```
 
-### 为SwanLab做出贡献
+### 为 SwanLab 做出贡献
 
-考虑为SwanLab做出贡献吗？首先，请花点时间阅读 [贡献指南](CONTRIBUTING.md)。
+考虑为 SwanLab 做出贡献吗？首先，请花点时间阅读 [贡献指南](CONTRIBUTING.md)。
 
-同时，我们非常欢迎通过社交媒体、活动和会议的分享来支持SwanLab，衷心感谢！
+同时，我们非常欢迎通过社交媒体、活动和会议的分享来支持 SwanLab，衷心感谢！
 
-### 下载Icon
+### 下载 Icon
 
 [SwanLab-Icon-SVG](readme_files/swanlab-logo.svg)
 
diff --git a/build_pypi.py b/build_pypi.py
index 3549d09c..107d6af6 100644
--- a/build_pypi.py
+++ b/build_pypi.py
@@ -11,12 +11,6 @@
 import shutil
 import os
 
-# 如果node_modules文件夹存在则不运行npm install
-if not os.path.exists("node_modules"):
-    # 安装依赖
-    subprocess.run("npm install", shell=True)
-# 构建node项目
-subprocess.run("npm run build.release", shell=True)
 # 如果dist文件夹存在则删除
 if os.path.exists("dist"):
     shutil.rmtree("dist")
diff --git a/jsconfig.json b/jsconfig.json
deleted file mode 100644
index 7ee3eea2..00000000
--- a/jsconfig.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "compilerOptions": {
-    "target": "ES6",
-    "module": "commonjs",
-    "allowSyntheticDefaultImports": true,
-    "baseUrl": "./",
-    "paths": {
-      "@swanlab-vue/*": ["vue/src/*"]
-    },
-    "resolveJsonModule": true
-  },
-  "exclude": ["node_modules", "dist", "vue/public"],
-  "include": ["vue/src/**/*.js", "vue/src/**/*.vue", "vue/auto-imports.d.ts"]
-}
diff --git a/package.json b/package.json
deleted file mode 100644
index 9341dd45..00000000
--- a/package.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
-  "name": "swanlab-ui",
-  "private": true,
-  "version": "0.0.0",
-  "type": "module",
-  "scripts": {
-    "dev": "vite",
-    "build": "vite build",
-    "build.release": "vite build --mode release",
-    "preview": "vite preview",
-    "fmt": "npx prettier --write .",
-    "prepare": "husky install",
-    "test": "vitest"
-  },
-  "dependencies": {
-    "@antv/g2plot": "^2.4.31",
-    "@headlessui/vue": "^1.7.22",
-    "axios": "^1.6.2",
-    "html2canvas": "^1.4.1",
-    "moment": "^2.29.4",
-    "pinia": "^2.1.7",
-    "sass": "^1.69.5",
-    "terser": "^5.31.0",
-    "tippy.js": "^6.3.7",
-    "vue": "^3.3.11",
-    "vue-i18n": "^9.8.0",
-    "vue-router": "^4.2.5",
-    "vue-tippy": "^6.3.1"
-  },
-  "devDependencies": {
-    "@vitejs/plugin-vue": "^4.5.2",
-    "autoprefixer": "^10.4.16",
-    "eslint": "^8.55.0",
-    "eslint-config-prettier": "^8.10.0",
-    "eslint-plugin-prettier": "^4.2.1",
-    "eslint-plugin-vue": "^9.19.2",
-    "husky": "^8.0.3",
-    "mockjs": "^1.1.0",
-    "postcss": "^8.4.32",
-    "prettier": "^2.8.8",
-    "tailwindcss": "^3.3.6",
-    "unplugin-auto-import": "^0.15.3",
-    "unplugin-vue-components": "^0.24.1",
-    "vite": "^5.0.6",
-    "vite-plugin-json5": "^1.1.2",
-    "vitest": "^1.6.0"
-  }
-}
diff --git a/postcss.config.cjs b/postcss.config.cjs
deleted file mode 100644
index 85f717cc..00000000
--- a/postcss.config.cjs
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
-  plugins: {
-    tailwindcss: {},
-    autoprefixer: {}
-  }
-}
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
deleted file mode 100644
index 40ce69d1..00000000
--- a/prisma/schema.prisma
+++ /dev/null
@@ -1,152 +0,0 @@
-generator client {
-  provider = "prisma-client-js"
-}
-
-datasource db {
-  provider = "sqlite"
-  url      = env("DATABASE_URL")
-}
-
-// 当前一个工程中只有一个项目
-model Project {
-  id          Int     @id @default(autoincrement())
-  name        String  @unique
-  description String?
-  sum         Int?
-  charts      Int     @default(0) // 是否已经生成项目级别图表，0 未生成，1 已生成
-  more        String? // json
-  version     String
-  create_time String
-  update_time String
-
-  Experiment Experiment[]
-  Namespace  Namespace[]
-  Chart      Chart[]
-
-  @@map("project")
-}
-
-model Experiment {
-  id          Int     @id @default(autoincrement())
-  run_id      String  @unique
-  name        String
-  description String?
-  sort        Int // >=0
-  status      Int     @default(0) // 实验状态 -1: crushed, 0: running, 1: finished
-  show        Int     @default(0) // 实验可见性 0: 不可见，1: 可见
-  light       String?
-  dark        String?
-  more        String? // json
-  version     String
-  create_time String
-  update_time String
-
-  project_id Int
-  Project    Project     @relation(fields: [project_id], references: [id])
-  Namespace  Namespace[]
-  Chart      Chart[]
-  Tag        Tag[]
-
-  @@unique([project_id, name])
-  @@unique([project_id, sort])
-  @@map("experiment")
-}
-
-// 一个实验/项目下可以有多个命名空间
-model Namespace {
-  id          Int     @id @default(autoincrement())
-  name        String
-  description String?
-  sort        Int // >=0, 索引越小，排序越靠前
-  more        String // json
-  create_time String
-  update_time String
-
-  // 一个命名空间只能属于项目或实验
-  project_id    Int?
-  experiment_id Int?
-  Project       Project?    @relation(fields: [project_id], references: [id])
-  Experiment    Experiment? @relation(fields: [experiment_id], references: [id])
-  Display       Display[]
-
-  @@unique([project_id, name])
-  @@unique([experiment_id, name])
-  @@map("namespace")
-}
-
-model Chart {
-  id          Int     @id @default(autoincrement())
-  name        String
-  description String?
-  system      Int     @default(1) // 是否为创建tag时自动生成的图表, -1: 删除的自动生成的图表; 0: 否; 1: 是; 系统图表不可删除，只能改为-1
-  type        String // 由创建者决定
-  reference   String  @default("step") // 由创建者决定
-  config      String? // json
-  more        String? // json
-  create_time String
-  update_time String
-
-  project_id    Int?
-  experiment_id Int?
-  Project       Project?    @relation(fields: [project_id], references: [id])
-  Experiment    Experiment? @relation(fields: [experiment_id], references: [id])
-  Display       Display[]
-  Source        Source[]
-
-  @@unique([project_id, name])
-  @@unique([experiment_id, name])
-  @@map("chart")
-}
-
-// 中间表，namespace 和 chart，设计排序和排序可变
-model Display {
-  id          Int     @id @default(autoincrement())
-  sort        Int // 索引越小，排序越靠前，索引>=0
-  more        String? // json
-  create_time String
-  update_time String
-
-  namespace_id Int
-  chart_id     Int
-  Namespace    Namespace @relation(fields: [namespace_id], references: [id])
-  Chart        Chart     @relation(fields: [chart_id], references: [id])
-
-  @@unique([namespace_id, chart_id])
-  @@map("display")
-}
-
-model Source {
-  id          Int     @id @default(autoincrement())
-  sort        Int     @default(0) // 值越小越靠前
-  error       String? // json
-  more        String? // json
-  create_time String
-  update_time String
-
-  chart_id Int
-  Chart    Chart @relation(fields: [chart_id], references: [id])
-  tag_id   Int
-  Tag      Tag   @relation(fields: [tag_id], references: [id])
-
-  @@unique([chart_id, tag_id])
-  @@map("source")
-}
-
-// 与实验关联
-model Tag {
-  id          Int     @id @default(autoincrement())
-  name        String  @unique
-  type        String
-  description String?
-  system      Int     @default(0) // 0: 用户生成，1: 系统生成
-  more        String? // json
-  create_time String
-  update_time String
-
-  experiment_id Int
-  Experiment    Experiment @relation(fields: [experiment_id], references: [id])
-  Source        Source[]
-
-  @@unique([experiment_id, name])
-  @@map("tag")
-}
diff --git a/pyproject.toml b/pyproject.toml
index fbb44137..42e3261a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "swanlab" # 项目名称
-dynamic = ["version", "dependencies", "optional-dependencies", "readme"] # 动态配置版本，依赖，可选依赖，readme
+dynamic = ["version", "dependencies", "readme"] # 动态配置版本，依赖，可选依赖，readme
 description = "Python library for streamlined tracking and management of AI training processes." # 项目描述
 license = "Apache-2.0" # 项目许可证
 requires-python = ">=3.8" # python版本要求，我们只维护python3.8以上版本
@@ -18,7 +18,7 @@ authors = [# 项目作者
     { name = "Cunyue", email = "team@swanhub.co" },
     { name = "Feudalman", email = "team@swanhub.co" },
     { name = "ZeYi Lin", email = "team@swanhub.co" },
-    { name = "KashiwaByte", email = "team@swanhub.co" }
+    { name = "KashiwaByte", email = "team@swanhub.co" },
 ]
 keywords = [# 项目关键词
     "machine learning",
@@ -53,12 +53,9 @@ swanlab = "swanlab.cli:cli" # 项目命令行工具(可添加多个)
 path = "swanlab/package.json"  # package.json文件，从中获取版本号
 pattern = ".*\"version\":\\s*\"(?P<version>[^\"]+)\""
 
-[tool.hatch.metadata.hooks.requirements_txt] # 动态设置依赖
+[tool.hatch.metadata.hooks.requirements_txt]
 filename = "requirements.txt"
 
-[tool.hatch.metadata.hooks.requirements_txt.optional-dependencies]  # 动态设置可选依赖
-oauth = ["requirements-cloud.txt"]  # 连接swanlab云端的可选依赖
-
 [tool.hatch.metadata.hooks.fancy-pypi-readme]  # 动态设置readme
 content-type = "text/markdown"
 fragments = [{ path = "README.md" }]
@@ -78,14 +75,11 @@ replacement = "https://github.com/SwanHubX/SwanLab/blob/main/README.md"
 
 
 [tool.hatch.build]
-artifacts = [# 将一些非python文件打包到项目中
+artifacts = [
     "*.json",
     "*.pyi", # 类型提示文件
 ]
 
-# [tool.hatch.build.targets.wheel.hooks.custom]  # 构建wheel时，用python执行一些脚本
-# path = ".config/copy_frontend.py"  # TODO 拷贝前端文件的脚本
-
 
 [tool.hatch.build.targets.sdist]  # 在执行构建之前，将一些必要文件拷贝到虚拟环境中，以便构建，此时已经完成了vue的编译
 include = [
@@ -94,53 +88,19 @@ include = [
     "/README.md", # 包含readme
     "/requirements.txt", # 包含依赖
     "/requirements-swan.txt", # 包含可选依赖
-    "/.config/copy_frontend.py", # 包含拷贝前端文件的脚本
-    "/package.json", # 包含前端的package.json文件
 ]
 
 [tool.hatch.build.targets.wheel]
 packages = ["swanlab"]  # 指定项目包名
 
-[tool.pyright]  # 静态类型检查
+[tool.pyright] # 静态类型检查，暂时没有配置
 include = ["swanlab/**/*.py"]  # 检查的文件
 exclude = []  # 排除的文件
 
-[tool.ruff]  # 代码风格检查
+[tool.ruff]  # 代码风格检查，暂时没有配置
 target-version = "py37"
 extend-select = ["B", "C", "I", "N", "SIM", "UP"]
-ignore = [
-    "C901", # function is too complex (TODO: un-ignore this)
-    "B023", # function definition in loop (TODO: un-ignore this)
-    "B008", # function call in argument defaults
-    "B017", # pytest.raises considered evil
-    "B028", # explicit stacklevel for warnings
-    "E501", # from scripts/lint_backend.sh
-    "SIM105", # contextlib.suppress (has a performance cost)
-    "SIM117", # multiple nested with blocks (doesn't look good with gr.Row etc)
-    "UP007", # use X | Y for type annotations (TODO: can be enabled once Pydantic plays nice with them)
-    "UP006", # use `list` instead of `List` for type annotations (fails for 3.8)
-]
-exclude = []
-
-# [tool.ruff.per-file-ignores]  # 指定某些文件的忽略规则，暂时没什么用
-# "目录/*" = [
-#   "E402", # Demos may have imports not at the top
-#   "E741", # Demos may have ambiguous variable names
-#   "F405", # Demos may use star imports
-#   "I",    # Don't care about import order
-# ]
-# "目录/__init__.py" = [
-#   "F401", # "Imported but unused" (TODO: it would be better to be explicit and use __all__)
-# ]
-# "目录/routes.py" = [
-#   "UP006", # Pydantic on Python 3.7 requires old-style type annotations (TODO: drop when Python 3.7 is dropped)
-# ]
-# "目录/cli/commands/files/NoTemplateComponent.py" = ["ALL"]
-
-# 可以在此处指定一些环境变量
-[tool.pytest.ini_options]
-
-
+ignore = []
 
 
 # black配置，与构建无关
diff --git a/requirements-cloud.txt b/requirements-cloud.txt
deleted file mode 100644
index e69de29b..00000000
diff --git a/requirements.txt b/requirements.txt
index e055f4c9..ba36c6e1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-# dat process
+# data process
 soundfile
 pillow
 matplotlib
@@ -15,7 +15,7 @@ click
 # database
 ujson
 pyyaml
-peewee
+swanboard==0.1.2b3
 
 
 # Information collection
diff --git a/script/transfer_logfile_0.1.4.py b/script/transfer_logfile_0.1.4.py
index b30a3d6d..79e11e70 100644
--- a/script/transfer_logfile_0.1.4.py
+++ b/script/transfer_logfile_0.1.4.py
@@ -12,7 +12,7 @@
 """
 import os
 from swanlab.env import ROOT
-from swanlab.db import connect, Experiment, Project, Chart, Tag, Source, Namespace, Display
+from swanboard.db import connect, Experiment, Project, Chart, Tag, Source, Namespace, Display
 import json
 import yaml
 import argparse
diff --git a/swanlab/__init__.py b/swanlab/__init__.py
index 784674d6..099406cd 100755
--- a/swanlab/__init__.py
+++ b/swanlab/__init__.py
@@ -13,9 +13,7 @@
     get_config,
 )
 
-
 from .data.run.main import config
 from .package import get_package_version
 
-
 __version__ = get_package_version()
diff --git a/swanlab/cli/commands/__init__.py b/swanlab/cli/commands/__init__.py
new file mode 100644
index 00000000..ec3909d1
--- /dev/null
+++ b/swanlab/cli/commands/__init__.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/12 21:17
+@File: __init__.py
+@IDE: pycharm
+@Description:
+    暴露子命令
+"""
+from .auth import login, logout
+from .dashboard import watch
+from .converter import convert
diff --git a/swanlab/cli/commands/auth/__init__.py b/swanlab/cli/commands/auth/__init__.py
new file mode 100644
index 00000000..f6a4fc21
--- /dev/null
+++ b/swanlab/cli/commands/auth/__init__.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/12 21:10
+@File: __init__.py
+@IDE: pycharm
+@Description:
+    cli认证模块
+"""
+from .login import login
+from .logout import logout
diff --git a/swanlab/cli/commands/auth/login.py b/swanlab/cli/commands/auth/login.py
new file mode 100644
index 00000000..8d625f7c
--- /dev/null
+++ b/swanlab/cli/commands/auth/login.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/12 21:11
+@File: login.py
+@IDE: pycharm
+@Description:
+    登录模块
+"""
+import click
+from swanlab.package import is_login
+from swanlab.api.auth import terminal_login
+from swanlab.utils import FONT
+
+
+@click.command()
+@click.option(
+    "--relogin", "-r",
+    is_flag=True,
+    default=False,
+    help="Relogin to the swanlab cloud, it will recover the token file.",
+)
+@click.option(
+    "--api-key", "-k",
+    default=None,
+    type=str,
+    help="If you prefer not to engage in commands-line interaction to input the api key, "
+         "this will allow automatic login.",
+)
+def login(api_key: str, relogin: bool):
+    """Login to the swanlab cloud."""
+    if not relogin and is_login():
+        # 此时代表token已经获取，需要打印一条信息：已经登录
+        command = FONT.bold("swanlab login --relogin")
+        tip = FONT.swanlab("You are already logged in. Use `" + command + "` to force relogin.")
+        return print(tip)
+    # 进行登录，此时将直接覆盖本地token文件
+    login_info = terminal_login(api_key)
+    print(FONT.swanlab("Login successfully. Hi, " + FONT.bold(FONT.default(login_info.username))) + "!")
diff --git a/swanlab/cli/commands/auth/logout.py b/swanlab/cli/commands/auth/logout.py
new file mode 100644
index 00000000..45e73906
--- /dev/null
+++ b/swanlab/cli/commands/auth/logout.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/12 21:11
+@File: logout.py
+@IDE: pycharm
+@Description:
+    登出模块
+"""
+import click
+from swanlab.package import is_login
+from swanlab.utils import FONT
+from swanlab.env import get_swanlab_folder
+import shutil
+import sys
+
+
+@click.command()
+def logout():
+    """Logout to the swanlab cloud."""
+    command = FONT.bold("swanlab login")
+    if is_login():
+        # 如果已经是登录状态，那么则询问用户是否确认，如果确认则删除token文件夹
+        confirm = input(FONT.swanlab("Are you sure you want to logout? (y/N): "))
+        if confirm.lower() == "y":
+            try:
+                shutil.rmtree(get_swanlab_folder())
+                return print(FONT.swanlab("Logout successfully. You can use `" + command + "` to login again."))
+            except Exception as e:
+                return print(FONT.swanlab("Logout failed. Reason:" + str(e)))
+        else:
+            return print(FONT.swanlab("Logout canceled."))
+    # 如果还未登录，则不做任何处理，并告知用户如何登录
+    tip = FONT.swanlab("You are not logged in. If you want to login in, please use `" + command + "` to login.")
+    print(tip)
+    return sys.exit(1)
diff --git a/swanlab/cli/commands/converter/__init__.py b/swanlab/cli/commands/converter/__init__.py
new file mode 100644
index 00000000..bcde74cd
--- /dev/null
+++ b/swanlab/cli/commands/converter/__init__.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/14 00:51
+@File: __init__.py.py
+@IDE: pycharm
+@Description:
+    转换命令，来自`swanlab.converter`，兼容其他可视化工具，转换为swanlab格式
+    由于`swanlab.converter`可以被用户调用，所以转换代码作为一个模块封装，这里只是调用 `swanlab.converter` 的代码
+"""
+
+import click
+
+
+@click.command()
+@click.option(
+    "--type",
+    "-t",
+    default="tensorboard",
+    type=click.Choice(["tensorboard", "wandb"]),
+    help="The type of the experiment tracking tool you want to convert to.",
+)
+@click.option(
+    "--project",
+    "-p",
+    default=None,
+    type=str,
+    help="SwanLab project name.",
+)
+@click.option(
+    "--workspace",
+    "-w",
+    default=None,
+    type=str,
+    help="swanlab.init workspace parameter.",
+)
+@click.option(
+    "--cloud",
+    default=True,
+    type=bool,
+    help="swanlab.init cloud parameter.",
+)
+@click.option(
+    "--logdir",
+    "-l",
+    type=str,
+    help="The directory where the swanlab log files are stored.",
+)
+@click.option(
+    "--tb_logdir",
+    type=str,
+    help="The directory where the tensorboard log files are stored.",
+)
+@click.option(
+    "--wb-project",
+    type=str,
+    help="The project name of the wandb runs.",
+)
+@click.option(
+    "--wb-entity",
+    type=str,
+    help="The entity name of the wandb runs.",
+)
+@click.option(
+    "--wb-runid",
+    type=str,
+    help="The run_id of the wandb run.",
+)
+def convert(
+        type: str,
+        project: str,
+        cloud: bool,
+        workspace: str,
+        logdir: str,
+        tb_logdir: str,
+        wb_project: str,
+        wb_entity: str,
+        wb_runid: str,
+        **kwargs,
+):
+    """Convert the log files of other experiment tracking tools to SwanLab."""
+    if type == "tensorboard":
+        from swanlab.converter.tfb import TFBConverter
+
+        tfb_converter = TFBConverter(
+            convert_dir=tb_logdir,
+            project=project,
+            workspace=workspace,
+            cloud=cloud,
+            logdir=logdir,
+        )
+        tfb_converter.run()
+
+    elif type == "wandb":
+        from swanlab.converter.wb import WandbConverter
+
+        print(wb_project, wb_entity, wb_runid)
+
+        wb_converter = WandbConverter(
+            project=project,
+            workspace=workspace,
+            cloud=cloud,
+            logdir=logdir,
+        )
+        wb_converter.run(
+            wb_project=wb_project,
+            wb_entity=wb_entity,
+            wb_run_id=wb_runid,
+        )
+
+    else:
+        raise ValueError("The type of the experiment tracking tool you want to convert to is not supported.")
diff --git a/swanlab/cli/commands/dashboard/__init__.py b/swanlab/cli/commands/dashboard/__init__.py
new file mode 100644
index 00000000..6fa88656
--- /dev/null
+++ b/swanlab/cli/commands/dashboard/__init__.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/13 01:36
+@File: __init__.py
+@IDE: pycharm
+@Description:
+    dashboard模块，依赖于swanboard
+"""
+from .watch import watch
diff --git a/swanlab/cli/commands/dashboard/watch.py b/swanlab/cli/commands/dashboard/watch.py
new file mode 100644
index 00000000..81d66f2c
--- /dev/null
+++ b/swanlab/cli/commands/dashboard/watch.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/13 01:35
+@File: watch.py
+@IDE: pycharm
+@Description:
+    watch命令
+"""
+from swanlab.env import get_swanlog_dir, ROOT, PORT, HOST
+from swanlab.log import swanlog
+from swanboard import SwanBoardRun
+import click
+import os
+import sys
+
+
+@click.command()
+@click.argument(
+    "path",
+    envvar=ROOT,
+    type=click.Path(
+        exists=True,
+        dir_okay=True,
+        file_okay=False,
+        resolve_path=True,
+        readable=True,
+    ),
+    nargs=1,
+    required=False,
+)
+@click.option(
+    "--host",
+    "-h",
+    default=lambda: os.environ.get(HOST, "127.0.0.1"),
+    type=str,
+    nargs=1,
+    help="The host of swanlab web, default by 127.0.0.1",
+)
+@click.option(
+    "--port",
+    "-p",
+    default=lambda: os.environ.get(PORT, 5092),
+    nargs=1,
+    type=click.IntRange(1, 65535),
+    help="The port of swanlab web, default by 5092",
+)
+@click.option(
+    "--logdir",
+    "-l",
+    default=lambda: os.environ.get(ROOT, None),
+    nargs=1,
+    type=click.Path(
+        exists=True,
+        dir_okay=True,
+        file_okay=False,
+        resolve_path=True,
+        readable=True,
+    ),
+    help="Specify the folder to store Swanlog, which is by default the folder where Swanlab Watch is run."
+         "The option will be deprecated in the future, you can just use `swanlab watch <LOG PATH>` to specify the path."
+)
+@click.option(
+    "--log-level",
+    default="info",
+    nargs=1,
+    type=click.Choice(["debug", "info", "warning", "error", "critical"]),
+    help="The level of log, default by info; You can choose one of [debug, info, warning, error, critical]",
+)
+def watch(path: str, host: str, port: int, logdir: str, log_level: str):
+    """
+    Run this commands to turn on the swanlab service.
+    """
+    swanlog.set_level(log_level)
+    # ----- 校验path，path如果被输入，已经由上层校验已存在，可读，是一个文件夹 -----
+    if logdir is not None:
+        swanlog.warning(
+            "The option `--logdir` will be deprecated in the future, "
+            f"you can just use `swanlab watch [PATH]` to specify the path."
+        )
+    # logdir 覆盖 path，接下来统一处理path而不再管logdir
+    path = logdir if logdir is not None else path
+    if path is not None:
+        path = os.path.abspath(path)
+        os.environ[ROOT] = path
+    # 为None时从环境变量中获取
+    try:
+        path = get_swanlog_dir()
+    except ValueError as e:
+        click.BadParameter(str(e))
+        return sys.exit(3)
+    except NotADirectoryError:
+        click.BadParameter("SWANLAB_LOG_DIR must be a directory")
+        return sys.exit(4)
+    except FileNotFoundError:
+        click.BadParameter(f"The log folder `{path}` was not found")
+        return sys.exit(5)
+    # ----- 校验host和port -----
+    try:
+        SwanBoardRun.is_valid_port(port)
+        SwanBoardRun.is_valid_ip(host)
+    except ValueError as e:
+        click.BadParameter(str(e))
+        return sys.exit(6)
+    # ---- 启动服务 ----
+    SwanBoardRun.run(
+        path=path,
+        host=host,
+        port=port,
+    )
diff --git a/swanlab/cli/main.py b/swanlab/cli/main.py
index 6561913e..3806a4cc 100644
--- a/swanlab/cli/main.py
+++ b/swanlab/cli/main.py
@@ -7,16 +7,9 @@
 @Description:
     swanlab脚本命令的主入口
 """
-
+from swanlab.package import get_package_version
+import swanlab.cli.commands as C
 import click
-from .utils import is_valid_ip, is_valid_port, is_valid_root_dir, URL
-from swanlab.package import version_limit, get_package_version, is_login
-from swanlab.api.auth import terminal_login
-from ..env import get_server_host, get_server_port, get_swanlog_dir, get_swanlab_folder
-from ..db import connect
-from ..utils import FONT
-import time
-import shutil
 
 
 @click.group(invoke_without_command=True)
@@ -25,249 +18,19 @@ def cli():
     pass
 
 
-# ---------------------------------- watch命令，开启本地后端服务 ----------------------------------
-
-
-@cli.command()
-# 控制服务发布的ip地址
-@click.option(
-    "--host",
-    "-h",
-    default=None,
-    type=str,
-    help="The host of swanlab web, default by 127.0.0.1",
-    callback=is_valid_ip,
-)
-# 控制服务发布的端口，默认5092
-@click.option(
-    "--port",
-    "-p",
-    default=None,
-    type=int,
-    help="The port of swanlab web, default by 5092",
-    callback=is_valid_port,
-)
-# 实验文件夹
-@click.option(
-    "--logdir",
-    "-l",
-    default=None,
-    type=str,
-    help="Specify the folder to store Swanlog, which is by default the folder where Swanlab Watch is run.",
-    callback=is_valid_root_dir,
-)
-# 日志等级
-@click.option(
-    "--log-level",
-    default="info",
-    type=click.Choice(["debug", "info", "warning", "error", "critical"]),
-    help="The level of log, default by info; You can choose one of [debug, info, warning, error, critical]",
-)
-def watch(log_level: str, **kwargs):
-    """Run this command to turn on the swanlab service."""
-    # 本质上此模块用于注入环境变量并启动服务
-    # 这里采用的是动态导入，因为路径的检查交由上面的is_valid_root_dir回调函数完成
-    start = time.time()
-    # 导入必要的模块
-    from ..log import swanlog as swl
-    from ..server import app
-    import uvicorn
-
-    log_dir = get_swanlog_dir()
-    version_limit(log_dir, mode="watch")
-
-    # debug一下当前日志文件夹的位置
-    swl.debug("Try to explore the swanlab experiment logs in: " + FONT.bold(log_dir))
-    try:
-        connect()
-    except FileNotFoundError:
-        swl.error("Can not find the swanlab db in: " + FONT.bold(log_dir))
-    # ---------------------------------- 日志等级处理 ----------------------------------
-    swl.set_level(log_level)
-    # ---------------------------------- 服务地址处理 ----------------------------------
-    # 当前服务地址
-    host = get_server_host()
-    # 当前服务端口
-    port = get_server_port()
-    # 所有可用ip
-    ipv4 = URL.get_all_ip()
-    # ---------------------------------- 日志打印 ----------------------------------
-    # 耗时
-    take_time = int((time.time() - start) * 1000).__str__() + "ms\n\n"
-    # 可用URL
-    if URL.is_zero_ip(host):
-        tip = "\n".join([URL(i, port).__str__() for i in ipv4])
-    else:
-        tip = URL(host, port).__str__()
-    tip = tip + "\n" + URL.last_tip() + "\n"
-    v = FONT.bold("v" + get_package_version())
-    swl.info(f"SwanLab Experiment Dashboard " + v + " ready in " + FONT.bold(take_time) + tip)
-
-    # ---------------------------------- 启动服务 ----------------------------------
-    # 使用 uvicorn 启动 FastAPI 应用，关闭原生日志
-    # 使用try except 捕获退出，涉及端口占用等
-    try:
-        uvicorn.run(app, host=host, port=port, log_level="critical")
-    except SystemExit as e:
-        code = e.code
-        if code == 1:
-            critical = "Error while attempting to bind on address ({}, {}): address already in use".format(host, port)
-            swl.critical(critical)
-        else:
-            swl.critical("Unhandled Exit Code: {}".format(code))
-
-
-# ---------------------------------- 登录命令，进行登录 ----------------------------------
-@cli.command()
-@click.option(
-    "--relogin",
-    "-r",
-    is_flag=True,
-    default=False,
-    help="Relogin to the swanlab cloud, it will recover the token file.",
-)
-@click.option(
-    "--api-key",
-    "-k",
-    default=None,
-    type=str,
-    help="If you prefer not to engage in command-line interaction to input the api key, "
-    "this will allow automatic login.",
-)
-def login(api_key: str, relogin: bool, **kwargs):
-    """Login to the swanlab cloud."""
-    if not relogin and is_login():
-        # 此时代表token已经获取，需要打印一条信息：已经登录
-        command = FONT.bold("swanlab login --relogin")
-        tip = FONT.swanlab("You are already logged in. Use `" + command + "` to force relogin.")
-        return print(tip)
-    # 进行登录，此时将直接覆盖本地token文件
-    login_info = terminal_login(api_key)
-    print(FONT.swanlab("Login successfully. Hi, " + FONT.bold(FONT.default(login_info.username))) + "!")
-
-
-# ---------------------------------- 退出登录命令 ----------------------------------
-@cli.command()
-def logout(**kwargs):
-    """Logout to the swanlab cloud."""
-    command = FONT.bold("swanlab login")
-    if is_login():
-        # 如果已经是登录状态，那么则询问用户是否确认，如果确认则删除token文件夹
-        confirm = input(FONT.swanlab("Are you sure you want to logout? (y/N): "))
-        if confirm.lower() == "y":
-            try:
-                shutil.rmtree(get_swanlab_folder())
-                return print(FONT.swanlab("Logout successfully. You can use `" + command + "` to login again."))
-            except Exception as e:
-                return print(FONT.swanlab("Logout failed. Please check if you have file operation permissions."))
-        else:
-            return print(FONT.swanlab("Logout canceled."))
-
-    # 如果还未登录，则不做任何处理，并告知用户如何登录
-    tip = FONT.swanlab("You are not logged in. If you want to login in, please use `" + command + "` to login.")
-    return print(tip)
-
-
-# ---------------------------------- 转换命令，用于转换其他实验跟踪工具 ----------------------------------
-@cli.command()
-@click.option(
-    "--type",
-    "-t",
-    default="tensorboard",
-    type=click.Choice(["tensorboard", "wandb"]),
-    help="The type of the experiment tracking tool you want to convert to.",
-)
-@click.option(
-    "--project",
-    "-p",
-    default=None,
-    type=str,
-    help="SwanLab project name.",
-)
-@click.option(
-    "--workspace",
-    "-w",
-    default=None,
-    type=str,
-    help="swanlab.init workspace parameter.",
-)
-@click.option(
-    "--cloud",
-    default=True,
-    type=bool,
-    help="swanlab.init cloud parameter.",
-)
-@click.option(
-    "--logdir",
-    "-l",
-    type=str,
-    help="The directory where the swanlab log files are stored.",
-)
-@click.option(
-    "--tb_logdir",
-    type=str,
-    help="The directory where the tensorboard log files are stored.",
-)
-@click.option(
-    "--wb-project",
-    type=str,
-    help="The project name of the wandb runs.",
-)
-@click.option(
-    "--wb-entity",
-    type=str,
-    help="The entity name of the wandb runs.",
-)
-@click.option(
-    "--wb-runid",
-    type=str,
-    help="The run_id of the wandb run.",
-)
-def convert(
-    type: str,
-    project: str,
-    cloud: bool,
-    workspace: str,
-    logdir: str,
-    tb_logdir: str,
-    wb_project: str,
-    wb_entity: str,
-    wb_runid: str,
-    **kwargs,
-):
-    """Convert the log files of other experiment tracking tools to SwanLab."""
-    if type == "tensorboard":
-        from swanlab.converter import TFBConverter
-
-        tfb_converter = TFBConverter(
-            convert_dir=tb_logdir,
-            project=project,
-            workspace=workspace,
-            cloud=cloud,
-            logdir=logdir,
-        )
-        tfb_converter.run()
-
-    elif type == "wandb":
-        from swanlab.converter import WandbConverter
+# ---------------------------------- 添加其他命令 ----------------------------------
 
-        print(wb_project, wb_entity, wb_runid)
 
-        wb_converter = WandbConverter(
-            project=project,
-            workspace=workspace,
-            cloud=cloud,
-            logdir=logdir,
-        )
-        wb_converter.run(
-            wb_project=wb_project,
-            wb_entity=wb_entity,
-            wb_run_id=wb_runid,
-        )
+# noinspection PyTypeChecker
+cli.add_command(C.login)  # 登录
+# noinspection PyTypeChecker
+cli.add_command(C.logout)  # 登出
 
-    else:
-        raise ValueError("The type of the experiment tracking tool you want to convert to is not supported.")
+# noinspection PyTypeChecker
+cli.add_command(C.watch)  # 启动服务
 
+# noinspection PyTypeChecker
+cli.add_command(C.convert)  # 转换命令，用于转换其他实验跟踪工具
 
 if __name__ == "__main__":
     cli()
diff --git a/swanlab/cli/utils.py b/swanlab/cli/utils.py
deleted file mode 100644
index 71ba4f53..00000000
--- a/swanlab/cli/utils.py
+++ /dev/null
@@ -1,153 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2023-12-15 15:53:21
-@File: swanlab/cli/utils.py
-@IDE: vscode
-@Description:
-    命令行工具
-"""
-import os
-import psutil
-import socket
-import click
-from ..utils import FONT, file
-from ..env import PORT, HOST, ROOT
-
-
-def is_valid_ip(ctx, param, ip: str) -> None:
-    """检测输入的是否是合法的ip地址,完成环境变量的注入
-
-    Parameters
-    ----------
-    ctx : click.Context
-        上下文
-    param : click.Parameter
-        参数
-    ip : str
-        带检测的字符串
-    """
-    if ip is None:
-        return
-    if not file.is_ipv4(ip):
-        raise click.BadParameter("Invalid ip address: " + ip)
-    os.environ[HOST] = ip
-
-
-def is_valid_port(ctx, param, port: int) -> int:
-    """检测是否是合法的端口号
-
-    Parameters
-    ----------
-    ctx : click.Context
-        上下文
-    param : click.Parameter
-        参数
-    port : int
-        带检测的端口号
-    """
-    if port is None:
-        return
-    if not file.is_port(port):
-        raise click.BadParameter("Invalid port number: " + str(port))
-    os.environ[PORT] = str(port)
-
-
-def is_valid_root_dir(ctx, param, log_dir: str) -> str:
-    """检测是否是合法的日志目录，保证其可读且存在
-
-    Parameters
-    ----------
-    ctx : click.Context
-        上下文
-    param : click.Parameter
-        参数
-    log_dir : str
-        带检测的日志目录
-    """
-    # 将日志目录注入环境变量，在这之前先转换为绝对路径
-
-    if log_dir is None:
-        return
-
-    # 将传入的路径转换为绝对路径
-    log_dir = os.path.abspath(log_dir)
-
-    # 必须是一个绝对路径
-    if not os.path.isabs(log_dir):
-        raise click.BadParameter("Log dir must be an absolute path: " + log_dir)
-    # 路径必须存在
-    if not os.path.isdir(log_dir):
-        raise click.BadParameter("Log dir is not a directory: " + log_dir)
-    # 路径必须可读
-    if not os.access(log_dir, os.R_OK):
-        raise click.BadParameter("Log dir is not readable: " + log_dir)
-
-    os.environ[ROOT] = log_dir
-
-
-class URL(object):
-    # 生成链接提示,先生成各个组件
-    _arrow = "\t\t\t➜"
-    arrow = FONT.bold(FONT.green(_arrow))
-    local = arrow + FONT.bold("  Local:   ")
-    netwo = arrow + FONT.bold("  Network: ")
-
-    def __init__(self, ip, port) -> None:
-        self.ip = ip
-        self.port = port
-
-    def __str__(self) -> str:
-        url = FONT.blue(f"http://{self.ip}:{self.port}")
-        if self.is_localhost(self.ip):
-            return self.local + url
-        else:
-            return self.netwo + url
-
-    @classmethod
-    def last_tip(cls) -> str:
-        """
-        打印最后一条提示信息
-        """
-        t = FONT.dark_gray("  press ") + FONT.bold(FONT.default("ctrl + c")) + FONT.dark_gray(" to quit")
-        return FONT.dark_green(cls._arrow) + t
-
-    @staticmethod
-    def is_localhost(ip):
-        return ip == "127.0.0.1" or ip == "localhost"
-
-    @staticmethod
-    def is_zero_ip(ip):
-        return ip == "0.0.0.0"
-
-    @staticmethod
-    def get_all_ip() -> list:
-        """获取所有可用的ip地址
-
-        Parameters
-        ----------
-        ip : str
-            本机的ip地址
-
-        Returns
-        -------
-        tuple
-            所有可用的ip地址
-        """
-        interfaces = psutil.net_if_addrs()
-        ipv4: list = []
-        # APIPA 地址范围
-        apipa_range = range(169, 255)
-        for _, addresses in interfaces.items():
-            for address in addresses:
-                # 如果是ipv4地址，且可以被访问到
-                if address.family == socket.AddressFamily.AF_INET:
-                    # 排除 APIPA 地址范围
-                    octets = list(map(int, address.address.split(".")))
-                    if octets[0] == 169 and octets[1] in apipa_range:
-                        continue
-                    ipv4.append(address.address)
-        # 对 IPv4 进行排序，"127.0.0.1" 在最前面，剩下按照从小到大排序
-        ipv4.sort(key=lambda x: (x != "127.0.0.1", x), reverse=True)
-        ipv4.reverse()
-        return ipv4
diff --git a/swanlab/compat/README.md b/swanlab/compat/README.md
deleted file mode 100644
index e6bf26b3..00000000
--- a/swanlab/compat/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# COMPAT
-
-因为各种原因，某些破坏性更新可能导致 SwanLab 无法正常运行。为了保证 SwanLab 的正常运行，我们需要兼容旧版本时的配置、数据等等的内容。
diff --git a/swanlab/compat/server/controller/experiment.py b/swanlab/compat/server/controller/experiment.py
deleted file mode 100644
index 62587f3e..00000000
--- a/swanlab/compat/server/controller/experiment.py
+++ /dev/null
@@ -1,89 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-04-02 21:23:30
-@File: swanlab/compat/server/controller/experiment.py
-@IDE: vscode
-@Description:
-    用于兼容实验部分的接口
-"""
-import shutil
-from ....db import *
-from urllib.parse import unquote
-from swanlab.server.settings import (
-    get_media_dir,
-    get_tag_dir,
-)
-import os
-import ujson
-
-
-def _transfer_logs(log_path: str, text_path: str):
-    """
-    将原本存于某 txt 中的内容直接以字符串的形式存到对应的 log 文件
-    一个 log 文件可能对应多个 txt 文件，该函数一次处理一个 log 文件
-
-    Parameters
-    ----------
-    log_path : str
-        tag 对应的 logs 下目录的路径
-    text_path : str
-        tag 对应 media 下目录的路径
-
-    Returns
-    -------
-        单个文件转化后的数据
-    """
-    with open(log_path, "r") as f:
-        data = f.read().split("\n")
-        # 将每个元素转成dict
-        data = [ujson.loads(line) for line in data if line]
-    # 打开对应的 txt 文件
-    for index, log in enumerate(data):
-        path = os.path.join(text_path, log["data"])
-        with open(path, "r") as f:
-            text = f.read()
-        data[index]["data"] = text
-    # # 保存 log 文件
-    with open(log_path, "w") as f:
-        for line in data:
-            f.write(ujson.dumps(line, ensure_ascii=False) + "\n")
-    return data
-
-
-def compat_text(experiment_id: int, tag: str):
-    """
-    0.3.0 之前，text 图表中的媒体数据单独存放于 txt 文件中
-    但是因为其本质是字符串，可以直接存储于 logs 下
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验 id
-    tag : str
-        tag 名
-
-    Returns
-    -------
-        如果不需要转化，返回 False，如果需要转化，返回转化后的数据
-    """
-
-    tag = Tag.filter(Tag.experiment_id == experiment_id, Tag.name == unquote(tag)).first()
-    if tag.type != "text":
-        return False
-    # 检测在 media 下是否存在同名目录
-    run_id = Experiment.get(experiment_id).run_id
-    text_path = get_media_dir(run_id, tag.name)
-    # 是否存在该路径
-    if not os.path.exists(text_path):
-        return False
-    # 将文件内容复制到 logs 记录中
-    tag_path = get_tag_dir(run_id, tag.name)
-    # 列出下面所有文件，除了 _summary.json
-    files = [file for file in os.listdir(tag_path) if file != "_summary.json"]
-    data: list = []
-    for file in files:
-        data.extend(_transfer_logs(os.path.join(tag_path, file), text_path))
-    # 删除原来的媒体目录
-    shutil.rmtree(text_path)
-    return data
diff --git a/swanlab/converter/__init__.py b/swanlab/converter/__init__.py
index 87e8d35e..8d8e7331 100644
--- a/swanlab/converter/__init__.py
+++ b/swanlab/converter/__init__.py
@@ -1,2 +1,11 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/14 00:51
+@File: __init__.py.py
+@IDE: pycharm
+@Description:
+    转换部分，兼容其他可视化工具，转换为swanlab格式
+"""
 from .tfb import TFBConverter
 from .wb import WandbConverter
diff --git a/swanlab/data/__init__.py b/swanlab/data/__init__.py
index 5ee93da0..31f5fdba 100644
--- a/swanlab/data/__init__.py
+++ b/swanlab/data/__init__.py
@@ -8,7 +8,6 @@
     在此处完成回调注册、swanlog注册，并为外界提供api，提供运行时生成的配置
 """
 from .modules import (
-    BaseType,
     Audio,
     Image,
     Text,
diff --git a/swanlab/data/sdk.py b/swanlab/data/sdk.py
index 0b268d03..bc43a156 100644
--- a/swanlab/data/sdk.py
+++ b/swanlab/data/sdk.py
@@ -19,12 +19,12 @@
 from .callback_cloud import CloudRunCallback
 from .callback_local import LocalRunCallback
 from .run.operator import SwanLabRunOperator
-from swanlab.env import init_env, get_swanlog_dir, SwanLabMode, MODE
+from swanlab.env import init_env, get_swanlog_dir, SwanLabMode, MODE, ROOT
 from swanlab.log import swanlog
 from swanlab.utils import check_load_json_yaml, check_proj_name_format
 from swanlab.api import code_login
-from swanlab.db import GlomCallback
 from swanlab.package import version_limit
+from swanboard import SwanBoardCallback
 
 
 def _check_proj_name(name: str) -> str:
@@ -69,16 +69,16 @@ def login(api_key: str = None):
 
 
 def init(
-    project: str = None,
-    workspace: str = None,
-    experiment_name: str = None,
-    description: str = None,
-    config: Union[dict, str] = None,
-    logdir: str = None,
-    suffix: Union[str, None, bool] = "default",
-    mode: Literal["disabled", "cloud", "local"] = None,
-    load: str = None,
-    **kwargs,
+        project: str = None,
+        workspace: str = None,
+        experiment_name: str = None,
+        description: str = None,
+        config: Union[dict, str] = None,
+        logdir: str = None,
+        suffix: Union[str, None, bool] = "default",
+        mode: Literal["disabled", "cloud", "local"] = None,
+        load: str = None,
+        **kwargs,
 ) -> SwanLabRun:
     """
     Start a new run to track and log. Once you have called this function, you can use 'swanlab.log' to log data to
@@ -295,5 +295,5 @@ def _create_operator(mode) -> Tuple[SwanLabRunOperator, Optional[CloudRunCallbac
         swanlog.warning("SwanLab run disabled, the data will not be saved or uploaded.")
         return SwanLabRunOperator(), None
     c = CloudRunCallback() if mode == SwanLabMode.CLOUD.value else LocalRunCallback()
-    callbacks = [c, GlomCallback()]
+    callbacks = [c, SwanBoardCallback()]
     return SwanLabRunOperator(callbacks), c
diff --git a/swanlab/db/__init__.py b/swanlab/db/__init__.py
deleted file mode 100644
index 961c96ad..00000000
--- a/swanlab/db/__init__.py
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-16 10:52:10
-@File: swanlab\db\__init__.py
-@IDE: vscode
-@Description:
-    数据库模型模块，在设计上应该在data模块或者server模块被初始化完毕后导入
-    导入之前必须确保文件路径存在，否则会报错
-"""
-from .models import (
-    Project,
-    Experiment,
-    Tag,
-    Chart,
-    Namespace,
-    Source,
-    Display,
-)
-from .error import (
-    ExistedError,
-    NotExistedError,
-    ForeignProNotExistedError,
-    ForeignExpNotExistedError,
-    ForeignTagNotExistedError,
-    ForeignChartNotExistedError,
-    ForeignNameNotExistedError,
-    ChartTypeError,
-)
-from .table_config import tables
-from .db_connect import connect
-from .utils import add_multi_chart
-from .callback import GlomCallback
diff --git a/swanlab/db/callback.py b/swanlab/db/callback.py
deleted file mode 100644
index de198939..00000000
--- a/swanlab/db/callback.py
+++ /dev/null
@@ -1,192 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024/5/12 15:22
-@File: callback.py
-@IDE: pycharm
-@Description:
-    注册回调函数
-"""
-from swanlab.data.run.callback import SwanLabRunCallback, ColumnInfo
-from swanlab.log import swanlog
-from .models import *
-from .utils.chart import add_multi_chart
-from .db_connect import connect
-from .error import NotExistedError, ExistedError, ChartTypeError
-from typing import Tuple, Callable, Optional
-from swanlab.utils.file import check_exp_name_format, check_desc_format
-from datetime import datetime
-import time
-import sys
-
-
-class GlomCallback(SwanLabRunCallback):
-    """
-    GlomCallback类，swanlab本体与数据库的连接回调函数
-    """
-
-    def __init__(self):
-        super(GlomCallback, self).__init__()
-        self.exp: Optional[Experiment] = None
-
-    @staticmethod
-    def __get_exp_name(experiment_name: str = None, suffix: str = None) -> Tuple[str, str]:
-        """
-        预处理实验名称，如果实验名称过长，截断
-
-        Parameters
-        ----------
-        experiment_name : str
-            实验名称
-        suffix : str
-            实验名称后缀添加方式，可以为None、"default"或自由后缀，前者代表不添加后缀，后者代表添加时间戳后缀
-
-        Returns
-        ----------
-        experiment_name : str
-            校验后的实验名称
-        exp_name : str
-            最终的实验名称
-        """
-        # ---------------------------------- 校验实验名称 ----------------------------------
-        experiment_name = "exp" if experiment_name is None else experiment_name
-        # 校验实验名称
-        experiment_name_checked = check_exp_name_format(experiment_name)
-        # 如果实验名称太长的tip
-        tip = "The experiment name you provided is too long, it has been truncated automatically."
-
-        # 如果前后长度不一样，说明实验名称被截断了，提醒
-        if len(experiment_name_checked) != len(experiment_name) and suffix is None:
-            swanlog.warning(tip)
-
-        # 如果suffix为None, 则不添加后缀，直接返回
-        if suffix is None or suffix is False:
-            return experiment_name_checked, experiment_name
-
-        # 如果suffix为True, 则添加默认后缀
-        if suffix is True:
-            suffix = "default"
-
-        # suffix必须是字符串
-        if not isinstance(suffix, str):
-            raise TypeError("The suffix must be a string, but got {}".format(type(suffix)))
-
-        # 如果suffix_checked为default，则设置为默认后缀
-        if suffix.lower().strip() == "default":
-            # 添加默认后缀
-            default_suffix = "{}".format(datetime.now().strftime("%b%d_%H-%M-%S"))
-            exp_name = "{}_{}".format(experiment_name_checked, default_suffix)
-        else:
-            exp_name = "{}_{}".format(experiment_name_checked, suffix)
-
-        # 校验实验名称，如果实验名称过长，截断
-        experiment_name_checked = check_exp_name_format(exp_name)
-        if len(experiment_name_checked) != len(exp_name):
-            swanlog.warning(tip)
-
-        return experiment_name_checked, exp_name
-
-    def __str__(self):
-        return "SwanLabGlomCallback"
-
-    def on_init(self, proj_name: str, *args, **kwargs):
-        # 连接本地数据库，要求路径必须存在，但是如果数据库文件不存在，会自动创建
-        connect(autocreate=True)
-        # 初始化项目数据库
-        Project.init(proj_name)
-
-    def on_run_error_from_operator(self, e):
-        # 更新数据库中的实验状态
-        swanlog.error(e)
-        Experiment.purely_delete(run_id=self.settings.run_id)
-        sys.exit(409)
-
-    def before_init_experiment(
-        self,
-        run_id: str,
-        exp_name: str,
-        description: str,
-        num: int,
-        suffix: str,
-        setter: Callable[[str, str, str, str], None],
-    ):
-        # ---------------------------------- 实验描述校验 ----------------------------------
-        description = description if description is not None else ""
-        desc = check_desc_format(description)
-        if desc != description:
-            swanlog.warning("The description has been truncated automatically.")
-        # ---------------------------------- 实验名称校验 ----------------------------------
-        # 这个循环的目的是如果创建失败则等零点五秒重新生成后缀重新创建，直到创建成功
-        # 但是由于需要考虑suffix为none不生成后缀的情况，所以需要在except中判断一下
-        old = exp_name
-        while True:
-            experiment_name, exp_name = self.__get_exp_name(old, suffix)
-            try:
-                # 获得数据库实例
-                self.exp = Experiment.create(name=exp_name, run_id=run_id, description=description, num=num)
-                break
-            except ExistedError:
-                # 如果suffix名为default，说明是自动生成的后缀，需要重新生成后缀
-                if isinstance(suffix, str) and suffix.lower().strip() == "default":
-                    swanlog.debug(f"Experiment {exp_name} has existed, try another name...")
-                    time.sleep(0.5)
-                    continue
-                # 其他情况下，说明是用户自定义的后缀，需要报错
-                else:
-                    Experiment.purely_delete(run_id=run_id)
-                    raise ExistedError(f"Experiment {exp_name} has existed in local, please try another name.")
-        # 执行相关设置
-        setter(experiment_name, self.exp.light, self.exp.dark, self.exp.description)
-
-    def on_log(self):
-        # 每一次log的时候检查一下数据库中的实验状态
-        # 如果实验状态不为0，说明实验已经结束，不允许再次调用log方法
-        # 这意味着每次log都会进行查询，比较消耗性能，后续考虑采用多进程共享内存的方式进行优化
-        swanlog.debug(f"Check experiment and state...")
-        try:
-            exp = Experiment.get(id=self.exp.id)
-        except NotExistedError:
-            raise KeyboardInterrupt("The experiment has been deleted by the user")
-        # 此时self.__state == 0，说明是前端主动停止的
-        if exp.status != 0:
-            raise KeyboardInterrupt("The experiment has been stopped by the user")
-
-    def on_column_create(self, column_info: ColumnInfo):
-        # 创建Chart
-        chart = Chart.create(
-            column_info.key,
-            experiment_id=self.exp,
-            type=column_info.chart.value.chart_type,
-            reference=column_info.reference,
-            config=column_info.config,
-        )
-        # 创建命名空间，如果命名空间已经存在，会抛出ExistedError异常，捕获不处理即可
-        # 需要指定sort，default命名空间的sort为0，其他命名空间的sort为None，表示默认添加到最后
-        namespace = column_info.namespace
-        try:
-            n = Namespace.create(name=namespace, experiment_id=self.exp.id, sort=column_info.sort)
-            swanlog.debug(f"Namespace {namespace} created, id: {n.id}")
-        except ExistedError:
-            n: Namespace = Namespace.get(name=namespace, experiment_id=self.exp.id)
-            swanlog.debug(f"Namespace {namespace} exists, id: {n.id}")
-        # 创建display，这个必然是成功的，因为display是唯一的，直接添加到最后一条即可
-        Display.create(chart_id=chart.id, namespace_id=n.id)
-        tag: Tag = Tag.create(
-            experiment_id=self.exp.id,
-            name=column_info.key,
-            type=column_info.chart.value.chart_type,
-        )
-        # 添加一条source记录
-        error = None
-        if column_info.error is not None:
-            error = {"data_class": column_info.error.got, "excepted": column_info.error.expected}
-        Source.create(tag_id=tag.id, chart_id=chart.id, error=error)
-        # 新建多实验对比图表数据
-        try:
-            add_multi_chart(tag_id=tag.id, chart_id=chart.id)
-        except ChartTypeError:
-            swanlog.debug("In the multi-experiment chart (Local), the current type of tag is not as expected.")
-
-    def on_stop(self, error: str = None):
-        # 更新数据库中的实验状态
-        self.exp.update_status(-1 if error is not None else 1)
diff --git a/swanlab/db/db_connect.py b/swanlab/db/db_connect.py
deleted file mode 100644
index e04d4838..00000000
--- a/swanlab/db/db_connect.py
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-02-05 16:56:06
-@File: swanlab/db/connect.py
-@IDE: vscode
-@Description:
-    数据库连接模块
-"""
-from ..env import get_db_path
-import os
-from peewee import SqliteDatabase
-from .table_config import tables, Tag, Experiment, Namespace, Chart
-from .migrate import *
-
-# 判断是否已经binded了
-binded = False
-
-
-def connect(autocreate=False) -> SqliteDatabase:
-    """
-    连接数据库，只有调用此方法以后，数据库才会被创建，所有导出的类才可用
-    这样设计的原因是因为路径问题，这里需要动态导入settings
-    第一次调用此方法时会创建数据库，返回None，以后调用此方法会返回数据库实例，通过其自身的`connect`方法连接数据库
-    完成复杂的数据库操作后，需要调用其`close`方法关闭数据库连接
-
-    Parameters:
-    ----------
-    existed : bool
-        是否指定创建数据库，如果为True，则会自动创建数据库，否则不会自动创建数据库
-        设置为False时，如果目标数据库不存在，则会抛出FileNotFoundError异常
-        但是即使设置为true，文件夹不存在的情况下也会抛出FileNotFoundError异常
-
-    Return:
-    -------
-    swandb :
-        数据库实例
-
-    Raises:
-    -------
-    FileNotFoundError :
-        如果数据库不存在且existed为False，则会抛出FileNotFoundError异常
-        或者路径不存在，也会抛出FileNotFoundError异常
-    """
-    global binded
-    path = get_db_path()
-    # 判断数据库是否存在
-    db_exists = os.path.exists(path)
-    path_exists = os.path.exists(os.path.dirname(path))
-    if not path_exists or (not db_exists and not autocreate):
-        raise FileNotFoundError(f"DB file {path} not found")
-    # 启用外键约束
-    swandb = SqliteDatabase(path, pragmas={"foreign_keys": 1})
-    if not binded:
-        # 动态绑定数据库
-        swandb.connect()
-        swandb.bind(tables)
-        swandb.create_tables(tables)
-        swandb.close()
-        # 完成数据迁移，如果chart表中没有status字段，则添加
-        if not Chart.field_exists("status"):
-            # 不启用外键约束
-            add_status(SqliteDatabase(path))
-        # 完成数据迁移，如果namespace表中没有opened字段，则添加
-        if not Namespace.field_exists("opened"):
-            # 不启用外键约束
-            add_opened(SqliteDatabase(path))
-        # 完成数据迁移，如果experiment表中没有finish_time字段，则添加
-        if not Experiment.field_exists("finish_time"):
-            # 不启用外键约束
-            add_finish_time(SqliteDatabase(path))
-        # 完成数据迁移，如果tag表中没有sort字段，则添加
-        if not Tag.field_exists("sort"):
-            # 不启用外键约束
-            add_sort(SqliteDatabase(path))
-        binded = True
-    return swandb
diff --git a/swanlab/db/docs/Errors.md b/swanlab/db/docs/Errors.md
deleted file mode 100644
index 1ea4a526..00000000
--- a/swanlab/db/docs/Errors.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# 错误类型
-
-自建错误和了解 peewee 内置错误类型的目的是提供更好的错误定位。
-
-## SwanLab 内部错误
-
-### ExistedError
-
-某条数据已存在
-
-### NotExistedError
-
-某条数据不存在
-
-## Peewee 错误类型
-
-### IntegrityError
-
-通常表示在执行数据库操作时违反了数据库的完整性约束。这个错误经常涉及到主键重复、唯一约束冲突或外键关联问题。
-
-一些常见的引发 IntegrityError 的情况包括：
-
-- 主键重复： 尝试将具有与已存在行相同主键值的新行插入表中。
-
-- 唯一约束冲突： 尝试插入具有唯一约束的列，而该列的值与已存在行中的值冲突。
-
-- 外键关联问题： 尝试插入或更新关联了外键的列，而没有对应的父表中的匹配行。
diff --git a/swanlab/db/docs/README.md b/swanlab/db/docs/README.md
deleted file mode 100644
index cad29312..00000000
--- a/swanlab/db/docs/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# DB
-
-此处存放数据库操作
-
-## 公共接口
-
-对于不同的数据表类，难免会有一些共同的接口和工具。
-
-### __dict__
-
-该方法为实例方法。
-
-对于 `<Model: cls>` 类型的行数据实例，我们更希望能够直接以字典的格式查看和获取数据，但是 `model_to_dict` 并不能接受这样的类型。
-所以我们在每个数据表类中，都配置了一个 `__dict__` 方法，用于在实例的身上下直接获取字典格式的数据。
-
-### create
-
-该方法复写了 `Model` 类的 `create` 方法。
-
-用于指定创建时的一些逻辑：检查外键、配置默认参数等待。
diff --git a/swanlab/db/error.py b/swanlab/db/error.py
deleted file mode 100644
index aada7266..00000000
--- a/swanlab/db/error.py
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-18 15:16:55
-@File: swanlab/db/error.py
-@IDE: vscode
-@Description:
-    在此处定义所有的数据错误类型，以便于在其他地方引用
-"""
-
-
-class ExistedError(Exception):
-    """某条数据已存在
-
-    Parameters
-    ----------
-    Exception : class
-        python内置异常类
-    """
-
-    pass
-
-
-class NotExistedError(Exception):
-    """某条数据不存在
-
-    Parameters
-    ----------
-    Exception : class
-        python内置异常类
-    """
-
-    pass
-
-
-class ForeignExpNotExistedError(NotExistedError):
-    """实验外键不存在"""
-
-    pass
-
-
-class ForeignProNotExistedError(NotExistedError):
-    """项目外键不存在"""
-
-    pass
-
-
-class ForeignNameNotExistedError(NotExistedError):
-    """命名空间外键不存在"""
-
-    pass
-
-
-class ForeignChartNotExistedError(NotExistedError):
-    """图表外键不存在"""
-
-    pass
-
-
-class ForeignTagNotExistedError(NotExistedError):
-    """标签外键不存在"""
-
-    pass
-
-
-class ChartTypeError(Exception):
-    """图表类型错误，对应的"""
-
-    pass
diff --git a/swanlab/db/migrate/__init__.py b/swanlab/db/migrate/__init__.py
deleted file mode 100644
index 32068e0e..00000000
--- a/swanlab/db/migrate/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-02-19 15:41:25
-@File: swanlab/db/migrate/__init__.py
-@IDE: vscode
-@Description:
-    数据库迁移模块
-"""
-from .tag import add_sort
-from .experiment import add_finish_time
-from .namespace import add_opened
-from .chart import add_status
diff --git a/swanlab/db/migrate/chart.py b/swanlab/db/migrate/chart.py
deleted file mode 100644
index 382150ca..00000000
--- a/swanlab/db/migrate/chart.py
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-03-09 17:39:12
-@File: swanlab/db/migrate/chart.py
-@IDE: vscode
-@Description:
-    chart相关迁移操作
-"""
-from peewee import SqliteDatabase, IntegerField
-from playhouse.migrate import migrate, SqliteMigrator
-from .experiment import (
-    add_hidden_opened as add_hidden_opened_experiment,
-    add_pinned_opened as add_pinned_opened_experiment,
-)
-from .project import add_hidden_opened as add_hidden_opened_project, add_pinned_opened as add_pinned_opened_project
-
-
-def add_status(db):
-    """
-    为数据库的Chart表添加status字段，默认为0
-    """
-    migrator = SqliteMigrator(db)
-    status = IntegerField(default=0)
-    migrate(migrator.add_column("chart", "status", status))
-    # 同时，添加sort字段
-    add_sort(db)
-    # 同时为experiment和project添加hidden_opened、pinned_opened字段
-    add_hidden_opened_experiment(db)
-    add_pinned_opened_experiment(db)
-    add_hidden_opened_project(db)
-    add_pinned_opened_project(db)
-
-
-def add_sort(db):
-    """
-    为数据库的Chart表添加sort字段，默认为NULL
-    """
-    migrator = SqliteMigrator(db)
-    sort = IntegerField(default=None, null=True)
-    migrate(migrator.add_column("chart", "sort", sort))
diff --git a/swanlab/db/migrate/experiment.py b/swanlab/db/migrate/experiment.py
deleted file mode 100644
index 165ea683..00000000
--- a/swanlab/db/migrate/experiment.py
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-02-29 14:34:29
-@File: swanlab/db/migrate/exp_finish_time.py
-@IDE: vscode
-@Description:
-    为数据库的Experiment表添加finish_time字段，用于记录实验结束时间
-    实验结束时间是指实验被标记为完成/崩溃的时间
-"""
-
-from peewee import SqliteDatabase, CharField, IntegerField
-from playhouse.migrate import migrate, SqliteMigrator
-
-
-def add_finish_time(db):
-    """
-    为数据库的Experoment表添加finish_time字段,用于记录实验结束时间
-    默认值为当前行的update_time
-    """
-    migrator = SqliteMigrator(db)
-    finish_time = CharField(max_length=30, null=True, default=None)
-    migrate(migrator.add_column("experiment", "finish_time", finish_time))
-    # 原子化操作，遍历所有status不为0的实验，将其finish_time设置为update_time
-    for exp in db.execute_sql("select id, update_time from experiment where status != 0"):
-        db.execute_sql(f"update experiment set finish_time = '{exp[1]}' where id = {exp[0]}")
-
-
-def add_pinned_opened(db):
-    """
-    为数据库的Experiment表添加pinned_opened字段,用于记录实验置顶部分是否打开
-    默认值为1，表示打开
-    """
-    migrator = SqliteMigrator(db)
-    pinned_opened = IntegerField(default=1, choices=[0, 1])
-    migrate(migrator.add_column("experiment", "pinned_opened", pinned_opened))
-
-
-def add_hidden_opened(db):
-    """
-    为数据库的Experiment表添加hidden_opened字段,用于记录实验隐藏部分是否打开
-    默认值为0,表示关闭
-    """
-    migrator = SqliteMigrator(db)
-    hidden_opened = IntegerField(default=0, choices=[0, 1])
-    migrate(migrator.add_column("experiment", "hidden_opened", hidden_opened))
diff --git a/swanlab/db/migrate/namespace.py b/swanlab/db/migrate/namespace.py
deleted file mode 100644
index 73595118..00000000
--- a/swanlab/db/migrate/namespace.py
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-03-05 19:42:52
-@File: swanlab/db/migrate/namespace_opened.py
-@IDE: vscode
-@Description:
-    为namespace增加opened字段
-"""
-from peewee import SqliteDatabase, IntegerField
-from playhouse.migrate import migrate, SqliteMigrator
-
-
-def add_opened(db):
-    """
-    为数据库的namespace表添加opened字段,用于记录命名空间是否已经被打开
-    默认值为False
-    """
-    migrator = SqliteMigrator(db)
-    opened = IntegerField(default=1, choices=[0, 1])
-    migrate(migrator.add_column("namespace", "opened", opened))
diff --git a/swanlab/db/migrate/project.py b/swanlab/db/migrate/project.py
deleted file mode 100644
index 7dc6023b..00000000
--- a/swanlab/db/migrate/project.py
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-03-09 17:43:27
-@File: swanlab/db/migrate/project.py
-@IDE: vscode
-@Description:
-    为数据库的Project表添加迁移操作
-"""
-from peewee import IntegerField
-from playhouse.migrate import migrate, SqliteMigrator
-
-
-def add_pinned_opened(db):
-    """
-    为数据库的Project表添加pinned_opened字段,用于记录项目置顶部分是否打开
-    默认值为1，表示打开
-    """
-    migrator = SqliteMigrator(db)
-    pinned_opened = IntegerField(default=1, choices=[0, 1])
-    migrate(migrator.add_column("project", "pinned_opened", pinned_opened))
-
-
-def add_hidden_opened(db):
-    """
-    为数据库的Project表添加hidden_opened字段,用于记录项目隐藏部分是否打开
-    默认值为0,表示关闭
-    """
-    migrator = SqliteMigrator(db)
-    hidden_opened = IntegerField(default=0, choices=[0, 1])
-    migrate(migrator.add_column("project", "hidden_opened", hidden_opened))
diff --git a/swanlab/db/migrate/tag.py b/swanlab/db/migrate/tag.py
deleted file mode 100644
index 4c6c52a9..00000000
--- a/swanlab/db/migrate/tag.py
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-02-19 15:40:58
-@File: swanlab/db/migrate/tag_sort.py
-@IDE: vscode
-@Description:
-    为数据库的Tag表添加sort字段
-"""
-from peewee import SqliteDatabase, IntegerField
-from playhouse.migrate import migrate, SqliteMigrator
-
-
-def add_sort(db: SqliteDatabase):
-    """
-    为数据库的Tag表添加sort字段
-    """
-    migrator = SqliteMigrator(db)
-    sort = IntegerField(default=0)
-    migrate(migrator.add_column("tag", "sort", sort))
diff --git a/swanlab/db/model.py b/swanlab/db/model.py
deleted file mode 100644
index 02028f2c..00000000
--- a/swanlab/db/model.py
+++ /dev/null
@@ -1,137 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-18 13:11:59
-@File: swanlab/db/model.py
-@IDE: vscode
-@Description:
-    在此处定义基础模型类
-"""
-from peewee import Model, OperationalError
-from playhouse.shortcuts import model_to_dict
-from ..utils import create_time
-from .error import NotExistedError
-import json
-
-
-class SwanModel(Model):
-    """基础模型类，用于定义数据库表的基本信息"""
-
-    @staticmethod
-    def search2dict(result) -> dict:
-        """将select、filter的结果转换为字典"""
-
-        dicts = [model_to_dict(row) for row in result]
-        if len(dicts) == 1:
-            return dicts[0]
-        else:
-            return dicts
-
-    @staticmethod
-    def search2list(result) -> list:
-        """将select、filter的结果转换为列表"""
-
-        return [model_to_dict(row) for row in result]
-
-    @staticmethod
-    def json2dict(json_str: str) -> dict:
-        """将json字符串转换为字典
-        如果字符串为空，返回空字典
-
-        Parameters
-        ----------
-        json_str : str
-            传入的json格式字符串，或者空字符串
-
-        Returns
-        ----------
-        dict :
-            转换后的字典
-
-        Raises
-        ----------
-        TypeError :
-            无法转换为字典
-        """
-        if json_str == "":
-            return {}
-        try:
-            return json.loads(json_str)
-        except:
-            raise TypeError
-
-    @staticmethod
-    def dict2json(dict: dict) -> str:
-        """将字典转换为json字符串,如果已经是json字符串，则直接返回
-
-        Parameters
-        ----------
-        dict : dict
-            输入的字典，如果dict为None，则解析为空字符串
-
-        Returns
-        -------
-        str
-            转换后的字符串
-
-                Raises
-        ----------
-
-        TypeError :
-            无法转换为Json字符串
-        """
-        if dict is None:
-            return ""
-        if isinstance(dict, str):
-            try:
-                if not dict:
-                    return ""
-                json.loads(dict)
-                return dict
-            except:
-                raise TypeError
-        try:
-            return json.dumps(dict)
-        except:
-            raise TypeError
-
-    def save(self, *args, **kwargs):
-        """保存数据
-        经过swanmodel的覆写，save方法自动更新update_time字段
-        """
-        self.update_time = create_time()
-        super().save(*args, **kwargs)
-
-    @classmethod
-    def insert_many(cls, rows, fields=None):
-        """批量插入数据
-        经过swanmodel的覆写，insert_many方法自动创建create_time和update_time字段
-        """
-        current_time = create_time()
-        rows = [{**row, "create_time": current_time, "update_time": current_time} for row in rows]
-        return super().insert_many(rows, fields=fields)
-
-    @classmethod
-    def create(cls, **query):
-        current_time = create_time()
-        query["create_time"] = current_time
-        query["update_time"] = current_time
-        return super().create(**query)
-
-    @classmethod
-    def field_exists(cls, field: str) -> bool:
-        """
-        判断某个字段是否存在于表中
-
-        Parameters
-        ----------
-        field : str
-            字段名
-        """
-        # 进行一次查询
-        try:
-            a = cls.select().where(cls.__getattribute__(cls, field) is not None)
-            cls.search2dict(a)
-        except OperationalError:
-            return False
-        return True
diff --git a/swanlab/db/models/__init__.py b/swanlab/db/models/__init__.py
deleted file mode 100644
index 297007fc..00000000
--- a/swanlab/db/models/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-18 13:08:48
-@File: swanlab/db/models/__init__.py
-@IDE: vscode
-@Description:
-    在此处自定义模型类，继承自SwanModel
-"""
-from .charts import Chart
-from .displays import Display
-from .experiments import Experiment
-from .namespaces import Namespace
-from .projects import Project
-from .tags import Tag
-from .sources import Source
diff --git a/swanlab/db/models/charts.py b/swanlab/db/models/charts.py
deleted file mode 100644
index e776a23f..00000000
--- a/swanlab/db/models/charts.py
+++ /dev/null
@@ -1,244 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-18 13:52:35
-@File: swanlab\db\models\chart.py
-@IDE: vscode
-@Description:
-    实验图标表
-"""
-from ..model import SwanModel
-from peewee import CharField, IntegerField, ForeignKeyField, TextField, IntegrityError, Check, DatabaseProxy, fn
-from ..error import ExistedError, ForeignExpNotExistedError, ForeignProNotExistedError
-from .experiments import Experiment
-from .projects import Project
-
-
-class Chart(SwanModel):
-    """chart表，图表在namspace下的排序交由display表处理"""
-
-    class Meta:
-        database = DatabaseProxy()
-        # 通过meta规定name和project_id的唯一性
-        indexes = ((("name", "experiment_id"), True), (("name", "project_id"), True))
-
-        constraints = [
-            # project_id 和 experiment_id 不能同时为空，也不能同时不为空
-            Check(
-                "(project_id IS NULL AND experiment_id IS NOT NULL) OR (project_id IS NOT NULL AND experiment_id IS NULL)"
-            ),
-        ]
-
-    id = IntegerField(primary_key=True)
-    """图表id, 自增"""
-    experiment_id = ForeignKeyField(Experiment, backref="charts", on_delete="CASCADE", on_update="CASCADE", null=True)
-    """外键，关联的实验id，与project_id只有一个为NULL"""
-    project_id = ForeignKeyField(
-        Project, backref="charts", default=1, on_delete="CASCADE", on_update="CASCADE", null=True
-    )
-    """外键，关联的项目id，与experiment_id只有一个为NULL"""
-    name = CharField(max_length=100, null=False)
-    """图表名称"""
-    description = CharField(max_length=255, null=True)
-    """图表描述，可为空"""
-    system = IntegerField(default=1, choices=[-1, 0, 1])
-    """是否为创建tag时自动生成的图表，-1: 删除的自动生成的图表，0: 否，1: 是，系统图表不可删除，只能改为-1"""
-    type = CharField(max_length=10, null=False)
-    """图表类型，由创建者决定，这与数据库本身无关"""
-    reference = CharField(max_length=10, default="step")
-    """图表数据参考，由创建者决定，这与数据库本身无关"""
-    status = IntegerField(null=False, default=0)
-    """图表状态 0代表默认状态 1代表被pinned -1代表被hidden"""
-    sort = IntegerField(null=True, default=None)
-    """图表在被pinned或hidden时的排序，越小越靠前，越大越靠后，如果为NULL则代表未被pinned或hidden"""
-    config = TextField(null=True)
-    """图表的其他配置，这实际上是一个json字符串"""
-    more = TextField(null=True)
-    """更多信息配置，json格式"""
-    create_time = CharField(max_length=30, null=False)
-    """创建时间"""
-    update_time = CharField(max_length=30, null=False)
-    """更新时间"""
-
-    def __dict__(self):
-        experiment_id = None if self.experiment_id is None else self.experiment_id.__dict__()
-        project_id = None if self.project_id is None else self.project_id.__dict__()
-        return {
-            "id": self.id,
-            "experiment_id": experiment_id,
-            "project_id": project_id,
-            "name": self.name,
-            "description": self.description,
-            "system": self.system,
-            "type": self.type,
-            "reference": self.reference,
-            "config": self.config,
-            "status": self.status,
-            "sort": self.sort,
-            "more": self.more,
-            "create_time": self.create_time,
-            "update_time": self.update_time,
-        }
-
-    @classmethod
-    def create(
-        cls,
-        name: str,
-        description: str = None,
-        experiment_id: int = None,
-        project_id: int = None,
-        system: int = 1,
-        type: str = "default",
-        reference: str = "step",
-        config: dict = None,
-        more: dict = None,
-    ) -> "Chart":
-        """创建实验图表
-
-        Parameters
-        ----------
-        name : str
-            实验图表名称
-        description : str
-            实验图表描述
-        experiment_id : int, optional
-            外键，关联的实验id，与project_id只有一个为NULL, 默认为None，但与project_id只有一个为None
-        project_id : int, optional
-            外键，关联的项目id，与experiment_id只有一个为NULL, 默认为None，但与experiment_id只有一个为None
-        system : int, optional
-            是否为创建tag时自动生成的图表，-1: 删除的自动生成的图表，0: 否，1: 是，系统图表不可删除，只能改为-1, 默认为是
-        type : str, optional
-            图表类型，由创建者决定，这与数据库本身无关, 默认为"default"
-        reference : str, optional
-            图表数据参考，由创建者决定，这与数据库本身无关, 默认为"step"
-        config : dict, optional
-            图表的其他配置，这实际上是一个json字符串, 默认为None
-        more : dict, optional
-            更多信息配置，json格式, 默认为None
-
-        Returns
-        -------
-        Chart : Chart
-            创建的实验图表
-
-        Raises
-        -------
-        ForeignExpNotExistedError
-            实验不存在
-        ForeignProNotExistedError
-            项目不存在
-        ExistedError
-            图标必须唯一
-        """
-
-        # 检查外键存在性
-        if project_id and not Project.filter(Project.id == project_id).exists():
-            raise ForeignProNotExistedError("项目不存在")
-        if experiment_id and not Experiment.filter(Experiment.id == experiment_id).exists():
-            raise ForeignExpNotExistedError("实验不存在")
-
-        try:
-            return super().create(
-                experiment_id=experiment_id,
-                project_id=project_id,
-                name=name,
-                description=description,
-                system=system,
-                type=type,
-                reference=reference,
-                config=config,
-                more=more,
-            )
-        except IntegrityError:
-            raise ExistedError("图表已存在")
-
-    @classmethod
-    def pin(cls, id: int, sort: int = None) -> "Chart":
-        """
-        把某个chart置顶
-
-        Parameters
-        ----------
-        id : int
-            图表id
-        sort : int
-            图表在被pinned或hidden时的排序，越小越靠前，越大越靠后，如果为NULL则代表未被pinned或hidden, 默认为None
-            默认排在最后，也就是传入None的时候
-
-        Returns
-        -------
-        Chart : Chart
-            置顶的图表
-        """
-        chart: Chart = cls.get(cls.id == id)
-        chart.status = 1
-        if sort is None:
-            # 判断当前实验/项目下有多少个hidden的chart
-            project_id = None if chart.project_id is None else chart.project_id.id
-            experiment_id = None if chart.experiment_id is None else chart.experiment_id.id
-            # 获取当前namespace下的最大索引,如果没有，则为0
-            sort = (
-                cls.select(fn.Max(cls.sort))
-                .where(cls.project_id == project_id, cls.experiment_id == experiment_id, cls.status == 1)
-                .scalar()
-            )
-            sort = 0 if sort is None else sort + 1
-        chart.sort = sort
-        chart.save()
-        return chart
-
-    @classmethod
-    def restore(cls, id: int) -> "Chart":
-        """
-        把某个chart恢复正常状态
-
-        Parameters
-        ----------
-        id : int
-            图表id
-
-        Returns
-        -------
-        Chart : Chart
-            恢复的图表
-        """
-        chart: Chart = cls.get(cls.id == id)
-        chart.status = 0
-        chart.sort = None
-        chart.save()
-        return chart
-
-    @classmethod
-    def hide(cls, id: int, sort: int = None) -> "Chart":
-        """
-        把某个chart隐藏
-
-        Parameters
-        ----------
-        id : int
-            图表id
-        sort : int
-            图表在被pinned或hidden时的排序，越小越靠前，越大越靠后，如果为NULL则代表未被pinned或hidden, 默认为None
-            默认排在最后，也就是传入None的时候
-
-        Returns
-        -------
-        Chart : Chart
-            隐藏的图表
-        """
-        chart: Chart = cls.get(cls.id == id)
-        chart.status = -1
-        if sort is None:
-            # 判断当前实验/项目下有多少个hidden的chart
-            project_id = None if chart.project_id is None else chart.project_id.id
-            experiment_id = None if chart.experiment_id is None else chart.experiment_id.id
-            # 获取当前namespace下的最大索引,如果没有，则为0
-            sort = (
-                cls.select(fn.Max(cls.sort))
-                .where(cls.project_id == project_id, cls.experiment_id == experiment_id, cls.status == -1)
-                .scalar()
-            )
-            sort = 0 if sort is None else sort + 1
-        chart.sort = sort
-        chart.save()
-        return chart
diff --git a/swanlab/db/models/displays.py b/swanlab/db/models/displays.py
deleted file mode 100644
index 45cfc7b8..00000000
--- a/swanlab/db/models/displays.py
+++ /dev/null
@@ -1,113 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-18 20:43:32
-@File: swanlab\db\models\displays.py
-@IDE: vscode
-@Description:
-    中间表，namespace 和 chart，设计排序和排序可变
-"""
-from ..model import SwanModel
-from peewee import CharField, IntegerField, ForeignKeyField, TextField, IntegrityError, Check, fn, DatabaseProxy
-from ..error import ExistedError, ForeignChartNotExistedError, ForeignNameNotExistedError
-from .charts import Chart
-from .namespaces import Namespace
-
-
-class Display(SwanModel):
-    """namespace 和 chart，设计排序和排序可变"""
-
-    class Meta:
-        database = DatabaseProxy()
-        # chart_id和namespace_id加起来唯一
-        indexes = ((("chart_id", "namespace_id"), True),)
-        # 写入check约束，sort必须大于等于0
-        constraints = [Check("sort >= 0")]
-
-    id = IntegerField(primary_key=True)
-    """display表唯一id"""
-    chart_id = ForeignKeyField(Chart, backref="displays", on_delete="CASCADE", on_update="CASCADE", null=False)
-    """关联的chart，不可为空"""
-    namespace_id = ForeignKeyField(Namespace, backref="displays", on_delete="CASCADE", on_update="CASCADE", null=False)
-    """关联的namespace，不可为空"""
-    sort = IntegerField()
-    """当前chart在namespace下的排序，索引越小，排序越靠前，索引>=0"""
-    more = TextField(default=None, null=True)
-    """更多信息配置，json格式，将在表函数中检查并解析"""
-    create_time = CharField(max_length=30, null=False)
-    """创建时间"""
-    update_time = CharField(max_length=30, null=False)
-    """更新时间"""
-
-    def __dict__(self):
-        return {
-            "id": self.id,
-            "chart_id": self.chart_id,
-            "namespace_id": self.namespace_id,
-            "sort": self.sort,
-            "more": self.more,
-            "create_time": self.create_time,
-            "update_time": self.update_time,
-        }
-
-    @classmethod
-    def create(
-        cls,
-        chart_id: int,
-        namespace_id: int,
-        sort: int = None,
-        more: dict = None,
-    ) -> "Display":
-        """添加行数据
-
-        Parameters
-        ----------
-        chart_id : int
-            chart表中的id
-        namespace_id : int
-            namespace表中的id
-        sort : int
-            排序索引，索引越小，排序越靠前，索引>=0，如果为None，则自动加到最后
-        more : str
-            更多信息配置，json格式，将在表函数中检查并解析
-
-        Returns
-        -------
-        Display
-            返回创建的display对象
-
-        Raises
-        ------
-        ForeignChartNotExistedError
-            外键对应的id不存在(chart不存在)
-        ForeignNameNotExistedError
-            外键对应的id不存在(namespace不存在)
-        ExistedError
-           "chart_id" 和 "namespace_id" 的对应关系已存在
-        """
-
-        # 检查外键存在性
-        if not Chart.filter(Chart.id == chart_id).exists():
-            raise ForeignChartNotExistedError("图表不存在")
-        if not Namespace.filter(Namespace.id == namespace_id).exists():
-            raise ForeignNameNotExistedError("命名空间不存在")
-
-        # 如果sort为None，则自动添加到最后
-        if sort is None:
-            # 获取当前namespace下的最大索引,如果没有，则为0
-            sort = cls.select(fn.Max(cls.sort)).where(cls.namespace_id == namespace_id).scalar()
-            sort = 0 if sort is None else sort + 1
-        elif sort >= 0 and cls.filter(cls.sort == sort).exists():
-            # 先判断当前 sort 是否存在，如果存在，则将大于等于 sort 的索引加1，这样可以直接将数据插入
-            if cls.filter(cls.sort == sort).exists():
-                cls.update(sort=cls.sort + 1).where(cls.sort >= sort).execute()
-
-        try:
-            return super().create(
-                chart_id=chart_id,
-                namespace_id=namespace_id,
-                sort=sort,
-                more=cls.dict2json(more),
-            )
-        except IntegrityError:
-            raise ExistedError("display对应关系已存在")
diff --git a/swanlab/db/models/experiments.py b/swanlab/db/models/experiments.py
deleted file mode 100644
index 75eac42b..00000000
--- a/swanlab/db/models/experiments.py
+++ /dev/null
@@ -1,208 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-17 14:56:38
-@File: swanlab\db\modules\experiment.py
-@IDE: vscode
-@Description:
-    实验数据表
-"""
-import os.path
-
-from ..model import SwanModel
-from peewee import ForeignKeyField, CharField, IntegerField, TextField, IntegrityError, Check, DatabaseProxy
-from .projects import Project
-from ..error import ExistedError, NotExistedError, ForeignProNotExistedError
-from swanlab.package import get_package_version
-from ...utils import generate_color
-from ...utils import create_time
-from swanlab.env import get_swanlog_dir
-import shutil
-
-
-# 定义模型类
-class Experiment(SwanModel):
-    """实验表
-    """
-
-    # 实验运行时状态符
-    RUNNING_STATUS = 0
-    # 实验停止时状态符
-    STOPPED_STATUS = -1
-    # 实验结束时状态符
-    FINISHED_STATUS = 1
-
-    class Meta:
-        database = DatabaseProxy()
-        # 通过meta规定name和project_id的唯一性
-        indexes = ((("name", "project_id"), True), (("sort", "project_id"), True))
-        # sort必须大于等于0
-        constraints = [Check("sort >= 0")]
-
-    id = IntegerField(primary_key=True)
-    """实验id"""
-    project_id = ForeignKeyField(Project, backref="experiments", on_delete="CASCADE", on_update="CASCADE", default=1)
-    """外键，项目id，可通过此外键反向查询项目下的所有实验"""
-
-    run_id = TextField(unique=True)
-    """运行时id，用于区分不同的实验保存的文件夹名称"""
-
-    name = CharField(max_length=100, null=False)
-    """实验名称，通过meta规定name和project_id的唯一性"""
-
-    description = CharField(max_length=255, null=True)
-    """实验描述"""
-
-    sort = IntegerField(null=False)
-    """实验索引，用于排序，通过meta规定index和project_id的唯一性"""
-
-    status = IntegerField(choices=[-1, 0, 1], default=0)
-    """实验状态，-1: crushed, 0: running, 1: finished，在创建一个实验时，状态默认为0"""
-
-    show = IntegerField(default=1, choices=[0, 1])
-    """实验可见性，0: 不可见，1: 可见"""
-
-    light = CharField(max_length=20, null=True)
-    """亮色主题颜色"""
-    dark = CharField(max_length=20, null=True)
-    """暗色主题颜色"""
-
-    pinned_opened = IntegerField(default=1, choices=[0, 1])
-    """实验图表置顶部分是否打开，默认值为1，表示打开"""
-
-    hidden_opened = IntegerField(default=0, choices=[0, 1])
-    """实验图表隐藏部分是否打开，默认值为0,表示关闭"""
-
-    more = TextField(null=True)
-    """更多信息配置，json格式，将在表函数中检查并解析"""
-
-    version = CharField(max_length=30, null=False)
-    """创建时的版本号"""
-
-    create_time = CharField(max_length=30, null=False)
-    """创建时间"""
-
-    finish_time = CharField(max_length=30, null=True, default=None)
-    """实验结束时间"""
-
-    update_time = CharField(max_length=30, null=False)
-    """更新的时间"""
-
-    def __dict__(self):
-        return {
-            "id": self.id,
-            "project_id": self.project_id.__dict__(),
-            "run_id": self.run_id,
-            "name": self.name,
-            "description": self.description,
-            "sort": self.sort,
-            "status": self.status,
-            "show": self.show,
-            "more": self.more,
-            "version": self.version,
-            "create_time": self.create_time,
-            "update_time": self.update_time,
-            "finish_time": self.finish_time,
-        }
-
-    @classmethod
-    def create(
-        cls,
-        name: str,
-        run_id: str,
-        description: str = None,
-        project_id: int = Project.DEFAULT_PROJECT_ID,
-        num: int = None,
-        more: dict = None,
-    ) -> "Experiment":
-        """覆写继承的create方法，创建一个新的实验
-
-        Parameters
-        ----------
-        name : str
-            实验名称
-        run_id : str
-            运行时信息
-        description : str
-            实验描述，默认为空字符串
-        project_id : int, optional
-            关联的项目id，由于目前为单项目模式，所以不需要手动设置此字段，默认置为DEFAULT_PROJECT_ID
-        num : int, optional
-            历史实验数量，如果不指定则自动计算
-        more : dict, optional
-            更多配置，在函数内部会转换为字符串格式
-
-        Returns
-        -------
-        Experiment
-            新建的实验实例
-
-        Raises
-        -------
-        ForeignProNotExistedError
-            项目不存在
-        ExistedError
-            实验已经存在
-        """
-        # 检查项目是否存在
-        if not Project.select().where(Project.id == project_id).exists():
-            raise ForeignProNotExistedError("项目不存在")
-        # 这个sum是+1以后的值
-        _sum = Project.increase_sum(project_id)
-        # 调用父类的create方法创建实验实例
-        light, dark = generate_color(_sum if num is None else num + 1)
-        try:
-            return super().create(
-                name=name,
-                run_id=run_id,
-                project_id=project_id,
-                description=description,
-                more=cls.dict2json(more),
-                sort=_sum,
-                version=get_package_version(),
-                light=light,
-                dark=dark,
-            )
-        except IntegrityError:
-            raise ExistedError("实验已经存在")
-
-    @classmethod
-    def get(cls, *args, **kwargs) -> "Experiment":
-        try:
-            return super().get(*args, **kwargs)
-        except:
-            raise NotExistedError("Experiment does not exist: {}".format(kwargs))
-
-    def update_status(self, status: int):
-        """更新实验状态
-
-        Parameters
-        ----------
-        status : int
-            实验状态
-        """
-        self.status = status
-        self.finish_time = create_time()
-        self.save()
-
-    @classmethod
-    def purely_delete(cls, run_id: str):
-        """
-        删除某一个实验，顺便删除实验的文件夹
-
-        Parameters
-        ----------
-        run_id : int
-            实验run_id
-        """
-        # 获取实验实例
-        try:
-            exp = cls.get(run_id=run_id)
-            # 删除实验
-            exp.delete_instance()
-            # 更新项目的实验数量
-            Project.decrease_sum()
-            # 删除实验文件夹
-            shutil.rmtree(os.path.join(get_swanlog_dir(), exp.run_id.__str__()))
-        except NotExistedError:
-            shutil.rmtree(os.path.join(get_swanlog_dir(), run_id))
diff --git a/swanlab/db/models/namespaces.py b/swanlab/db/models/namespaces.py
deleted file mode 100644
index 20085d1c..00000000
--- a/swanlab/db/models/namespaces.py
+++ /dev/null
@@ -1,164 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-18 15:12:37
-@File: swanlab\db\models\namespace.py
-@IDE: vscode
-@Description:
-    实验表格命名空间：一个实验/项目下可以有多个命名空间
-"""
-from ..model import SwanModel
-from peewee import CharField, IntegerField, ForeignKeyField, TextField, IntegrityError, Check, fn, DatabaseProxy
-from ..error import ExistedError, ForeignProNotExistedError, ForeignExpNotExistedError
-from .experiments import Experiment
-from .projects import Project
-
-
-class Namespace(SwanModel):
-    """命名空间表
-
-    Parameters
-    ----------
-    SwanModel : Class
-        自建基类
-    """
-
-    class Meta:
-        database = DatabaseProxy()
-
-        """
-        name 和 experiment_id 和 project_id 组合后需要确保唯一性
-        """
-
-        # 同一个项目/实验下，命名空间名称不能重复
-        indexes = ((("name", "experiment_id"), True), (("name", "project_id"), True))
-
-        constraints = [
-            # project_id 和 experiment_id 不能同时为空，也不能同时不为空
-            Check(
-                "(project_id IS NULL AND experiment_id IS NOT NULL)"
-                " OR "
-                "(project_id IS NOT NULL AND experiment_id IS NULL)"
-            ),
-            # index必须大于等于0
-            Check("sort >= 0"),
-        ]
-
-    id = IntegerField(primary_key=True)
-    """namespace唯一id"""
-    experiment_id = ForeignKeyField(
-        Experiment, backref="namespaces", on_delete="CASCADE", on_update="CASCADE", null=True
-    )
-    """外键，对应的实验id，与project_id只能有一个为null"""
-    project_id = ForeignKeyField(Project, backref="namespaces", on_delete="CASCADE", on_update="CASCADE", null=True)
-    """外键，对应的项目id，与experiment_id只能有一个为null"""
-    name = CharField(max_length=100, null=False)
-    """这个命名空间的名称，同一个项目/实验下，命名空间名称不能重复"""
-    description = CharField(max_length=255, null=True)
-    """命名空间描述，可为空"""
-    sort = IntegerField()
-    """命名空间索引，用于排序，同一个项目/实验下，命名空间索引不能重复，索引越小，排序越靠前，索引>=0"""
-    opened = IntegerField(default=1, choices=[0, 1])
-    """命名空间是否已经被打开，默认为1，表示已经被打开，0表示未被打开"""
-    more = TextField(default=None, null=True)
-    """更多信息配置，json格式，将在表函数中检查并解析"""
-    create_time = CharField(max_length=30, null=False)
-    """创建时间"""
-    update_time = CharField(max_length=30, null=False)
-    """更新的时间"""
-
-    def __dict__(self):
-        return {
-            "id": self.id,
-            "experiment_id": self.experiment_id.__dict__(),
-            "project_id": self.project_id,
-            "name": self.name,
-            "description": self.description,
-            "sort": self.sort,
-            "more": self.more,
-            "create_time": self.create_time,
-            "update_time": self.update_time,
-        }
-
-    @classmethod
-    def create(
-        cls,
-        name: str,
-        experiment_id: int = None,
-        project_id: int = None,
-        description: str = None,
-        sort=None,
-        more: dict = None,
-    ) -> "Namespace":
-        """创建命名空间
-
-        Parameters
-        ----------
-        name : str
-            命名空间名称
-        experiment_id : int, optional
-            实验id, 默认为None，但是实验id和项目id不能同时为None，也不能同时不为None
-        project_id : int, optional
-            项目id, 默认为None，但是实验id和项目id不能同时为None，也不能同时不为None
-        description : str, optional
-            命名空间描述, 默认为""
-        sort : int, optional
-            命名空间索引, >=0, 越小排在越前面，默认为None，自动添加到最后
-            如果指定了sort，那么将会把其他sort大于等于指定值的命名空间索引+1
-
-        Returns
-        -------
-        Namespace : Namespace
-            创建的命名空间
-
-        Raises
-        ------
-        ForeignProNotExistedError
-            外键对应的id不存在(project不存在)
-        ForeignExpNotExistedError
-            外键对应的id不存在(experiment不存在)
-        ExistedError
-           同名命名空间不可存在于同一个实验/项目中
-        """
-
-        # 检查外键存在性
-        if experiment_id and not Experiment.filter(Experiment.id == experiment_id).exists():
-            raise ForeignExpNotExistedError("实验不存在")
-        if project_id and not Project.filter(Project.id == project_id).exists():
-            raise ForeignProNotExistedError("项目不存在")
-
-        if sort is None:
-            # 如果没有指定sort，那么就自动添加到最后
-            # 寻找当前实验/项目下的最大索引，如果实验存在就在实验下找，如果实验不存在就在项目下找
-            if experiment_id is not None:
-                sort = cls.select(fn.Max(cls.sort)).where(cls.experiment_id == experiment_id).scalar()
-            else:
-                sort = cls.select(fn.Max(cls.sort)).where(cls.project_id == project_id).scalar()
-            sort = 0 if sort is None else sort + 1
-        elif sort >= 0:
-            # 如果指定了sort，那么将会把其他sort大于等于指定值的命名空间索引+1
-            if experiment_id is not None:
-
-                cls.update(sort=cls.sort + 1).where(
-                    cls.experiment_id == experiment_id,
-                    cls.name != name,
-                    cls.sort >= sort,
-                ).execute()
-            else:
-                cls.update(sort=cls.sort + 1).where(
-                    cls.project_id == project_id,
-                    cls.name != name,
-                    cls.sort >= sort,
-                ).execute()
-
-        try:
-            return super().create(
-                name=name,
-                experiment_id=experiment_id,
-                project_id=project_id,
-                description=description,
-                sort=sort,
-                more=more,
-            )
-        except IntegrityError:
-            raise ExistedError("命名空间已存在")
diff --git a/swanlab/db/models/projects.py b/swanlab/db/models/projects.py
deleted file mode 100644
index 6809aabb..00000000
--- a/swanlab/db/models/projects.py
+++ /dev/null
@@ -1,135 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-16 10:54:30
-@File: swanlab\db\modules\project.py
-@IDE: vscode
-@Description:
-    项目表，对应于0.1.5之前版本中的 project.json
-"""
-from peewee import CharField, IntegerField, DatabaseProxy
-from ..model import SwanModel
-from swanlab.package import get_package_version
-
-
-class Project(SwanModel):
-    """项目表
-    目前，在一个工程中，只有一个项目
-    """
-
-    # 默认的项目id应该是1
-    DEFAULT_PROJECT_ID = 1
-
-    class Meta:
-        database = DatabaseProxy()
-
-    id = IntegerField(primary_key=True)
-    """项目id"""
-    name = CharField(max_length=100, null=False, unique=True)
-    """项目名称，唯一"""
-    description = CharField(max_length=255, null=True)
-    """项目描述，可为空"""
-    sum = IntegerField(null=True)
-    """项目下实验数量，包括已删除的实验，这是一个只增不减的值"""
-    charts = IntegerField(default=0, choices=[0, 1], null=False)
-    """是否已经生成项目级别图表，0 未生成，1 已生成"""
-    pinned_opened = IntegerField(default=1, choices=[0, 1])
-    """多实验图表置顶部分是否打开，默认值为1，表示打开"""
-    hidden_opened = IntegerField(default=0, choices=[0, 1])
-    """多实验图表隐藏部分是否打开，默认值为0,表示关闭"""
-    more = CharField(null=True)
-    """更多信息配置，json格式"""
-    version = CharField(max_length=30, null=False)
-    """创建时的版本号"""
-    create_time = CharField(max_length=30, null=False)
-    """创建时间"""
-    update_time = CharField(max_length=30, null=False)
-    """更新时间"""
-
-    def __dict__(self):
-        return {
-            "id": self.id,
-            "name": self.name,
-            "description": self.description,
-            "sum": self.sum,
-            "charts": self.charts,
-            "more": self.more,
-            "pinned_opened": self.pinned_opened,
-            "hidden_opened": self.hidden_opened,
-            "version": self.version,
-            "create_time": self.create_time,
-            "update_time": self.update_time,
-        }
-
-    @classmethod
-    def init(cls, name="", description=None, more="") -> "Project":
-        """
-        静态方法
-        初始化项目表，如果已经有项目存在，则不创建，直接返回第一条数据实例
-
-        Parameters
-        ----------
-        name : str, optional
-            项目名称，默认为空字符串
-        description : str, optional
-            项目描述，默认为空字符串
-        more : str, optional
-            更多信息配置，默认为空字符串
-
-        Returns
-        -------
-        Project:
-            项目实例
-        """
-        # 如果已经初始化，则不创建，直接返回第一条数据实例
-        if cls.select().count() >= 1:
-            return cls.filter(cls.id == cls.DEFAULT_PROJECT_ID)[0]
-        # 创建项目
-        return cls.create(
-            name=name,
-            description=description,
-            more=more,
-            version=get_package_version(),
-        )
-
-    @classmethod
-    def increase_sum(cls, id=DEFAULT_PROJECT_ID) -> int:
-        """
-        静态方法
-        更新实验统计数量，增加1
-        此方法通常在创建实验时被Experiment类调用
-        Parameters
-        ----------
-        id : int
-            实验id, 默认为DEFAULT_PROJECT_ID
-
-        Returns
-        -------
-        int:
-            当前实验统计数量
-        """
-        project: "Project" = cls.filter(cls.id == id)[0]
-        project.sum = project.sum + 1 if project.sum else 1
-        project.save()
-        return project.sum
-
-    @classmethod
-    def decrease_sum(cls, id=DEFAULT_PROJECT_ID) -> int:
-        """
-        静态方法
-        更新实验统计数量，减少1
-        此方法通常在删除实验时被Experiment类调用
-        Parameters
-        ----------
-        id : int
-            实验id, 默认为DEFAULT_PROJECT_ID
-
-        Returns
-        -------
-        int:
-            当前实验统计数量
-        """
-        project: "Project" = cls.filter(cls.id == id)[0]
-        project.sum = project.sum - 1 if project.sum else 0
-        project.save()
-        return project.sum
diff --git a/swanlab/db/models/sources.py b/swanlab/db/models/sources.py
deleted file mode 100644
index 8e7a9e45..00000000
--- a/swanlab/db/models/sources.py
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-18 20:36:01
-@File: swanlab\db\models\sources.py
-@IDE: vscode
-@Description:
-    中间表，多chart多tag
-"""
-from .tags import Tag
-from .charts import Chart
-from ..model import SwanModel
-from peewee import CharField, IntegerField, ForeignKeyField, TextField, IntegrityError, DatabaseProxy, fn
-from ..error import ExistedError, ForeignTagNotExistedError, ForeignChartNotExistedError
-
-
-class Source(SwanModel):
-    """多chart多tag"""
-
-    class Meta:
-        database = DatabaseProxy()
-        # 写入check约束，name和experiment_id加起来唯一
-        indexes = ((("tag_id", "chart_id"), True),)
-
-    id = IntegerField(primary_key=True)
-    """此表唯一id"""
-    tag_id = ForeignKeyField(Tag, backref="sources", null=False, on_delete="CASCADE", on_update="CASCADE")
-    """代表关联的tag，不可为空"""
-    chart_id = ForeignKeyField(Chart, backref="sources", null=False, on_delete="CASCADE", on_update="CASCADE")
-    """关联的chart，不可为空"""
-    sort = IntegerField(default=0)
-    """此tag在此chart中的排序，值越小越靠前"""
-    error = TextField(null=True)
-    """代表这个tag数据转化到chart上失败了，错误信息，是一个json字符串"""
-    more = TextField(null=True)
-    """更多信息配置，json格式，将在表函数中检查并解析"""
-    create_time = CharField(max_length=30, null=False)
-    """创建时间"""
-    update_time = CharField(max_length=30, null=False)
-    """更新时间"""
-
-    def __dict__(self):
-        return {
-            "id": self.id,
-            "tag_id": self.tag_id,
-            "chart_id": self.chart_id,
-            "sort": self.sort,
-            "error": self.error,
-            "more": self.more,
-            "create_time": self.create_time,
-            "update_time": self.update_time,
-        }
-
-    @classmethod
-    def create(
-        cls,
-        tag_id: int,
-        chart_id: int,
-        error: dict = None,
-        sort: int = None,
-        more: dict = None,
-    ) -> "Source":
-        """添加行数据
-
-        Parameters
-        ----------
-        tag_id : int
-            对应的tag_id
-        chart_id : int
-            对应的chart_id
-        error : str, optional
-            对应的错误信息, 默认为None
-        sort : int, optional
-            排序索引，索引越小，排序越靠前，索引>=0，如果为None，则自动加到最后
-        more : str, optional
-            更多信息，json格式
-
-        Raises
-        ------
-        ForeignTagNotExistedError
-            外键对应的id不存在(tag不存在)
-        ForeignChartNotExistedError
-            外键对应的id不存在(chart不存在)
-        ExistedError
-           对应关系已经在数据库中存在（"tag_id"和"chart_id"唯一）
-        """
-
-        # 检查外键存在性
-        if not Tag.filter(Tag.id == tag_id).exists():
-            raise ForeignTagNotExistedError("tag不存在")
-        if not Chart.filter(Chart.id == chart_id).exists():
-            raise ForeignChartNotExistedError("chart不存在")
-
-        # 如果sort为None，则自动添加到最后
-        if sort is None:
-            sort = Source.select(fn.Max(Source.sort)).where(Source.chart_id == chart_id).scalar()
-            sort = sort + 1 if sort is not None else 0
-        else:
-            # 如果sort不为None，则检查当前chart下的sort是否存在，如果存在，则把sort大于等于sort的索引+1
-            cls.update(sort=sort + 1).where(cls.chart_id == chart_id, cls.sort >= sort).execute()
-
-        try:
-            return super().create(
-                tag_id=tag_id,
-                chart_id=chart_id,
-                error=cls.dict2json(error),
-                more=cls.dict2json(more),
-                sort=sort,
-            )
-        except IntegrityError:
-            raise ExistedError("source对应关系已存在")
diff --git a/swanlab/db/models/tags.py b/swanlab/db/models/tags.py
deleted file mode 100644
index e3da9617..00000000
--- a/swanlab/db/models/tags.py
+++ /dev/null
@@ -1,121 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-17 20:43:22
-@File: swanlab\db\modules\tag.py
-@IDE: vscode
-@Description:
-    实验 tag 表
-"""
-from ..model import SwanModel
-from peewee import ForeignKeyField, CharField, TextField, IntegerField, IntegrityError, DatabaseProxy, fn
-from ..error import ExistedError, ForeignExpNotExistedError
-from .experiments import Experiment
-
-
-class Tag(SwanModel):
-    """实验tag数据表
-
-    Parameters
-    ----------
-    SwanModel : class
-        自封装类
-    """
-
-    class Meta:
-        database = DatabaseProxy()
-        # 写入check约束，name和experiment_id加起来唯一
-        indexes = ((("name", "experiment_id"), True),)
-
-    id = IntegerField(primary_key=True)
-    """tag唯一id"""
-    # backref是反向引用，可以通过实验获取tag，比如experiment.tags获取此实验下的所有tag
-    experiment_id = ForeignKeyField(Experiment, backref="tags", null=False, on_delete="CASCADE", on_update="CASCADE")
-    """tag对应的实验id，代表这个tag由谁创建"""
-    name = CharField(max_length=255, null=False)
-    """tag名称，同一个实验下，tag名称不能重复"""
-    type = CharField(max_length=10, null=False)
-    """tag的类型"""
-    description = CharField(max_length=100, null=True)
-    """tag的描述，可为空"""
-    system = IntegerField(default=0, choices=[0, 1])
-    """标识这个tag数据由系统生成还是用户生成，0: 用户生成，1: 系统生成，默认为0"""
-    sort = IntegerField(default=0)
-    """tag在实验中的排序，值越小越靠前"""
-    more = TextField(null=True)
-    """更多信息配置，json格式，将在表函数中检查并解析"""
-    create_time = CharField(max_length=30, null=False)
-    """tag的创建时间"""
-    update_time = CharField(max_length=30, null=False)
-    """tag的更新时间"""
-
-    def __dict__(self):
-        return {
-            "id": self.id,
-            "experiment_id": self.experiment_id,
-            "name": self.name,
-            "description": self.description,
-            "system": self.system,
-            "more": self.more,
-            "create_time": self.create_time,
-            "update_time": self.update_time,
-        }
-
-    @classmethod
-    def create(
-        cls,
-        experiment_id: int,
-        name: str,
-        type: str,
-        description="",
-        system: int = 0,
-        more: dict = None,
-    ) -> "Tag":
-        """在tags表中创建一条tag指定实验的tag数据
-
-        Parameters
-        ----------
-        experiment_id : int
-            这将成为tag数据的外键，指向实验表中的某一条实验数据
-        name : str
-            tag的名称，字符串类型，小于255个字符
-        type : str
-            tag的类型，字符串类型，小于10个字符
-        description : str, optional
-            tag的描述，字符串类型，小于100个字符, 默认为空字符串
-        system : int, optional
-            判断是否是系统自动生成的tag，默认为否
-        more : str, optional
-            这必须是字典格式数据，代表更多信息, 默认为空字典
-
-        Returns
-        -------
-        Tag
-            返回创建的tag数据实例，可以通过实例获取tag数据
-
-        Raises
-        ------
-        ForeignExpNotExistedError
-            实验对应的id不存在
-        ExistedError
-            此tag已经在数据库中存在（tag-experiment_id唯一）
-        """
-        # 如果实验id不存在，则抛出异常
-        if not Experiment.filter(Experiment.id == experiment_id).exists():
-            raise ForeignExpNotExistedError("experiment不存在")
-        # 获取当前实验下tag的最大排序索引，如果没有则为0
-        sort = Tag.select(fn.Max(Tag.sort)).where(Tag.experiment_id == experiment_id).scalar()
-        sort = sort + 1 if sort is not None else 0
-        # 尝试创建实验tag，如果已经存在则抛出异常
-        try:
-            return super().create(
-                experiment_id=experiment_id,
-                name=name,
-                type=type,
-                description=description,
-                system=system,
-                sort=sort,
-                more=more,
-            )
-        except IntegrityError:
-            raise ExistedError("tag已存在")
diff --git a/swanlab/db/table_config.py b/swanlab/db/table_config.py
deleted file mode 100644
index 929d800e..00000000
--- a/swanlab/db/table_config.py
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-02-05 17:03:54
-@File: swanlab/db/table_config.py
-@IDE: vscode
-@Description:
-    数据库表格配置
-"""
-from .models import (
-    Project,
-    Experiment,
-    Tag,
-    Chart,
-    Namespace,
-    Source,
-    Display,
-)
-
-tables = [Project, Experiment, Tag, Chart, Namespace, Source, Display]
diff --git a/swanlab/db/utils/__init__.py b/swanlab/db/utils/__init__.py
deleted file mode 100644
index 258186a6..00000000
--- a/swanlab/db/utils/__init__.py
+++ /dev/null
@@ -1,1 +0,0 @@
-from .chart import add_multi_chart
diff --git a/swanlab/db/utils/chart.py b/swanlab/db/utils/chart.py
deleted file mode 100644
index ebf1a5f6..00000000
--- a/swanlab/db/utils/chart.py
+++ /dev/null
@@ -1,231 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-02-03 16:00:13
-@File: swanlab\db\utils\chart.py
-@IDE: vscode
-@Description:
-    关于表格的一些特殊处理
-    这里的函数可能在 server、data 中都有作用
-"""
-
-from peewee import fn
-from ..models import (
-    Project,
-    Namespace,
-    Tag,
-    Source,
-    Chart,
-    Display,
-)
-
-from ..db_connect import connect
-from ..error import (
-    NotExistedError,
-    ChartTypeError,
-)
-
-
-def transform_to_multi_exp_charts(project_id: int):
-    """兼容以前没有多实验对比数据的情况
-    根据早期版本的数据创建多实验对比图表
-
-    Parameters
-    ----------
-    project_id : int
-        project 表 id
-
-    Raises
-    ------
-    NotExistedError
-        project 不存在
-    IndexError
-        project 已经生成过多实验图表
-    NotExistedError
-        没有满足多实验图表的 tag
-    """
-
-    project = Project.filter(Project.id == project_id)
-    if project.count() == 0:
-        raise NotExistedError("Target project does not exist")
-
-    if project.first().charts == 1:
-        raise IndexError("project.charts must be 0, which means no multiple charts")
-
-    # ---------------------------------- 未生成多实验图表 ----------------------------------
-
-    # 1. 查找
-    # - 找到所有满足条件的 tag : tag 名在多个实验中出现
-    # - tag 名一样，但类型不一样，以第一个被找到的 tag 类型为基准
-    # 2. 生成
-    # - 生成 namespace
-    # - 生成 chart
-    # - 添加 chart 和 namespace 到 display
-    # - 生成 source
-    # 3. 标记
-    # - 设置 project 中字段 charts 为 1，表示已经生成多实验表格
-
-    # 根据名称和类型分类获取 tag
-    tags_with_same_name = Tag.select(Tag.name, Tag.type, fn.COUNT(Tag.id).alias("count")).group_by(Tag.name, Tag.type)
-    tags = [{"name": tag.name, "type": tag.type} for tag in tags_with_same_name]
-    # 如果没有满足多实验图表生成条件的 tag，抛出异常
-    # 这是一个非预期异常，因为按道理不会出现这种情况,这种情况也不会写到文档中，仅仅是调试方便
-    if len(tags) == 0:
-        raise ValueError("No mutiple experment charts found")
-    # 用于收集满足多实验图表生成条件的 tag
-    result_lists = {}
-    # 遍历查询，获取满足多实验图表生成条件的 tag
-    for tag in tags:
-        # 如果当前 tag 名已经存在收集列表，说明有同名不同类的 tag 都满足多图表条件，目前只取第一次出现的 type，即后续有同名不同类的 tag 都忽略
-        if tag["name"] in result_lists and not tag["type"] == Tag.filter(Tag.name == tag["name"]).first().type:
-            continue
-        # 获取当前name的所有数据行
-        target = Tag.filter(Tag.name == tag["name"], Tag.type == tag["type"])
-        # 将数据行添加到结果列表
-        result_lists[tag["name"]] = [
-            {
-                "experiment_id": item["experiment_id"]["id"],
-                "experiment_name": item["experiment_id"]["name"],
-                "tag_id": item["id"],
-            }
-            for item in Project.search2list(target)
-        ]
-    # 至此 result_list 数据结构为：{ tag_name: [{experiment_id, experiment_name, tag_id}] }，找到了所有可以生成多实验图表的 tag
-    # 查找过滤后，需要生成多实验图表，生成操作需要使用原子操作
-    db = connect()
-    with db.atomic():
-        for tag_name in result_lists:
-            tag_list = result_lists[tag_name]
-            # 1. 获取/生成 namespace
-            # 获取 tag 对应的伴生 namespace 名
-            accompanying_chart = Chart.filter(
-                Chart.name == tag_name, Chart.experiment_id == tag_list[0]["experiment_id"], Chart.system != 0
-            ).first()
-            ns_name = Display.filter(Display.chart_id == accompanying_chart.id).first().namespace_id.name
-            # 如果属于项目的同名 namespace 已经存在，直接使用这个
-            namespaces = Namespace.filter(Namespace.name == ns_name, Namespace.project_id == project_id)
-            if namespaces.count():
-                namespace = namespaces.first()
-            # 没有该命名空间，需创建
-            else:
-                namespace = Namespace.create(ns_name, project_id=project_id)
-            # 2. 生成图表
-            chart_type = Source.filter(Source.tag_id == tag_list[0]["tag_id"]).first().chart_id.type
-            chart = Chart.create(
-                tag_name,
-                experiment_id=None,
-                project_id=project_id,
-                system=0,
-                type=chart_type,
-            )
-            # 3. 添加 chart 和 namespace 到 display
-            Display.create(chart_id=chart.id, namespace_id=namespace.id)
-            # 4. 生成source
-            sources = [
-                {
-                    "tag_id": item["tag_id"],
-                    "chart_id": chart.id,
-                    "sort": index,
-                    "error": Source.filter(Source.tag_id == item["tag_id"]).first().error,
-                }
-                for index, item in enumerate(tag_list)
-            ]
-            Source.insert_many(sources).execute()
-        # 最后将 project 表中的 charts 标注为 1，即已经生成多实验表格
-        Project.update(charts=1).where(Project.id == project_id).execute()
-    db.commit()
-
-
-def add_multi_chart(
-    tag_id: int,
-    chart_id: int,
-    project_id: int = None,
-):
-    """添加新的多实验对比图表
-    1. 当前 tag 已有多实验图表，将该 tag 添加 source 即可
-    2. 当前 tag 满足条件但未创建多实验图表，进行创建和添加
-
-    Parameters
-    ----------
-    tag_id : int
-        tag id
-    chart_id : int
-        tag 伴生 chart 的 id
-    project_id : int
-        项目 id, 由于目前为单项目，所以可以不传
-    Raises
-    ------
-    ChartTypeError
-        当前 tag 类型和期望 chart 类型不一致
-    IndexError
-        project 已经生成过多实验图表, 无法再次生成
-    """
-    if project_id is None:
-        project_id = Project.DEFAULT_PROJECT_ID
-
-    try:
-        project = Project.filter(Project.id == project_id).first()
-    except Exception:
-        raise NotExistedError("Target project does not exist")
-
-    # ---------------------------------- charts 为 0 说明还没有生成过多实验图表 ----------------------------------
-
-    if project.charts == 0:
-        return transform_to_multi_exp_charts(project_id)
-
-    # ---------------------------------- charts 为 1 说明已经生成过多实验图表 ----------------------------------
-    # 当前新增 tag
-    tag = Tag.get_by_id(tag_id)
-    # 属于项目的所有与 tag 同名的 chart，用于判断是否已经生成该 tag 的多实验对比图
-    charts = Chart.filter(Chart.project_id == project_id, Chart.name == tag.name)
-    chart = charts.first()
-
-    # 已经有对应名称的 chart
-    if charts.count() != 0:
-        # 获取图所属 tag 的类型
-        if chart.sources.first().tag_id.type != tag.type:
-            # 同名 chart 已生成，但是类型不满足
-            raise ChartTypeError("Error tag type")
-        else:
-            # 如果该 chart 和 tag 同类，直接添加
-            return Source.create(tag_id, chart.id, error=Source.filter(Source.tag_id == tag_id).first().error)
-
-    # 如果没有对应名称的 chart, 执行创建新多实验对比图表操作
-    # 此时该 tag 应该是第一次出现
-    # 获取/创建 namespace
-    # - 生成或者查找 namespace
-    # - 生成 chart
-    # - 添加 chart 和 namespace 到 display
-    # - 生成 source
-
-    db = connect()
-    with db.atomic():
-        # 1. 获取 namespace
-        name = Display.filter(Display.chart_id == chart_id).first().namespace_id.name
-        namespaces = Namespace.filter(Namespace.project_id == project_id, Namespace.name == name)
-        # 如果属于项目的同名 namespace 已经存在，直接使用这个
-        if Namespace.filter(Namespace.project_id == project_id, Namespace.name == name).count() > 0:
-            namespace: Namespace = namespaces.first()
-        # 没有该命名空间，需创建, 生成 namespace
-        else:
-            namespace: Namespace = Namespace.create(name, project_id=project_id)
-        # 2. 生成图表
-        accompanying_chart: Chart = Chart.get_by_id(chart_id)
-        # 通过 tag 的伴生图表获取部分信息
-        chart = Chart.create(
-            name=accompanying_chart.name,
-            project_id=project_id,
-            # 不是创建tag时自动生成的图表
-            system=0,
-            type=accompanying_chart.type,
-            reference=accompanying_chart.reference,
-        )
-        # 3. 添加 chart 和 namespace 到 display
-        Display.create(chart.id, namespace.id)
-        # 4. 生成 source
-        Source.create(
-            tag_id,
-            chart.id,
-            error=Source.filter(Source.tag_id == tag_id).first().error,
-        )
-    db.commit()
diff --git a/swanlab/env.py b/swanlab/env.py
index 265f0bd5..ff763cb9 100644
--- a/swanlab/env.py
+++ b/swanlab/env.py
@@ -9,7 +9,6 @@
 """
 import os
 from typing import MutableMapping, Optional
-from .utils.file import is_port, is_ipv4
 from .error import UnKnownSystemError
 import enum
 import sys
@@ -25,10 +24,10 @@
 """命令执行目录SWANLAB_LOG_DIR，日志文件存放在这个目录下，如果自动生成，则最后的目录名为swanlog，第一次调用时如果路径不存在，会自动创建路径"""
 
 PORT = "SWANLAB_SERVER_PORT"
-"""服务端口SWANLAB_SERVER_PORT，服务端口"""
+"""cli 服务端口"""
 
 HOST = "SWANLAB_SERVER_HOST"
-"""服务端口SWANLAB_SERVER_PORT，服务地址"""
+"""cli 服务地址"""
 
 DEV = "SWANLAB_DEV"
 """是否是开发模式SWANLAB_DEV，开发模式下会打印更多的日志信息，并且切换一些配置"""
@@ -100,7 +99,7 @@ def get_swanlog_dir(env: Optional[Env] = None) -> Optional[str]:
         target_type="folder",
         desc=(
             'The log file was not found in the default path "{path}". '
-            'Please use the "swanlab watch -l <LOG '
+            'Please use the "swanlab watch <LOG '
             'PATH>" command to specify the location of the log path."'.format(path=path)
             if path == default
             else 'SWANLAB_LOG_DIR must be an existing path, now is "{path}"'.format(path=path)
@@ -111,61 +110,6 @@ def get_swanlog_dir(env: Optional[Env] = None) -> Optional[str]:
     return path
 
 
-def get_server_port(env: Optional[Env] = None) -> Optional[int]:
-    """获取服务端口
-
-    Parameters
-    ----------
-    env : Optional[Env], optional
-        环境变量map,可以是任意实现了MutableMapping的对象, 默认将使用os.environ
-
-    Returns
-    -------
-    Optional[int]
-        服务端口
-    """
-    # 第一次调用时，从环境变量中提取，之后就不再提取，而是从缓存中提取
-    if _env.get(PORT) is not None:
-        return _env.get(PORT)
-    # 否则从环境变量中提取
-    if env is None:
-        env = os.environ
-    default: Optional[int] = 5092
-    port = env.get(PORT, default=default)
-    # 必须可以转换为整数，且在0-65535之间
-    if not is_port(port):
-        raise ValueError('SWANLAB_SERVER_PORT must be a port, now is "{port}"'.format(port=port))
-    _env[PORT] = int(port)
-    return _env.get(PORT)
-
-
-def get_server_host(env: Optional[Env] = None) -> Optional[str]:
-    """获取服务端口
-
-    Parameters
-    ----------
-    env : Optional[Env], optional
-        环境变量map,可以是任意实现了MutableMapping的对象, 默认将使用os.environ
-
-    Returns
-    -------
-    Optional[int]
-        服务端口
-    """
-    default: Optional[str] = "127.0.0.1"
-    # 第一次调用时，从环境变量中提取，之后就不再提取，而是从缓存中提取
-    if _env.get(HOST) is not None:
-        return _env.get(HOST)
-    # 否则从环境变量中提取
-    if env is None:
-        env = os.environ
-    _env[HOST] = env.get(HOST, default=default)
-    # 必须是一个ipv4地址
-    if not is_ipv4(_env.get(HOST)):
-        raise ValueError('SWANLAB_SERVER_HOST must be an ipv4 address, now is "{host}"'.format(host=_env.get(HOST)))
-    return _env.get(HOST)
-
-
 def is_dev(env: Optional[Env] = None) -> bool:
     """判断是否是开发模式
     此函数会在一开始就被package.py调用，所以不需要判断是否已经初始化
@@ -187,7 +131,7 @@ def is_dev(env: Optional[Env] = None) -> bool:
 # ---------------------------------- 初始化基础环境变量 ----------------------------------
 
 # 所有的初始化函数
-function_list = [get_mode, get_swanlog_dir, get_server_port, get_server_host]
+function_list = [get_mode, get_swanlog_dir]
 
 
 def init_env(env: Optional[Env] = None):
@@ -218,13 +162,6 @@ def is_strict_mode() -> bool:
     return get_mode() != SwanLabMode.DISABLED.value
 
 
-def get_db_path() -> Optional[str]:
-    """
-    获取数据库路径，这是一个计算变量，每次调用都会重新计算
-    """
-    return os.path.join(get_swanlog_dir(), "runs.swanlab")
-
-
 def is_windows() -> bool:
     """判断当前操作系统是否是windows还是类unix系统
     此外的系统会报错为 UnKnownSystemError
@@ -295,6 +232,9 @@ def assert_exist(path: str, target_type: str = None, ra: bool = True, desc: str
     :param ra: 文件不存在时是否抛出异常，非严格模式下强制不抛出，此参数无效，此参数只影响文件是否存在，不影响文件类型判断时抛出异常
     :param desc: 异常描述信息，非严格模式下强制不抛出，此参数无效
     :param t_desc: 文件类型描述信息，非严格模式下强制不抛出，此参数无效
+    :raises FileNotFoundError: 文件不存在时抛出异常
+    :raises NotADirectoryError: 文件夹是一个文件时抛出异常
+    :raises IsADirectoryError: 文件是一个文件夹时抛出异常
     """
     if not is_strict_mode():
         return os.path.exists(path)
diff --git a/swanlab/package.json b/swanlab/package.json
index 930f03ba..39e0008e 100644
--- a/swanlab/package.json
+++ b/swanlab/package.json
@@ -1,6 +1,6 @@
 {
   "name": "swanlab",
-  "version": "0.3.10",
+  "version": "0.3.11",
   "description": "",
   "python": "true",
   "host": {
diff --git a/swanlab/server/__init__.py b/swanlab/server/__init__.py
deleted file mode 100644
index 369e56fa..00000000
--- a/swanlab/server/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2023-11-24 20:36:33
-@File: swanlab/app/__init__.py
-@IDE: vscode
-@Description:
-        在此处引出swanlab的web服务器框架，名为SwanWeb以及一些神奇的路由配置，以完成在库最外层的函数式调用
-"""
-from ..env import init_env
-from ..log import install
-
-# 在此处完成环境变量的初始化
-init_env()
-# 日志安装
-install()
-
-# 导出app对象
-from .app import app
diff --git a/swanlab/server/app.py b/swanlab/server/app.py
deleted file mode 100644
index 82808c26..00000000
--- a/swanlab/server/app.py
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2023-11-30 20:47:18
-@File: swanlab\server\route.py
-@IDE: vscode
-@Description:
-    综合服务 api
-"""
-
-from fastapi import FastAPI
-from fastapi.staticfiles import StaticFiles
-from swanlab.package import get_package_version
-from .middleware.common import (
-    resp_base,
-    resp_static,
-    catch_error,
-    log_print,
-    resp_params,
-)
-
-# 响应路径
-from .settings import ASSETS
-
-# 服务全局对象
-app = FastAPI()
-version = get_package_version()
-
-# 注册前端静态文件路径
-static_path = "/assets"
-static = StaticFiles(directory=ASSETS)
-app.mount(static_path, static)
-
-# 将uvicorn的日志输出handler删除
-import logging
-
-# 删除 uvicorn logger
-uvicorn_error = logging.getLogger("uvicorn.error")
-uvicorn_error.disabled = True
-uvicorn_access = logging.getLogger("uvicorn.access")
-uvicorn_access.disabled = True
-
-
-# ---------------------------------- 在此处注册中间件 ----------------------------------
-
-
-@app.middleware("http")
-async def _(*args, **kwargs):
-    """基础中间件，调整响应结果，添加处理时间等信息"""
-    return await resp_base(*args, **kwargs)
-
-
-@app.middleware("http")
-async def _(*args, **kwargs):
-    """资源中间件，此时所有与api相关的内容不会在此中间件中处理"""
-    return await resp_static(*args, **kwargs)
-
-
-@app.middleware("http")
-async def _(*args, **kwargs):
-    """异常中间件，捕获异常，重构异常信息"""
-    return await catch_error(*args, **kwargs)
-
-
-@app.middleware("http")
-async def _(*args, **kwargs):
-    """日志打印中间件"""
-    return await log_print(*args, **kwargs)
-
-
-@app.middleware("http")
-async def _(*args, **kwargs):
-    """参数中间件，处理api请求中的参数校验问题，重新结构化校验错误结果
-
-    参数校验错误并不会影响其他情况的响应结果
-    此外由于参数校验错误在绝大多数情况应该是开发时的错误
-    所以不会影响正式版本的性能
-    """
-    return await resp_params(*args, **kwargs)
-
-
-# ---------------------------------- 在此处注册相关路由 ----------------------------------
-
-# 导入数据相关的路由
-from .router.experiment import router as experiment
-from .router.project import router as project
-from .router.namespace import router as namespace
-from .router.chart import router as chart
-
-# 媒体文件路由，允许前端获取其他产生的媒体文件
-from .router.media import router as media
-
-# 使用配置列表，统一导入
-prefix = "/api/v1"
-app.include_router(project, prefix=prefix + "/project")
-app.include_router(experiment, prefix=prefix + "/experiment")
-app.include_router(media, prefix=prefix + "/media")
-app.include_router(namespace, prefix=prefix + "/namespace")
-app.include_router(chart, prefix=prefix + "/chart")
diff --git a/swanlab/server/controller/chart.py b/swanlab/server/controller/chart.py
deleted file mode 100644
index 385433ba..00000000
--- a/swanlab/server/controller/chart.py
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-03-10 13:53:16
-@File: swanlab/server/controller/chart.py
-@IDE: vscode
-@Description:
-    图表相关操作api
-"""
-from .db import Chart
-from .utils import get_exp_charts, get_proj_charts
-from ..module import SUCCESS_200, PARAMS_ERROR_422
-
-
-def update_charts_status(chart_id: int, status: int):
-    """
-    更新图表状态，status=1表示置顶，status=0表示正常状态，status=-1表示隐藏
-
-    Parameters
-    ----------
-    chart_id : int
-        图表id
-    status : int
-        状态，1表示置顶，0表示正常状态，-1表示隐藏
-    """
-    chart: Chart = Chart.get_by_id(chart_id)
-
-    if not chart:
-        return PARAMS_ERROR_422("chart_id not exist")
-    if status == 1:
-        Chart.pin(id=chart_id)
-    elif status == -1:
-        Chart.hide(id=chart_id)
-    else:
-        Chart.restore(id=chart_id)
-    if chart.project_id:
-        chart_list, namespace_list = get_proj_charts(chart.project_id.id)
-    else:
-        chart_list, namespace_list = get_exp_charts(chart.experiment_id.id)
-    for namespace in namespace_list:
-        for j, chart_id in enumerate(namespace["charts"]):
-            # 在charts中找到对应的chart_id
-            namespace["charts"][j] = next((x for x in chart_list if x["id"] == chart_id), None)
-    return SUCCESS_200({"groups": namespace_list})
diff --git a/swanlab/server/controller/db.py b/swanlab/server/controller/db.py
deleted file mode 100644
index faeb440e..00000000
--- a/swanlab/server/controller/db.py
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-03-09 21:44:40
-@File: swanlab/server/controller/db.py
-@IDE: vscode
-@Description:
-    数据库，从数据库模块中导入数据库相关函数，供其他模块调用，不用每次都通过相对路径导入了
-"""
-from ...db import *
diff --git a/swanlab/server/controller/experiment.py b/swanlab/server/controller/experiment.py
deleted file mode 100644
index 0275767c..00000000
--- a/swanlab/server/controller/experiment.py
+++ /dev/null
@@ -1,564 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-20 21:21:45
-@File: swanlab\server\controller\experiment.py
-@IDE: vscode
-@Description:
-    实验相关 api 的处理函数
-"""
-
-import os
-import ujson
-import shutil
-from datetime import datetime
-from ..module.resp import SUCCESS_200, DATA_ERROR_500, NOT_FOUND_404
-from fastapi import Request
-from urllib.parse import quote
-from ..settings import (
-    get_logs_dir,
-    get_exp_dir,
-    get_config_path,
-    get_console_dir,
-    get_meta_path,
-    get_requirements_path,
-)
-from ...utils.time import create_time
-import yaml
-from ...log import swanlog
-from .db import (
-    Project,
-    Experiment,
-    Chart,
-    Tag,
-    connect,
-    NotExistedError,
-)
-from .utils import get_exp_charts, clear_field, read_tag_data, get_tag_files, LOGS_CONFIGS, lttb
-from ...compat.server.controller.experiment import compat_text
-
-__to_list = Experiment.search2list
-
-# ---------------------------------- 通用 ----------------------------------
-
-
-# 默认项目 id
-DEFAULT_PROJECT_ID = Project.DEFAULT_PROJECT_ID
-# 实验运行状态
-RUNNING_STATUS = Experiment.RUNNING_STATUS
-
-
-# ---------------------------------- 工具函数 ----------------------------------
-
-
-def __get_exp_dir_by_id(experiment_id: int) -> str:
-    """通过 experiment_id 获取实验目录
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一id
-
-    Returns
-    -------
-    str
-        实验目录路径
-    """
-
-    return get_exp_dir(Experiment.get(experiment_id).run_id)
-
-
-def __get_logs_dir_by_id(experiment_id: int) -> str:
-    """通过 experiment_id 获取实验 保存目录
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一id
-
-    Returns
-    -------
-    str
-        实验 logs 目录路径
-    """
-    return get_logs_dir(Experiment.get(experiment_id).run_id)
-
-
-def __get_console_dir_by_id(experiment_id: int) -> str:
-    """通过 experiment_id 获取实验 console 保存目录
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一id
-
-    Returns
-    -------
-    str
-        实验 console 目录路径
-    """
-
-    return get_console_dir(Experiment.get(experiment_id).run_id)
-
-
-def __get_requirements_path_by_id(experiment_id: int):
-    """通过 experiment_id 获取实验依赖存储路径
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一id
-
-    Returns
-    -------
-    str
-        实验依赖存储路径
-    """
-
-    return get_requirements_path(Experiment.get(experiment_id).run_id)
-
-
-# ---------------------------------- 路由对应的处理函数 ----------------------------------
-
-
-# 获取实验信息
-def get_experiment_info(experiment_id: int):
-    """获取实验信息
-    1. 数据库中获取实验的基本信息
-    2. 从实验目录获取配置信息
-    3. 获取实验元信息
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一id
-    """
-
-    experiment = Experiment.get(experiment_id).__dict__()
-    experiment.pop("project_id")
-
-    # 加载实验配置
-    config_path = get_config_path(experiment["run_id"])
-    if os.path.exists(config_path):
-        with open(config_path, 'r+') as f:
-            experiment["config"] = yaml.load(f, Loader=yaml.FullLoader)
-
-    # 加载实验元信息
-    meta_path = get_meta_path(experiment["run_id"])
-    if os.path.exists(meta_path) and not os.stat(meta_path).st_size == 0:
-        with open(meta_path, 'r+') as f:
-            experiment["system"] = ujson.load(f)
-    else:
-        experiment["system"] = {}
-    return SUCCESS_200(experiment)
-
-
-# 获取表单数据
-def get_tag_data(experiment_id: int, tag: str) -> dict:
-    """获取表单数据
-    根据实验id得到实验的运行id，然后根据运行id和tag得到实验的数据
-
-    Parameters
-    ----------
-    experiment_id: int
-        实验唯一id，路径传参
-    tag: str
-        表单标签，路径传参，已经进行了 URIComponent 编码
-    """
-    # ---------------------------------- 前置处理 ----------------------------------
-    # 获取tag对应的存储目录
-    try:
-        tag_path: str = os.path.join(__get_logs_dir_by_id(experiment_id), tag)
-    except NotExistedError:
-        return NOT_FOUND_404("experiment not found")
-    if not os.path.exists(tag_path):
-        return NOT_FOUND_404("tag not found")
-    # 获取目录下存储的所有数据
-    tag_data: list = []
-    # ---------------------------------- 读取文件数据 ----------------------------------
-    current_logs = get_tag_files(tag_path, LOGS_CONFIGS)
-    for file in current_logs:
-        tag_data = tag_data + read_tag_data(os.path.join(tag_path, file))
-    # ---------------------------------- 返回所有数据 ----------------------------------
-    # 如果数据为空，返回空列表
-    if len(tag_data) == 0:
-        return SUCCESS_200(data={"sum": 0, "list": [], "experiment_id": experiment_id})
-    # COMPAT 如果第一个数据没有index，就循环每个数据，加上index
-    if tag_data[0].get("index") is None:
-        for index, data in enumerate(tag_data):
-            data["index"] = str(index + 1)
-    # COMPAT 如果第一个数据的index不是int，改为int
-    if not isinstance(tag_data[0]["index"], int):
-        for data in tag_data:
-            data["index"] = int(data["index"])
-    # COMPAT 如果当前tag的类型为text，并且在media文件夹下存在相同名文件夹
-    texts = compat_text(experiment_id, tag)
-    # 如果需要兼容，进行重新赋值，直接获取文本内容
-    if texts:
-        tag_data = texts
-    # 根据index升序排序
-    tag_data.sort(key=lambda x: int(x["index"]))
-    # tag_data 的 最后一个数据增加一个字段_last = True
-    tag_data[-1]["_last"] = True
-    # 获取_summary文件
-    summary_path = os.path.join(tag_path, "_summary.json")
-    if os.path.exists(summary_path):
-        with open(summary_path, "r") as f:
-            summary = ujson.load(f)
-            max_value = summary.get("max", None)
-            min_value = summary.get("min", None)
-    else:
-        # COMPAT 如果_summary文件不存在，手动获取最大值和最小值
-        warn = f"Summary file of tag '{tag}' not found, SwanLab will automatically get the maximum and minimum values."
-        swanlog.warning(warn)
-        # 遍历tag_data，获取最大值和最小值
-        # 提取 data 字段的值
-        data_values = [entry["data"] for entry in tag_data]
-        # 获取最大值和最小值
-        max_value = max(data_values)
-        min_value = min(data_values)
-    return SUCCESS_200(
-        data={
-            "sum": len(tag_data),
-            "max": max_value,
-            "min": min_value,
-            "list": lttb(tag_data),
-            # 标注此数据隶属于哪个实验
-            "experiment_id": experiment_id,
-        }
-    )
-
-
-# 获取实验状态
-def get_experiment_status(experiment_id: int):
-    """获取实验状态以及实验图表配置，用于实时更新实验状态
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一id，路径传参
-    """
-
-    experiment = Experiment.get(experiment_id)
-    chart_list, namespace_list = get_exp_charts(experiment_id)
-
-    return SUCCESS_200(
-        {
-            "status": experiment.status,
-            "update_time": experiment.update_time,
-            "finish_time": experiment.finish_time,
-            "charts": {
-                "charts": clear_field(chart_list, "experiment_id"),
-                "namespaces": namespace_list,
-            },
-        }
-    )
-
-
-# 获取实验的总结数据
-def get_experiment_summary(experiment_id: int) -> dict:
-    """获取实验的总结数据——每个tag的最后一个setp的data
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验id
-
-    Returns
-    -------
-    dict:
-        summaries: list
-            每个tag的最后一个数据
-    """
-
-    experiment = Experiment.get_by_id(experiment_id)
-    # 通过外键反链获取实验下的所有tag
-    tag_list = [tag["name"] for tag in __to_list(experiment.tags)]
-    experiment_path = __get_logs_dir_by_id(experiment_id)
-    # 通过目录结构获取所有正常的tag
-    tags = [f for f in os.listdir(experiment_path) if os.path.isdir(os.path.join(experiment_path, f))]
-    # 实验总结数据
-    summaries = []
-    for tag in tag_list:
-        # 如果 tag 记录在数据库，但是没有对应目录，说明 tag 有问题
-        # 所以 tags 是 tag_list 的子集，出现异常的 tag 会记录在数据库但不会添加到目录结构中
-        if quote(tag, safe="") not in tags:
-            summaries.append({"key": tag, "value": "TypeError"})
-            continue
-        tag_path = os.path.join(experiment_path, quote(tag, safe=""))
-        # 获取 tag 目录下的所有存储的日志文件
-        logs = get_tag_files(tag_path, LOGS_CONFIGS)
-        # 打开 tag 目录下最后一个存储文件，获取最后一条数据
-        with open(os.path.join(tag_path, logs[-1])) as f:
-            lines = f.readlines()
-            # 最后一行数据，如果为空，取倒数第二行
-            data = lines[-1] if len(lines[-1]) else lines[-2]
-            last_data = ujson.loads(data)
-            summaries.append({"key": tag, "value": last_data["data"]})
-    # 获取数据库记录时在实验下的排序
-    sorts = {item["name"]: item["sort"] for item in __to_list(experiment.tags)}
-    # COMPAT 如果 sorts 中的值不都为 0，说明是新版添加排序后的 tag，这里进行排序 (如果是旧版没有排序的tag，直接按照数据库顺序即可)
-    if not all(value == 0 for value in sorts.values()):
-        temp = [0] * len(summaries)
-        for item in summaries:
-            temp[sorts[item["key"]]] = item
-        summaries = temp
-    return SUCCESS_200({"summaries": summaries})
-
-
-MAX_NUM = 6000
-
-
-# 获取实验最近日志
-def get_recent_logs(experiment_id):
-    """一下返回最多 MAX_NUM 条打印记录
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一ID
-
-    Returns
-    -------
-    dict :
-        recent: List[str]
-            0: 截止处日志文件
-            1: 开始处日志文件
-        logs: List[str]
-            日志列表
-    """
-
-    console_path: str = __get_console_dir_by_id(experiment_id)
-    # 是否存在
-    if not os.path.exists(console_path):
-        return NOT_FOUND_404("Log Folder not Found")
-    consoles: list = [f for f in os.listdir(console_path)]
-    # 含有error.log，在返回值中带上其中的错误信息
-    error = None
-    if "error.log" in consoles:
-        with open(os.path.join(console_path, "error.log"), mode="r") as f:
-            error = f.read().split("\n")
-        # 在consoles里删除error.log
-        consoles.remove("error.log")
-    # 没有日志，也没有错误日志
-    if len(consoles) == 0 and error is None:
-        return NOT_FOUND_404("No Logs Found")
-    # 如果只有错误日志，直接返回
-    elif len(consoles) == 0 and error:
-        return SUCCESS_200({"error": error})
-
-    total: int = len(consoles)
-    # # 如果 total 大于 1, 按照时间排序
-    if total > 1:
-        consoles = sorted(consoles, key=lambda x: datetime.strptime(x[:-4], "%Y-%m-%d"), reverse=True)
-    logs = []
-    # # current_page = total
-    for index, f in enumerate(consoles, start=1):
-        with open(os.path.join(console_path, f), mode="r", encoding="utf-8") as log:
-            logs = log.read().split("\n") + logs
-            # 如果当前收集到的数据超过限制，退出循环
-            # if len(logs) >= MAX_NUM:
-            #     # current_page = index
-            #     break
-    # 如果 logs 内容为空
-    if len(logs) == 0:
-        return NOT_FOUND_404("No Logs Found")
-
-    # logs = logs[:MAX_NUM]
-    end = (logs[-1] if not logs[-1] == "" else logs[-2]).split(" ")[0]
-    data = {
-        "recent": [logs[0].split(" ")[0], end],
-        "logs": logs,
-    }
-    if error is not None:
-        data["error"] = error
-    # 返回最新的 MAX_NUM 条记录
-    return SUCCESS_200(data)
-
-
-# 获取图表信息
-def get_experimet_charts(experiment_id: int):
-    """获取图表信息
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一 ID
-
-    Returns
-    -------
-    dict :
-        _sum: integer
-        charts: List[dict]
-        namesapces: List[dict]
-    """
-    chart_list, namespace_list = get_exp_charts(experiment_id)
-
-    return SUCCESS_200(
-        {
-            "charts": clear_field(chart_list, "experiment_id"),
-            "namespaces": namespace_list,
-        }
-    )
-
-
-# 更改项目信息
-async def update_experiment_info(experiment_id: int, request: Request):
-    """修改实验的信息
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验id
-    body : Body
-        name: str
-            实验名称
-        description: str
-            实验描述
-
-    Returns
-    -------
-    dict :
-        name: str
-        description: str
-    """
-
-    db = connect()
-    body = await request.json()
-    body["name"] = body["name"].strip()
-    with db.atomic():
-        experiment = Experiment.get(experiment_id)
-        experiment.name = body.get("name")
-        experiment.description = body.get("description")
-        experiment.save()
-
-    db.close()
-
-    return SUCCESS_200(body)
-
-
-# 删除实验
-def delete_experiment(experiment_id: int):
-    """删除实验
-
-    注意，需要先判断当前实验是否正在运行中，不可删除运行中的实验
-    同时，需要删除所有表中含有该 experiment_id 的行
-
-    Parameters
-    ----------
-    experiment_id : Int
-        实验唯一ID
-
-    Returns
-    -------
-    project : Dictionary
-        删除实验后的项目信息，提供给前端更新界面
-
-    TODO: 该怎么删才能高效删干净
-    """
-
-    # 先删除实验目录
-    experiment_path = __get_exp_dir_by_id(experiment_id)
-    shutil.rmtree(experiment_path)
-    # 看看该实验下有哪儿些 tag
-    tags = Tag.filter(Tag.experiment_id == experiment_id)
-    tag_names = [tag["name"] for tag in __to_list(tags)]
-    # 检查多实验图表是否有需要删除的
-    project_id = Experiment.get_by_id(experiment_id).project_id.id
-    # 找到属于多实验且与该实验相关的图表
-    charts = Chart.filter(Chart.project_id == project_id, Chart.name.in_(tag_names))
-
-    db = connect()
-    with db.atomic():
-        # 必须先清除数据库中的实验数据
-        Experiment.delete().where(Experiment.id == experiment_id).execute()
-        # 图表无 source 的需要删除
-        del_list = []
-        for chart in charts:
-            if chart.sources.count() == 0:
-                del_list.append(chart.id)
-        Chart.delete().where(Chart.id.in_(del_list)).execute()
-    db.commit()
-
-    return SUCCESS_200({"experiment_id": experiment_id})
-
-
-# 停止实验
-def stop_experiment(experiment_id: int):
-    """停止实验
-
-    Parameters
-    ----------
-    experiment_id : Int
-        实验唯一ID
-
-    Returns
-    -------
-    project : Dictionary
-        停止实验后的项目信息，提供给前端更新界面
-    """
-    experiment: Experiment = Experiment.get(experiment_id)
-    experiment.update_status(Experiment.STOPPED_STATUS)
-
-    return SUCCESS_200(
-        {
-            "id": experiment_id,
-            "status": Experiment.STOPPED_STATUS,
-            "finish_time": create_time(),
-        }
-    )
-
-
-# 获取实验依赖
-def get_experiment_requirements(experiment_id: int):
-    """获取实验依赖
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一ID
-
-    Returns
-    -------
-    dict :
-        requirements: list
-            每个依赖项为一行，以列表的形式返回
-    """
-
-    path = __get_requirements_path_by_id(experiment_id)
-    if not os.path.exists(path):
-        return DATA_ERROR_500("failed to find requirements")
-    with open(path) as f:
-        requirements = f.read()
-
-    return SUCCESS_200({"requirements": requirements.split("\n")})
-
-
-# 修改实验是否可见
-def change_experiment_visibility(experiment_id: int, show: bool):
-    """修改实验是否可见
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验id
-    show : bool
-        在多实验对比图表中，该实验是否可见，true -> 1 , false -> 0
-
-    Returns
-    -------
-    experiment : dict
-        当前实验信息
-    """
-
-    try:
-        experiment = Experiment.get_by_id(experiment_id)
-    except NotExistedError:
-        return NOT_FOUND_404("Experiment with id {} does not exist.".format(experiment_id))
-
-    if show:
-        experiment.show = 1
-    else:
-        experiment.show = 0
-    experiment.save()
-    return SUCCESS_200({"experiment": experiment.__dict__()})
diff --git a/swanlab/server/controller/namespace.py b/swanlab/server/controller/namespace.py
deleted file mode 100644
index d0769956..00000000
--- a/swanlab/server/controller/namespace.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from ..module.resp import SUCCESS_200, NOT_FOUND_404, PARAMS_ERROR_422
-from .db import NotExistedError
-
-from .db import (
-    Namespace,
-    Experiment,
-    Project,
-)
-
-
-# 修改namespace可见性
-def change_namespace_opened(namespace_id: int, opened: int, experiment_id: int, project_id: int):
-    """修改namespace的打开/关闭状态
-
-    Parameters
-    ----------
-    namespace_id : int
-        namespace的id，特别的，-1表示pinned，-2表示hidden
-    opened : int
-        在一个实验下，该namespace是否可见，true -> 1 , false -> 0
-    experiment_id : int
-        实验id，修改pinned/hidden的namespace的开启关闭状态时需要
-    project_id : int
-        项目id，修改pinned/hidden的namespace的开启关闭状态时需要
-    Returns
-    -------
-    _type_:dict
-        当前namespace信息
-    """
-    if namespace_id in [-1, -2]:
-        if experiment_id is not None:
-            exp_or_proj = Experiment.get_by_id(experiment_id)
-        else:
-            exp_or_proj = Project.get_by_id(project_id)
-        # 如果实验或者项目不存在，则返回404
-        if exp_or_proj is None:
-            return NOT_FOUND_404("Experiment or project with id {} does not exist.".format(experiment_id))
-        if namespace_id == -1:
-            exp_or_proj.pinned_opened = 1 if opened else 0
-        else:
-            exp_or_proj.hidden_opened = 1 if opened else 0
-        exp_or_proj.save()
-        return SUCCESS_200(None)
-    # 正常的namespace
-    try:
-        namespace: Namespace = Namespace.get_by_id(namespace_id)
-    except NotExistedError:
-        return NOT_FOUND_404("Namespace with id {} does not exist.".format(namespace_id))
-    namespace.opened = 1 if opened else 0
-    namespace.save()
-    return SUCCESS_200(None)
diff --git a/swanlab/server/controller/project.py b/swanlab/server/controller/project.py
deleted file mode 100644
index da99b6fd..00000000
--- a/swanlab/server/controller/project.py
+++ /dev/null
@@ -1,271 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-19 22:09:09
-@File: swanlab\server\controller\project.py
-@IDE: vscode
-@Description:
-    项目相关 api 的处理函数
-"""
-
-import os
-import ujson
-import shutil
-from fastapi import Request
-from urllib.parse import quote
-from ...env import get_swanlog_dir
-from ...utils import COLOR_LIST
-from ...utils.file import check_desc_format
-import yaml
-from typing import List
-from ...db.utils.chart import transform_to_multi_exp_charts
-from .db import NotExistedError
-from .utils import get_proj_charts
-from ...log import swanlog
-
-# 自定义响应
-from ..module.resp import (
-    SUCCESS_200,
-    DATA_ERROR_500,
-    CONFLICT_409,
-    NOT_FOUND_404,
-)
-
-# 功能性函数
-from ..settings import (
-    get_tag_dir,
-    get_exp_dir,
-    get_config_path,
-)
-
-# 数据表模型
-from ...db import (
-    Project,
-    Experiment,
-    Tag,
-    Chart,
-    Namespace,
-)
-from .utils import get_tag_files, LOGS_CONFIGS
-
-# 将查询结果对象转为列表
-__to_list = Project.search2list
-
-# ---------------------------------- 通用 ----------------------------------
-
-# 默认项目 id
-DEFAULT_PROJECT_ID = Project.DEFAULT_PROJECT_ID
-# 实验运行状态
-RUNNING_STATUS = Experiment.RUNNING_STATUS
-
-
-# ---------------------------------- 路由对应的处理函数 ----------------------------------
-
-
-# 获取项目信息
-def get_project_info(project_id: int = DEFAULT_PROJECT_ID) -> dict:
-    """
-    1. 获取项目信息
-    2. 列出当前项目下的所有实验
-    3. 获取 swanlog 目录路径
-
-    Parameters
-    ----------
-    project_id : int, optional
-        项目id, 默认为DEFAULT_PROJECT_ID
-
-    Returns
-    -------
-    dict:
-        列出当前项目下的所有实验
-    """
-
-    try:
-        project = Project.filter(Project.id == project_id).first()
-        data = project.__dict__()
-        data["logdir"] = get_swanlog_dir()
-        experiments = __to_list(project.experiments)
-        for experiment in experiments:
-            experiment["experiment_id"] = experiment["id"]
-        data["experiments"] = experiments
-        for experiment in data["experiments"]:
-            # 将其中的 project_id 字段去除
-            experiment.pop("project_id")
-            # 检查配置文件是否存在
-            config_path = get_config_path(experiment["run_id"])
-            if os.path.exists(config_path):
-                # 加载config字段
-                with open(config_path, 'r+') as f:
-                    experiment["config"] = yaml.load(f, Loader=yaml.FullLoader)
-        # 色盘
-        data["colors"] = COLOR_LIST
-        return SUCCESS_200(data)
-    except Exception as e:
-        return DATA_ERROR_500(f"Get list experiments failed: {e}")
-
-
-# 获取项目总结信息
-def get_project_summary(project_id: int = DEFAULT_PROJECT_ID) -> dict:
-    """
-    获取项目下所有实验的总结信息
-
-    Parameters
-    ----------
-    project_id : int, optional
-        项目id, 默认为DEFAULT_PROJECT_ID
-
-    Returns
-    -------
-    dict:
-        项目总结信息
-    """
-
-    # 查找所有实验，提出 id 列表
-    experiments = Experiment.select().where(Experiment.project_id == project_id)
-    exprs = [
-        {
-            "id": experiment.id,
-            "name": experiment.name,
-            "run_id": experiment.run_id,
-            "tags": [tag["name"] for tag in __to_list(experiment.tags)],
-        }
-        for experiment in experiments
-    ]
-    ids = [item["id"] for item in exprs]
-
-    # 根据 id 列表找到所有的 tag，提出不含重复 tag 名的元组
-    # tags = Tag.filter(Tag.experiment_id.in_(ids)).order_by(Tag.experiment_id)
-    tags = (
-        Tag.select()
-        .join(Experiment, on=(Tag.experiment_id == Experiment.id))
-        .where(Tag.experiment_id.in_(ids))
-        .order_by(Tag.experiment_id, Tag.create_time)
-    )
-    # tag_names不用编码，因为前端需要展示
-    # 要保持顺序，需要额外使用一个列表，直接用 set 会打乱顺序
-    unique_values = []
-    seen = set()
-    for value in [item["name"] for item in __to_list(tags)]:
-        if value not in seen:
-            unique_values.append(value)
-            seen.add(value)
-    tag_names = list(unique_values)
-    # 所有总结数据
-    data = {}
-    # 第一层循环对应实验层，每次探寻一个实验
-    for expr in exprs:
-        # 第二层为 tag 层，在实验目录下遍历所有 tag，若存在则获取最后一个次提交的值
-        experiment_summaries = {}
-        for tag in expr["tags"]:
-            tag_path = get_tag_dir(expr["run_id"], quote(tag, safe=""))
-            if not os.path.exists(tag_path):
-                experiment_summaries[tag] = "TypeError"
-                continue
-            # 获取所有 tag 文件
-            logs = get_tag_files(tag_path, exclude=LOGS_CONFIGS)
-            # 打开最后一个文件，获取最后一行数据
-            with open(os.path.join(tag_path, logs[-1]), mode="r") as f:
-                try:
-                    lines = f.readlines()
-                    tag_data = ujson.loads(lines[-1])
-                    experiment_summaries[tag] = tag_data["data"]
-                except Exception as e:
-                    swanlog.error(f"[expr: {expr['name']} - {tag}] --- {e}")
-                    continue
-
-        data[expr["name"]] = experiment_summaries
-
-    return SUCCESS_200({"tags": tag_names, "summaries": data})
-
-
-# 修改项目信息
-async def update_project_info(request: Request, project_id: int = DEFAULT_PROJECT_ID) -> dict:
-    """修改项目信息
-    - 项目名
-    - 项目描述
-
-    Parameters
-    ----------
-    project_id : int, optional
-        默认为 DEFAULT_PROJECT_ID
-
-    Returns
-    -------
-    dict
-        新项目信息
-        - name
-        - description
-    """
-
-    body = await request.json()
-
-    # 检查格式
-    body["description"] = check_desc_format(body["description"], False)
-
-    project = Project.filter(Project.id == project_id).first()
-    dict_project = project.__dict__()
-
-    # 检查名字
-    if "name" in dict_project and dict_project["name"] == body["name"]:
-        pass
-    else:
-        project.name = body["name"]
-
-    # 检查描述
-    if "description" in dict_project and dict_project["description"] == body["description"]:
-        pass
-    else:
-        project.description = body["description"]
-
-    project.save()
-    return SUCCESS_200({"updates": body})
-
-
-# 删除项目
-async def delete_project(project_id: int = DEFAULT_PROJECT_ID):
-    """删除项目
-    1. 删除实验目录
-    2. 删除数据库
-
-    Parameters
-    ----------
-    project_id : int, optional
-        默认为第一个项目
-
-    TODO: 原子操作，同时删除项目和实验
-    """
-
-    # 检查是否有正在运行的实验
-    running_exp = Experiment.filter(Experiment.project_id == project_id, Experiment.status == RUNNING_STATUS).count()
-    if running_exp > 0:
-        return CONFLICT_409("Can't delete project since there is experiment running")
-
-    project = Project.filter(Project.id == project_id).first()
-    run_ids = [experiment["run_id"] for experiment in __to_list(project.experiments)]
-
-    # 删除实验目录
-    for run_id in run_ids:
-        exp_path = get_exp_dir(run_id)
-        if os.path.isdir(exp_path):
-            shutil.rmtree(exp_path)
-
-    # 清空数据库
-    project.delete_instance()
-
-    return SUCCESS_200({})
-
-
-async def get_project_charts(project_id: int = DEFAULT_PROJECT_ID) -> dict:
-    """获取多实验对比图表数据,并且考虑往期版本兼容性
-    1. 如果当前项目的chart字段为0，先生成多实验对比数据，跳转步骤2
-    2. 依据规则获取所有实验的图表数据
-    """
-
-    # COMPAT 兼容以前没有多实验对比数据的情况
-    try:
-        transform_to_multi_exp_charts(project_id)
-    except IndexError:  # 已经有多实验对比数据
-        pass
-    charts, namespace = get_proj_charts(project_id)
-    # 获取项目下所有实验的图表数据
-    return SUCCESS_200({"charts": charts, "namespaces": namespace})
diff --git a/swanlab/server/controller/utils/__init__.py b/swanlab/server/controller/utils/__init__.py
deleted file mode 100644
index c272fa38..00000000
--- a/swanlab/server/controller/utils/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-03-09 21:41:37
-@File: swanlab/server/controller/utils/__init__.py
-@IDE: vscode
-@Description:
-    共享工具函数
-"""
-from .charts import get_exp_charts, get_proj_charts
-from .tag import read_tag_data, get_tag_files, LOGS_CONFIGS, lttb
-from typing import List
-
-
-def clear_field(target: List[dict], field: str) -> List[dict]:
-    """遍历字典列表清除某个字段
-
-    Parameters
-    ----------
-    target : List[dict]
-        需要处理的列表
-    field : str
-        需要删除的字段
-
-    Returns
-    -------
-    List[dict]
-        处理后的字典列表
-    """
-
-    for item in target:
-        item.pop(field)
-
-    return target
diff --git a/swanlab/server/controller/utils/charts.py b/swanlab/server/controller/utils/charts.py
deleted file mode 100644
index 2ed6040c..00000000
--- a/swanlab/server/controller/utils/charts.py
+++ /dev/null
@@ -1,167 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-03-09 21:43:59
-@File: swanlab/server/controller/utils/charts.py
-@IDE: vscode
-@Description:
-    图表相关函数
-"""
-from ..db import Chart, Display, Namespace
-from typing import List, Union
-
-
-def get_exp_charts(id: int):
-    """
-    获取单实验图表数据
-
-    Parameters
-    ----------
-    id : int
-        实验id
-    """
-    charts: List[Chart] = Chart.filter(Chart.experiment_id == id)
-    chart_list = Chart.search2list(charts)
-    # 获取每个图表对应的数据源
-    for index, chart in enumerate(charts):
-        sources = []
-        error = {}
-        # source->experiment_id
-        source_map = {}
-        for source in Chart.search2list(chart.sources):
-            sources.append(source["tag_id"]["name"])
-            source_map[source["tag_id"]["name"]] = id
-            if source["error"]:
-                error[source["tag_id"]["name"]] = Chart.json2dict(source["error"])
-        chart_list[index]["error"] = error
-        chart_list[index]["source"] = sources
-        chart_list[index]["multi"] = False
-        chart_list[index]["source_map"] = source_map
-
-    # 当前实验下的命名空间
-    namespaces = Namespace.filter(Namespace.experiment_id == id)
-    namespace_list = Namespace.search2list(namespaces)
-    # 获取每个命名空间对应的 display
-    # display 含有 chart 与 namespace 的对应关系
-    for index, namespace in enumerate(namespaces):
-        displays = []
-        for display in Namespace.search2list(namespace.displays):
-            displays.append(display["chart_id"]["id"])
-        namespace_list[index]["charts"] = displays
-    return get_pinned_and_hidden(chart_list, namespace_list)
-
-
-def get_proj_charts(id: int):
-    """
-    获取多实验对比图表数据
-
-    Parameters
-    ----------
-    id : int
-        项目id
-    """
-    # 获取当前项目下所有的多实验对比表
-    # 暂时只请求chart.type为default或者line的图表
-    allow_types = ["default", "line", "image", "audio"]
-    multi_charts = Chart.filter(Chart.project_id == id, Chart.type.in_(allow_types))
-    # 获取图表配置
-    charts = []
-    # source -> experiment_id
-    source_map = {}
-    for _chart in multi_charts:
-        # 多实验图表的 source 中不是 tag_name，而是 experiment_name
-        sources, error = [], {}
-        # 单箭头是通过外键反向索引的 chart -> source -> tag -> experiment => experiment_name
-        for source in _chart.sources:
-            sources.append(source.tag_id.experiment_id.name)
-            source_map[source.tag_id.experiment_id.name] = source.tag_id.experiment_id.id
-        # 当前chart的error字段，将所有的error字段转换为dict，key为实验名
-        for source in _chart.sources:
-            if source.error:
-                error[source.tag_id.experiment_id.name] = Chart.json2dict(source.error)
-        t = _chart.__dict__()
-        charts.append({**t, "error": error, "source": sources, "multi": True, "source_map": source_map})
-    # 获取命名空间配置
-    namespaces = Namespace.filter(Namespace.project_id == id)
-    namespace_list = Namespace.search2list(namespaces)
-    # 通过 namespace 获取 display，从而获取该 namespace 下的 charts
-    for index, namespace in enumerate(namespaces):
-        displays = []
-        for display in Display.search2list(namespace.displays):
-            # 只获取默认图表
-            if display["chart_id"]["type"] in allow_types:
-                displays.append(display["chart_id"]["id"])
-        if len(displays) > 0:
-            namespace_list[index]["charts"] = displays
-    # 过滤namespace中的空charts的namespace
-    namespace_list = [namespace for namespace in namespace_list if "charts" in namespace]
-
-    return get_pinned_and_hidden(charts, namespace_list)
-
-
-def get_pinned_and_hidden(chart_list: List[dict], namespace_list: List[dict]) -> Union[List[dict], List[dict]]:
-    """
-    获取置顶图表与隐藏图表
-    """
-    if not len(chart_list) or not len(namespace_list):
-        return chart_list, namespace_list
-    # 获取pinned和hidden的开启/关闭状态，通过chart_list的第一个元素的experiment_id或者project_id获取
-    first_chart = chart_list[0]
-    if first_chart["experiment_id"] is not None:
-        exp_or_proj = first_chart["experiment_id"]
-    else:
-        exp_or_proj = first_chart["project_id"]
-    pinned_opened, hidden_opened = exp_or_proj["pinned_opened"], exp_or_proj["hidden_opened"]
-
-    # 遍历chart_list，动态生成pinned与hidden的namespace，这两个namespace的id分别为-1与-2
-    pinned_namespace = {
-        "id": -1,
-        "name": "pinned",
-        "charts": [],
-        "opened": pinned_opened,
-        "experiment_id": first_chart["experiment_id"],
-        "project_id": first_chart["project_id"],
-    }
-    hidden_namespace = {
-        "id": -2,
-        "name": "hidden",
-        "charts": [],
-        "opened": hidden_opened,
-        "experiment_id": first_chart["experiment_id"],
-        "project_id": first_chart["project_id"],
-    }
-    for chart in chart_list:
-        # 如果chart的status为1，则将其加入pinned的namespace，如果是-1加入hidden的namespace
-        # 如果是0，则不加入任何namespace
-        # 首先将chart对象加入pinned或hidden的namespace，后续会滤除为id
-        if chart["status"] == 1:
-            pinned_namespace["charts"].append(chart)
-        elif chart["status"] == -1:
-            hidden_namespace["charts"].append(chart)
-        if chart["status"] != 0:
-            del_chart_from_namespace(namespace_list, chart["id"])
-    # 滤除namespace中的空charts的namespace
-    namespace_list = [namespace for namespace in namespace_list if len(namespace["charts"]) != 0]
-    # 如果pinned_namespace中有charts，则加入namespace_list的首位
-    if len(pinned_namespace["charts"]) > 0:
-        # namespaces的charts字段根据每个元素的sort排序，小的在前
-        pinned_namespace["charts"].sort(key=lambda x: x["sort"])
-        pinned_namespace = {**pinned_namespace, "charts": [chart["id"] for chart in pinned_namespace["charts"]]}
-        namespace_list.insert(0, pinned_namespace)
-    # 如果hidden_namespace中有charts，则加入namespace_list的末位
-    if len(hidden_namespace["charts"]) > 0:
-        # namespaces的charts字段根据每个元素的sort排序，小的在前
-        hidden_namespace["charts"].sort(key=lambda x: x["sort"])
-        hidden_namespace = {**hidden_namespace, "charts": [chart["id"] for chart in hidden_namespace["charts"]]}
-        namespace_list.append(hidden_namespace)
-    return chart_list, namespace_list
-
-
-def del_chart_from_namespace(namespace_list: List[dict], chart_id: int) -> List[dict]:
-    """
-    从命名空间中删除图表
-    """
-    for namespace in namespace_list:
-        if chart_id in namespace["charts"]:
-            namespace["charts"].remove(chart_id)
-    return namespace_list
diff --git a/swanlab/server/controller/utils/tag.py b/swanlab/server/controller/utils/tag.py
deleted file mode 100644
index 3aaacd10..00000000
--- a/swanlab/server/controller/utils/tag.py
+++ /dev/null
@@ -1,153 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-03-17 15:13:31
-@File: swanlab/server/controller/utils/tag.py
-@IDE: vscode
-@Description:
-    tag相关处理函数
-"""
-from typing import List
-import json
-import ujson
-import math
-import os
-
-# tag 总结文件名
-TAG_SUMMARY_FILE = "_summary.json"
-# logs 目录下的配置文件
-LOGS_CONFIGS = [TAG_SUMMARY_FILE]
-
-
-def read_tag_data(file_path: str) -> List[dict]:
-    """读取某一个tag文件数据，数据依据其中的index字段排序
-
-    Parameters
-    ----------
-    file_path : str
-        tag文件路径，绝对路径
-    """
-    # 如果文件内容为空，返回空列表
-    if os.path.getsize(file_path) == 0:
-        return []
-    # 读取文件内容，文件内部本质上是字符串一堆json格式的数据，每一行是一个json并且有换行符分隔，需要拿到然后解析为list<dict>
-    data = []
-    with open(file_path, "r") as f:
-        lines = f.readlines()
-        # 解析为list<dict>
-        for i in range(len(lines)):
-            if len(lines[i]):
-                data.append(json.loads(lines[i]))
-        return data
-
-
-def get_tag_files(tag_path: str, exclude=None) -> List[str]:
-    """
-    获取实验数据，并且做向下兼容，在v0.2.4版本以前的实验日志数据将转换为新的格式
-
-    Parameters
-    ----------
-    tag_path : str
-        tag的路径
-    exclude : List[str]
-        需要排除的文件列表
-
-    Returns
-    -------
-    List[str]
-        tag文件列表
-    """
-    # 降序排列，最新的数据在最前面
-    if exclude is None:
-        exclude = []
-    files: list = os.listdir(tag_path)
-    previous_logs = [f for f in files if f.endswith(".json") and f not in exclude]
-    current_logs = [f for f in files if f.endswith(".log")]
-    current_logs.sort()
-    # COMPAT 如果目标文件夹不存在*.log文件但存在*.json文件，说明是之前的日志格式，需要转换
-    if len(current_logs) == 0 and len(previous_logs) > 0:
-        for file in previous_logs:
-            with open(os.path.join(tag_path, file), "r") as f:
-                data = ujson.load(f)
-                with open(os.path.join(tag_path, file.replace(".json", ".log")), "a") as i:
-                    for d in data["data"]:
-                        i.write(ujson.dumps(d) + "\n")
-        current_logs = [f for f in os.listdir(tag_path) if f.endswith(".log")]
-        current_logs.sort()
-    return current_logs
-
-
-def _sample_a_bucket(bucket: List[dict]) -> dict:
-    """
-    对一个bucket进行采样，返回采样后的数据
-    :param bucket: 一个桶中的信息
-    :return: 采样后的数据
-    """
-    if len(bucket) <= 2:
-        return bucket[0]
-    # 计算bucket头尾直线表达式
-    tail = bucket[0]
-    head = bucket[-1]
-    # k，b
-    k = (head["data"] - tail["data"]) / (head["index"] - tail["index"])
-    b = head["data"] - k * head["index"]
-    # 计算每个点到直线的距离
-    max_distance_p = (0, 0)  # (d, i)
-    for i in range(1, len(bucket) - 1):
-        distance = abs(k * bucket[i]["index"] + b - bucket[i]["data"]) / math.sqrt(k ** 2 + 1)
-        if distance > max_distance_p[0]:
-            max_distance_p = (distance, i)
-    return bucket[max_distance_p[1]]
-
-
-def _calculate_bucket_capacity(total_data_points, target_bucket_count) -> List[int]:
-    """
-    计算每个桶的容量
-    :param total_data_points: 总数据点数
-    :param target_bucket_count: 目标桶数量
-    :return: 每个桶的容量
-    """
-    # 计算每个桶的容量
-    bucket_capacity = total_data_points // target_bucket_count  # 整除部分
-    remaining_points = total_data_points % target_bucket_count  # 余数
-
-    # 将余数均匀地分配到前几个桶中
-    bucket_capacity_list = [bucket_capacity] * target_bucket_count
-    for i in range(remaining_points):
-        bucket_capacity_list[i] += 1
-
-    return bucket_capacity_list
-
-
-def lttb(data: List[dict], threshold: int = 1500):
-    """
-    LTTB算法，对数据进行降采样
-    最后保留头尾数据，因此桶的数量应该是实际数量-2
-    桶大小向下取整，保留第一个和最后一个点
-    降采样将优先发生在尾部
-    :param data: 数据
-    :param threshold: 采样后的数据长度，也是降采样阈值
-    """
-    if len(data) <= threshold:
-        return data
-    bucket_n = threshold - 2
-    _data = data[1:-1]
-    # 需要保留第一、最后一个点
-    sampled = [data[0]]
-    # 计算每一个桶的容量，每个桶容量不尽相同
-    buckets_capacity = _calculate_bucket_capacity(len(_data), bucket_n)
-    # 当前使用的bucket
-    now_bucket = []
-    # 当前bucket的数量
-    now_bucket_n = 0
-    for _d in _data:
-        now_bucket.append(_d)
-        if len(now_bucket) < buckets_capacity[now_bucket_n]:
-            continue
-        sampled.append(_sample_a_bucket(now_bucket))
-        now_bucket = []
-        now_bucket_n += 1
-        if now_bucket_n == bucket_n - 1:
-            break
-    sampled.append(data[-1])
-    return sampled
diff --git a/swanlab/server/middleware/common.py b/swanlab/server/middleware/common.py
deleted file mode 100644
index 3da9ea6a..00000000
--- a/swanlab/server/middleware/common.py
+++ /dev/null
@@ -1,115 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-19 22:10:15
-@File: swanlab\server\middleware\common.py
-@IDE: vscode
-@Description:
-    常规中间件
-"""
-
-from fastapi import Request
-from fastapi.responses import HTMLResponse
-from fastapi.staticfiles import StaticFiles
-import time
-from ..module.resp import UNEXPECTED_ERROR_500, PARAMS_ERROR_422
-from ...log import swanlog as swl
-from swanlab.package import get_package_version
-from ..settings import ASSETS, INDEX  # 响应路径
-
-version = get_package_version()
-
-# 注册静态文件路径
-static_path = "/assets"
-static = StaticFiles(directory=ASSETS)
-
-
-async def resp_base(request, call_next):
-    """基础中间件，调整响应结果，添加处理时间等信息"""
-    # 如果请求路径不以 '/api' 开头，说明并不是后端服务的请求，直接返回
-    if not request.url.path.startswith("/api"):
-        return await call_next(request)
-    # 记录请求开始时间
-    start_time = time.time()
-    # 调用下一个中间件或者最终的路由处理函数
-    # 我们约定路由处理函数最终返回三个参数，一个是错误码，一个是错误信息，一个是响应内容
-    response = await call_next(request)
-    # 记录请求结束时间
-    end_time = time.time()
-    # 计算处理时间，添加到响应头中
-    process_time = round(end_time - start_time, 4)
-    response.headers["SwanLab-Process-Time"] = str(process_time)
-    # 添加版本信息
-    response.headers["SwanLab-Version"] = version
-    # 返回响应
-    return response
-
-
-async def resp_static(request, call_next):
-    """资源中间件，此时所有与api相关的内容不会在此中间件中处理"""
-    if request.url.path.startswith(static_path):
-        # 如果是请求静态资源，直接返回
-        return await call_next(request)
-    if request.url.path.startswith("/api"):
-        # 如果是请求api，直接返回
-        return await call_next(request)
-    # 剩余情况返回index.html，交由前端路由处理
-    with open(INDEX, "r", encoding="utf-8") as file:
-        html_content = file.read()
-    return HTMLResponse(content=html_content, status_code=200)
-
-
-async def catch_error(request: Request, call_next):
-    """异常中间件，捕获异常，重构异常信息"""
-    if not request.url.path.startswith("/api"):
-        # 如果不是请求api，直接返回
-        return await call_next(request)
-    try:
-        return await call_next(request)
-    except Exception as e:
-        swl.error(str(e))
-        return UNEXPECTED_ERROR_500(str(e))
-
-
-async def log_print(request: Request, call_next):
-    """日志打印中间件"""
-    swl.debug("[" + request.method + "] from " + request.base_url._url)
-    resp = await call_next(request)
-    # 拿到状态码
-    # status = str(resp.status_code)
-    if not request.url.path.startswith("/api"):
-        # 如果不是请求api，直接返回
-        swl.debug("[" + str(resp.status_code) + "] " + request.method + " assets: " + request.url.path)
-    else:
-        content = "[" + str(resp.status_code) + "] " + request.method + " api: " + request.url.path
-        swl.debug(content)
-    return resp
-
-
-async def resp_params(request: Request, call_next):
-    """参数中间件，处理api请求中的参数校验问题，重新结构化校验错误结果"""
-    if not request.url.path.startswith("/api"):
-        # 如果不是请求api，直接返回
-        return await call_next(request)
-    resp = await call_next(request)
-    # 拿到状态码
-    status = resp.status_code
-    if status == 422:
-        # 参数校验错误，重构错误信息
-        # 拿到响应体
-        import json
-
-        body = [chunk async for chunk in resp.body_iterator][0].decode()
-        body = json.loads(body)
-        detail = body["detail"][0]
-        msg = detail["msg"].split(" ")[1:]
-        loc = detail["loc"][1]
-        msg.insert(0, loc)
-        msg = " ".join(msg)
-        return PARAMS_ERROR_422(msg)
-    """
-    参数校验错误并不会影响其他情况的响应结果
-    此外由于参数校验错误在绝大多数情况应该是开发时的错误
-    所以不会影响正式版本的性能
-    """
-    return resp
diff --git a/swanlab/server/module/__init__.py b/swanlab/server/module/__init__.py
deleted file mode 100644
index 3f8c118a..00000000
--- a/swanlab/server/module/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from .resp import (
-    SUCCESS_200,
-    SUCCESS_200,
-    PARAMS_ERROR_422,
-    NOT_FOUND_404,
-    CONFLICT_409,
-    DATA_ERROR_500,
-    UNEXPECTED_ERROR_500,
-)
diff --git a/swanlab/server/module/resp.py b/swanlab/server/module/resp.py
deleted file mode 100644
index b782cece..00000000
--- a/swanlab/server/module/resp.py
+++ /dev/null
@@ -1,115 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2023-12-13 19:46:18
-@File: swanlab/server/module/resp.py
-@IDE: vscode
-@Description:
-    定义响应结构体
-    说是结构体，实际上是各种函数，用于返回各种响应
-    结构体名称结构为：[错误描述]_[HTTP状态码]
-"""
-from fastapi.responses import JSONResponse as _JSONResponse
-
-# ---------------------------------- 错误码 ----------------------------------
-
-
-_SUCCESS_200 = 0
-"""一切正常，成功，期望的HTTP状态码为200"""
-
-_PARAMS_ERROR_422 = 3422
-"""参数错误，通常对应着前端传输的参数无法通过校验，这在中间件中处理，期望的HTTP状态码为422"""
-
-_NOT_FOUND_404 = 3404
-"""资源不存在，通常对应着路径不存在，期望的HTTP状态码为404"""
-
-_CONFLICT_409 = 3409
-"""可能造成冲突，通常意味着试图对资源进行不合理的操作，期望的HTTP状态码为409"""
-
-_DATA_ERROR_500 = 3500
-"""服务端存储的数据格式错误，这通常意味着指定资源无法解析为期望格式，期望的HTTP状态码为500"""
-
-_UNEXCEPTED_ERROR_500 = 3555
-"""未知错误，通常对应着未知的异常，期望的HTTP状态码为500"""
-
-# ---------------------------------- 定义响应结构体 ----------------------------------
-
-
-def _ResponseBody(code: int, message: str = None, data: dict = None):
-    """构造响应，返回一个字典，包含响应码，响应信息和响应数据
-
-    Parameters
-    ----------
-    code : int
-        响应码，0表示成功，非0表示失败
-    message : str, optional
-        错误信息，如果code为0，错误信息强制为success，如果code不为0，错误信息必须提供
-    data : dict, optional
-        响应数据，如果传入，必须为字典类型
-    """
-    # 如果code为0，错误信息强制为success
-    message = "success" if code == 0 else message
-    # 如果code不为0，错误信息必须提供
-    assert code == 0 or message is not None and len(message) > 0
-    # 如果传入了响应数据，必须为字典类型
-    assert data is None or isinstance(data, dict)
-    # 构造响应
-    if data is None:
-        return {
-            "code": code,
-            "message": message,
-        }
-    else:
-        return {
-            "code": code,
-            "message": message,
-            "data": data,
-        }
-
-
-def SUCCESS_200(data: dict):
-    """成功响应"""
-    return _JSONResponse(
-        status_code=200,
-        content=_ResponseBody(_SUCCESS_200, data=data),
-    )
-
-
-def PARAMS_ERROR_422(message: str = "params error"):
-    """请求参数错误"""
-    return _JSONResponse(
-        status_code=422,
-        content=_ResponseBody(_PARAMS_ERROR_422, message=message),
-    )
-
-
-def NOT_FOUND_404(message: str = "NotFound"):
-    """资源不存在"""
-    return _JSONResponse(
-        status_code=404,
-        content=_ResponseBody(_NOT_FOUND_404, message=message),
-    )
-
-
-def CONFLICT_409(message: str = "Conflict"):
-    """请求会造成资源冲突"""
-    return _JSONResponse(
-        status_code=409,
-        content=_ResponseBody(_CONFLICT_409, message=message),
-    )
-
-
-def DATA_ERROR_500(message: str = "data error"):
-    """服务端存储的数据格式错误"""
-    return _JSONResponse(
-        status_code=500,
-        content=_ResponseBody(_DATA_ERROR_500, message=message),
-    )
-
-
-def UNEXPECTED_ERROR_500(message: str = "unexpected error"):
-    """未知错误"""
-    return _JSONResponse(
-        status_code=500,
-        content=_ResponseBody(_UNEXCEPTED_ERROR_500, message=message),
-    )
diff --git a/swanlab/server/router/chart.py b/swanlab/server/router/chart.py
deleted file mode 100644
index 2cdfbea4..00000000
--- a/swanlab/server/router/chart.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from fastapi import APIRouter
-from ..module.resp import PARAMS_ERROR_422
-
-from ..controller.chart import (
-    update_charts_status,
-)
-from fastapi import Request
-from urllib.parse import quote
-
-router = APIRouter()
-
-
-@router.patch("/{chart_id}/status")
-async def _(chart_id: int, request: Request):
-    """修改图表状态
-
-    Parameters
-    ----------
-    chart_id : int
-        图表id
-    request : Request
-        请求体，包含 status 字段
-    Returns
-    -------
-    groups : list
-        图表列表
-    """
-
-    data = await request.json()
-    status = data.get("status")
-
-    if status is None:
-        return PARAMS_ERROR_422("Request parameter 'status'")
-    return update_charts_status(chart_id, status)
diff --git a/swanlab/server/router/experiment.py b/swanlab/server/router/experiment.py
deleted file mode 100644
index 5c8aee7a..00000000
--- a/swanlab/server/router/experiment.py
+++ /dev/null
@@ -1,221 +0,0 @@
-from fastapi import APIRouter
-from ..module.resp import PARAMS_ERROR_422
-
-from ..controller.experiment import (
-    # 获取实验信息
-    get_experiment_info,
-    # 获取 tag 信息
-    get_tag_data,
-    # 获取实验状态
-    get_experiment_status,
-    # 获取实验的总结数据
-    get_experiment_summary,
-    # 获取实验最近日志
-    get_recent_logs,
-    # 获取实验图标
-    get_experimet_charts,
-    # 更改实验信息
-    update_experiment_info,
-    # 删除实验
-    delete_experiment,
-    # 停止实验
-    stop_experiment,
-    # 获取实验依赖
-    get_experiment_requirements,
-    # 修改实验可见性
-    change_experiment_visibility,
-)
-from fastapi import Request
-from urllib.parse import quote
-
-router = APIRouter()
-
-
-@router.get("/{experiment_id}")
-def _(experiment_id: int):
-    """获取实验信息
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一 id
-
-    Returns
-    -------
-    dict
-        实验信息
-    """
-
-    return get_experiment_info(experiment_id)
-
-
-# COMPAT 由于fastapi不支持%2F的路径转换，所以采用通配符:path，并且在下面将path进行quote编码
-@router.get("/{experiment_id}/tag/{tag:path}")
-def _(experiment_id: int, tag: str) -> dict:
-    """获取表单数据
-
-    parameter
-    ----------
-    experiment_id: int
-        实验唯一id，路径传参
-    tag: str
-        表单标签，路径传参，使用时需要 URIComponent 编码
-    """
-    tag = quote(tag, safe="")
-
-    return get_tag_data(experiment_id, tag)
-
-
-@router.get("/{experiment_id}/status")
-def _(experiment_id: int):
-    """获取实验状态以及实验图表配置，用于实时更新实验状态
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一id，路径传参
-    """
-
-    return get_experiment_status(experiment_id)
-
-
-@router.get("/{experiment_id}/summary")
-def _(experiment_id: int) -> dict:
-    """获取实验的总结数据——每个tag的最后一个setp的data
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验id
-
-    Returns
-    -------
-    dict
-        响应信息
-    """
-
-    return get_experiment_summary(experiment_id)
-
-
-@router.get("/{experiment_id}/recent_log")
-async def _(experiment_id: int):
-    """一下返回最多 MAX_NUM 条打印记录
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一ID
-    """
-
-    return get_recent_logs(experiment_id)
-
-
-@router.get("/{experiment_id}/chart")
-def _(experiment_id: int):
-    """获取图标信息
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一 ID
-    """
-
-    return get_experimet_charts(experiment_id)
-
-
-@router.patch("/{experiment_id}")
-async def _(experiment_id: int, request: Request):
-    """修改实验的信息
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验id
-    body : Body
-        name: str
-            实验名称
-        description: str
-            实验描述
-
-    Returns
-    -------
-    dict
-    """
-
-    return await update_experiment_info(experiment_id, request)
-
-
-@router.delete("/{experiment_id}")
-def _(experiment_id: int):
-    """删除实验
-
-    注意，需要先判断当前实验是否正在运行中，不可删除运行中的实验
-
-    Parameters
-    ----------
-    experiment_id : Int
-        实验唯一ID
-
-    Returns
-    -------
-    project : Dictionary
-        删除实验后的项目信息，提供给前端更新界面
-    """
-
-    return delete_experiment(experiment_id)
-
-
-@router.get("/{experiment_id}/stop")
-def _(experiment_id: int):
-    """停止实验
-
-    Parameters
-    ----------
-    experiment_id : Int
-        实验唯一ID
-    """
-
-    return stop_experiment(experiment_id)
-
-
-# 获取实验依赖
-@router.get("/{experiment_id}/requirements")
-def _(experiment_id: int):
-    """获取实验依赖
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验唯一ID
-
-    Returns
-    -------
-    requirements: list
-        每个依赖项为一行，以列表的形式返回
-    """
-
-    return get_experiment_requirements(experiment_id)
-
-
-# 修改实验可见性
-@router.patch("/{experiment_id}/show")
-async def _(experiment_id: int, request: Request):
-    """修改实验是否可见
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验id
-    request : Request
-        请求体，包含 show 字段
-
-    Returns
-    -------
-    experiment : dict
-        当前实验信息
-    """
-
-    show = (await request.json()).get("show")
-    if show is None:
-        return PARAMS_ERROR_422("Request parameter 'show'")
-    return change_experiment_visibility(experiment_id, show)
diff --git a/swanlab/server/router/media.py b/swanlab/server/router/media.py
deleted file mode 100644
index aca2ffc6..00000000
--- a/swanlab/server/router/media.py
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-29 14:14:02
-@File: swanlab\server\router\static.py
-@IDE: vscode
-@Description:
-    多媒体相关接口路由
-    暂时只有一个，用于获取媒体数据，这媒体包括音频、视频、图片、文字等，返回的接口是统一的
-    返回的数据格式也不再是JSON，而是二进制数据，这样可以减少数据传输的大小
-    我们约定前端通过一个相对路径获取媒体数据，这个路径相对于当前watch的文件夹
-"""
-
-from ..settings import get_media_dir
-from fastapi import APIRouter
-from urllib.parse import quote
-from fastapi.responses import FileResponse
-from ...db import Experiment
-import os
-
-router = APIRouter()
-
-
-# ---------------------------------- 音频相关 ----------------------------------
-
-
-@router.get("/{path:path}")
-def _(path: str, tag: str, experiment_id: str):
-    """获取媒体文件
-    通过参数拼接为本地路径，返回二进制数据
-    tag需要进行url编码
-    """
-    run_id = Experiment.get(Experiment.id == experiment_id).run_id
-    media_path = os.path.join(get_media_dir(run_id, quote(tag, safe="")), path)
-    return FileResponse(media_path)
diff --git a/swanlab/server/router/namespace.py b/swanlab/server/router/namespace.py
deleted file mode 100644
index 8867df61..00000000
--- a/swanlab/server/router/namespace.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from fastapi import APIRouter
-from ..module.resp import PARAMS_ERROR_422
-
-from ..controller.namespace import (
-    # 修改namespace开启关闭状态
-    change_namespace_opened,
-)
-from fastapi import Request
-from urllib.parse import quote
-
-router = APIRouter()
-
-
-# 修改namespace可见性
-@router.patch("/{namespace_id}/opened")
-async def _(namespace_id: int, request: Request):
-    """修改namespace可见性
-
-    Parameters
-    ----------
-    experiment_id : int
-        实验id
-    request : Request
-        请求体，包含 opened 字段
-    Returns
-    -------
-    experiment : dict
-        当前实验信息
-    """
-
-    data = await request.json()
-    opened = data.get("opened")
-    experiment_id = data.get("experiment_id")
-    project_id = data.get("project_id")
-
-    if opened is None:
-        return PARAMS_ERROR_422("Request parameter 'opened'")
-    if experiment_id is None and project_id is None:
-        return PARAMS_ERROR_422("Request parameter 'experiment_id' or 'project_id'")
-    return change_namespace_opened(namespace_id, opened, experiment_id, project_id)
diff --git a/swanlab/server/router/project.py b/swanlab/server/router/project.py
deleted file mode 100644
index b784a6f4..00000000
--- a/swanlab/server/router/project.py
+++ /dev/null
@@ -1,93 +0,0 @@
-from fastapi import APIRouter, Request
-
-from ..controller.project import (
-    # 列出实验列表
-    get_project_info,
-    # 获取项目总结信息
-    get_project_summary,
-    # 修改项目信息
-    update_project_info,
-    # 删除项目
-    delete_project,
-    # 获取多实验对比图表
-    get_project_charts,
-    # 默认项目id
-    DEFAULT_PROJECT_ID,
-)
-
-router = APIRouter()
-
-
-@router.get("")
-def _():
-    """获取项目下的实验列表
-
-    Returns
-    -------
-    list[dict]:
-        项目下的实验列表
-    """
-
-    return get_project_info()
-
-
-@router.get("/summaries")
-def _():
-    """获取项目总结信息
-
-    Returns
-    -------
-    dict
-        项目下每个实验的总结信息
-    """
-
-    return get_project_summary()
-
-
-@router.patch("/update")
-async def _(request: Request, project_id: int = None):
-    """修改项目信息
-
-    Parameters
-    ----------
-    request : Request
-        请求体，应该包含 name 和 description 字段
-    project_id : int, optional
-        by default None
-
-    Returns
-    -------
-    dict
-        修改后的项目信息
-        - name
-        - description
-    """
-
-    if project_id is None:
-        return await update_project_info(request)
-    return await update_project_info(request, project_id)
-
-
-@router.delete("")
-async def _(project_id: int = None):
-    """删除项目
-
-    Parameters
-    ----------
-    project_id : int, optional
-        默认为第一个项目
-    """
-
-    if project_id is None:
-        return await delete_project()
-    return await delete_project(project_id)
-
-
-@router.get("/charts")
-async def _(project_id: int = DEFAULT_PROJECT_ID):
-    """获取多实验对比图表数据,并且考虑往期版本兼容性
-    1. 如果当前项目的chart字段为0，先生成多实验对比数据，跳转步骤2
-    2. 依据规则获取所有实验的图表数据
-    """
-
-    return await get_project_charts(project_id)
diff --git a/swanlab/server/settings.py b/swanlab/server/settings.py
deleted file mode 100644
index c0ee8d32..00000000
--- a/swanlab/server/settings.py
+++ /dev/null
@@ -1,94 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024-01-01 15:43:21
-@File: swanlab/server/settings.py
-@IDE: vscode
-@Description:
-    服务部分基础配置
-"""
-import os
-import mimetypes
-from ..env import get_swanlog_dir
-
-"""
-在此处注册静态文件路径，因为静态文件由vue框架编译后生成，在配置中，编译后的文件存储在/swanlab/template中
-入口文件为index.html，网页图标为logo.ico，其他文件为assets文件夹中的文件
-"""
-# 注册静态文件路径
-mimetypes.add_type("application/javascript", ".js")
-mimetypes.add_type("text/css", ".css")
-# 静态文件路径
-FILEPATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-TEMPLATE_PATH = os.path.join(FILEPATH, "template")
-ASSETS = os.path.join(TEMPLATE_PATH, "assets")
-INDEX = os.path.join(TEMPLATE_PATH, "index.html")
-# swanlog文件夹路径
-SWANLOG_DIR = get_swanlog_dir()
-
-
-# ---------------------------------- 实验目录下的各个路径 ----------------------------------
-
-# tags 存储目录
-LOGS = "logs"
-# files 实验环境目录
-FILES = "files"
-# console 实验环境目录
-CONSOLE = "console"
-# media 实验生成媒体目录
-MEDIA = "media"
-# 实验环境(元数据)
-META_DATA = "swanlab-metadata.json"
-# 实验依赖
-REQUIREMENTS = "requirements.txt"
-# 数据库名
-DB_NAME = "runs.swanlab"
-# 数据库路径
-DB_PATH = os.path.join(SWANLOG_DIR, DB_NAME)
-# 实验配置
-EXPERIMENT_CONFIG = "config.yaml"
-
-
-def get_exp_dir(name) -> str:
-    """实验目录路径"""
-    return os.path.join(SWANLOG_DIR, name)
-
-
-def get_logs_dir(name) -> str:
-    """logs 目录路径，存放 tag 数据"""
-    return os.path.join(SWANLOG_DIR, name, LOGS)
-
-
-def get_files_dir(name) -> str:
-    """files 目录路径"""
-    return os.path.join(SWANLOG_DIR, name, FILES)
-
-
-def get_console_dir(name) -> str:
-    """console 目录路径"""
-    return os.path.join(SWANLOG_DIR, name, CONSOLE)
-
-
-def get_config_path(name) -> str:
-    """实验配置存储路径"""
-    return os.path.join(get_files_dir(name), EXPERIMENT_CONFIG)
-
-
-def get_meta_path(name) -> str:
-    """实验环境存储路径"""
-    return os.path.join(SWANLOG_DIR, name, FILES, META_DATA)
-
-
-def get_requirements_path(name) -> str:
-    """实验依赖存储路径"""
-    return os.path.join(SWANLOG_DIR, name, FILES, REQUIREMENTS)
-
-
-def get_tag_dir(name, tag) -> str:
-    """获取 tag 对应的目录路径"""
-    return os.path.join(SWANLOG_DIR, name, LOGS, tag)
-
-
-def get_media_dir(name, tag) -> str:
-    """获取 media 对应的目录路径"""
-    return os.path.join(SWANLOG_DIR, name, MEDIA, tag)
diff --git a/swanlab/utils/key.py b/swanlab/utils/key.py
index 66bf8fc2..cbeed2a3 100644
--- a/swanlab/utils/key.py
+++ b/swanlab/utils/key.py
@@ -22,6 +22,11 @@ def get_key(path: str, host: str):
     host : str
         token对应的host
 
+    Returns
+    -------
+    tuple
+        返回一个元组，如果要获取key，应该使用[2]索引
+
     Raises
     ------
     KeyFileError
diff --git a/tailwind.config.js b/tailwind.config.js
deleted file mode 100644
index e8b9d0ec..00000000
--- a/tailwind.config.js
+++ /dev/null
@@ -1,90 +0,0 @@
-const length = {
-  0.25: '1px',
-  4.5: '18px',
-  6.5: '26px',
-  7.5: '30px',
-  15: '60px',
-  66: '264px'
-}
-
-/** @type {import('tailwindcss').Config} */
-export default {
-  content: ['./index.html', './vue/src/**/*.{vue,js,ts,jsx,tsx}'],
-  theme: {
-    extend: {
-      margin: {
-        ...length
-      },
-      padding: {
-        ...length
-      },
-      width: {
-        ...length
-      },
-      height: {
-        ...length
-      },
-      minHeight: {
-        ...length
-      },
-      zIndex: {
-        // 最顶层，给message用
-        message: '9999',
-        // 其余最顶层
-        full: '9998'
-      },
-      // 背景颜色
-      backgroundColor: {
-        overlay: `var(--background-overlay, rgba(15, 21, 36, 0.8))`,
-        default: 'var(--background-default, #fff)',
-        higher: 'var(--background-higher, #f6f8fA)',
-        highest: 'var(--background-highest, #dadde1)',
-        dimmest: 'var(--background-dimmest, #27282E)'
-      },
-      // 边框颜色
-      borderColor: {
-        default: 'var(--outline-default, #d1d7dd)',
-        dimmer: 'var(--outline-dimmer, #e5e8eb)',
-        dimmest: 'var(--outline-dimmest, #f2f2f2)'
-      },
-      // 文字颜色
-      textColor: {
-        default: 'var(--foreground-default, #000)',
-        dimmer: 'var(--foreground-dimmer, #555)',
-        dimmest: 'var(--foreground-dimmest, #b5b5b5)'
-      }
-    },
-    // 基础颜色
-    colors: {
-      transparent: 'transparent',
-      current: 'currentColor',
-      /* 成功颜色 */
-      'positive-highest': 'var(--positive-highest, #004d0d)',
-      'positive-higher': 'var(--positive-higher, #036e15)',
-      'positive-default': 'var(--positive-default, #00a11b)',
-      'positive-dimmer': 'var(--positive-dimmer, #3cc954)',
-      'positive-dimmest': 'var(--positive-dimmest, #97e896)',
-      /* 标准颜色 */
-      'primary-highest': 'var(--primary-highest, #004182)',
-      'primary-higher': 'var(--primary-higher, #005cb8)',
-      'primary-default': 'var(--primary-default, #0f87ff)',
-      'primary-dimmer': 'var(--primary-dimmer, #74b9ff)',
-      'primary-dimmest': 'var(--primary-dimmest, #b2d9ff)',
-      /* 警告颜色 */
-      'warning-dimmer': 'var(--warning-dimmer, #ff9933)',
-      'warning-dimmest': 'var(--warning-dimmest, #ffad5a)',
-      /* 危险颜色 */
-      'negative-highest': 'var(--negative-highest, #8a0000)',
-      'negative-higher': 'var(--negative-higher, #c20a0a)',
-      'negative-default': 'var(--negative-default, #fa4b4b)',
-      'negative-dimmest': 'var(--negative-dimmest, #ff9494)',
-      'negative-dimmer': 'var(--negative-dimmer, #ffc7c7)',
-      /** 辅助高亮颜色 */
-      'white-highest': 'var(--accent-white-highest, #B5B5B5)',
-      'white-higher': 'var(--accent-white-higher, #DADDE1)',
-      'white-default': 'var(--accent-white-default, #fff)',
-      'green-default': 'var(--accent-green-default, #99A559)'
-    }
-  },
-  plugins: []
-}
diff --git a/tutils/__init__.py b/tutils/__init__.py
index dbb34372..987cf8f9 100644
--- a/tutils/__init__.py
+++ b/tutils/__init__.py
@@ -29,9 +29,9 @@ def init_db():
     """
     初始化数据库
     """
-    from swanlab.db import connect, Project
+    from swanboard.db import connect, Project
     clear()
-    connect(autocreate=True)
+    connect(autocreate=True, path=SWANLAB_LOG_DIR)
     Project.init(name="pytest-swanlab", description="测试swanlab")
 
 
diff --git a/vite.config.js b/vite.config.js
deleted file mode 100644
index fbcc7a05..00000000
--- a/vite.config.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import { defineConfig, loadEnv } from 'vite'
-import AutoImport from 'unplugin-auto-import/vite'
-import Components from 'unplugin-vue-components/vite'
-import json5Plugin from 'vite-plugin-json5'
-import vue from '@vitejs/plugin-vue'
-import path from 'path'
-// https://vitejs.dev/config/
-export default defineConfig(({ mode }) => {
-  const env = loadEnv(mode, path.resolve(process.cwd(), 'vue'))
-  // console.log('当前模式：', mode)
-  // console.log('当前环境：', env)
-  // 如果使用mock模式，不使用代理
-  const proxy = {
-    '/api': {
-      target: env.VITE_SERVER_PROXY,
-      changeOrigin: true
-    }
-  }
-  return {
-    // 服务插件
-    plugins: [
-      json5Plugin(),
-      vue(),
-      // 自动化导入
-      AutoImport({
-        imports: ['vue', 'vue-router'],
-        dts: 'auto-imports.d.ts',
-        eslintrc: {
-          enabled: true
-        }
-      }),
-      // 自动导入组件，自定义组件库
-      Components({
-        // 指定组件所在文件夹的位置
-        dirs: ['src/components', 'src/layouts'],
-        // 文件扩展名
-        extensions: ['vue'],
-        // 配置type文件生成位置
-        dts: 'components.d.ts'
-      })
-    ],
-    root: 'vue',
-    // 重定向
-    resolve: {
-      alias: {
-        '@swanlab-vue': path.resolve(__dirname, 'vue/src')
-      }
-    },
-    // 标明编译后存放的位置
-    build: {
-      outDir: path.resolve(__dirname, 'swanlab/template'),
-      emptyOutDir: true,
-      minify: 'terser',
-      // 根据模式应用不同的 terser 配置
-      terserOptions: {
-        // 生产环境移除console
-        compress: {
-          drop_console: mode === 'release',
-          drop_debugger: mode === 'release'
-        }
-      }
-    },
-    // 服务配置
-    server: {
-      proxy,
-      host: '0.0.0.0',
-      port: 5175,
-      open: '.'
-    }
-  }
-})
diff --git a/vue/.env b/vue/.env
deleted file mode 100644
index d8ece9f9..00000000
--- a/vue/.env
+++ /dev/null
@@ -1,1 +0,0 @@
-VITE_BASE_URL="/api/v1"
diff --git a/vue/.env.development b/vue/.env.development
deleted file mode 100644
index c5938b7b..00000000
--- a/vue/.env.development
+++ /dev/null
@@ -1,1 +0,0 @@
-VITE_SERVER_PROXY="http://127.0.0.1:6092"
diff --git a/vue/index.html b/vue/index.html
deleted file mode 100644
index 82c77dcf..00000000
--- a/vue/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <link rel="icon" type="image/vnd.microsoft.icon" href="/src/assets/logo.ico" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>SwanLab</title>
-  </head>
-  <body>
-    <div id="app"></div>
-    <script type="module" src="/src/main.js"></script>
-  </body>
-</html>
diff --git a/vue/src/App.vue b/vue/src/App.vue
deleted file mode 100644
index e81d5638..00000000
--- a/vue/src/App.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<template>
-  <MainLayout :show-side-bar="!errorCode" v-if="ready">
-    <router-view v-if="!errorCode" />
-    <ErrorView :code="errorCode" :message="errorMessage" v-else />
-  </MainLayout>
-  <!-- 全局气泡提示 -->
-  <SLMessages ref="messagesRef" />
-  <!-- 全局确认弹窗 -->
-  <SLConfirm ref="confirmRef" />
-</template>
-
-<script setup>
-import MainLayout from './layouts/main/MainLayout.vue'
-import ErrorView from './views/error/ErrorView.vue'
-import http from './api/http'
-import { useProjectStore } from '@swanlab-vue/store'
-import { computed } from 'vue'
-import { ref } from 'vue'
-import { useRoute } from 'vue-router'
-import { watch } from 'vue'
-import { installMessage, SLMessages, message } from '@swanlab-vue/components/message'
-import { installConfirm, SLConfirm } from './components/confirm'
-import { onMounted } from 'vue'
-
-// ---------------------------------- state ----------------------------------
-
-const projectStore = useProjectStore()
-const ready = ref()
-
-// ---------------------------------- 在此处请求项目信息 ----------------------------------
-http
-  .get('/project')
-  .then(({ data }) => {
-    projectStore.setProject(data)
-  })
-  .catch((response) => {
-    // console.error(response)
-    errorCode.value = response.data?.code || 3000 // 3000 时，后端启动失败
-  })
-  .finally(() => {
-    ready.value = true
-  })
-
-// ---------------------------------- 错误处理 ----------------------------------
-
-const errorCode = ref(0) // 错误码
-const errorMessage = ref('') // 错误信息
-const route = useRoute()
-
-// 监测路由修改
-watch(
-  computed(() => route.fullPath),
-  (oldVal) => {
-    // console.log('route change', newVal, oldVal)
-    if (oldVal === undefined) return
-    // 清除消息弹窗
-    message.clear()
-  }
-)
-
-// ---------------------------------- 项目配置 ----------------------------------
-
-const messagesRef = ref(null)
-const confirmRef = ref(null)
-
-onMounted(() => {
-  // 注册全局顶部提醒
-  installMessage(messagesRef)
-  installConfirm(confirmRef)
-})
-</script>
-
-<style scoped></style>
diff --git a/vue/src/api/chart.js b/vue/src/api/chart.js
deleted file mode 100644
index 5cad7365..00000000
--- a/vue/src/api/chart.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * chart组件相关api，包括获取媒体文件，更改namespace展开状态、chart置顶等
- */
-import http from './http'
-
-/**
- * 获取媒体文件，获取blob对象
- * 返回promise，如果成功，返回blob对象，否则返回错误信息
- */
-export const media = {
-  /**
-   * 获取媒体文件，获取blob对象
-   * 返回promise，如果成功，返回blob对象，否则返回错误信息
-   * @param { string } data 即文件名，即数据中的data字段
-   * @param { string } experiment_id 实验id
-   * @param { string } tag 数据名称
-   * @returns { Promise<Blob> }
-   */
-  get: (data, experiment_id, tag) => {
-    return new Promise((resolve, reject) => {
-      http
-        .get('/media/' + data, { params: { tag, experiment_id }, responseType: 'blob' })
-        .then((res) => {
-          resolve(res)
-        })
-        .catch((err) => {
-          // 如果blob的type为application/json，说明是后端返回的错误信息，需要解析
-          if (err.data.type === 'application/json') {
-            const reader = new FileReader()
-            reader.readAsText(err.data)
-            reader.onload = () => {
-              reject(JSON.parse(reader.result))
-            }
-          } else reject(err)
-        })
-    })
-  }
-}
-
-/**
- * 更新命名空间展开状态
- * @param { boolean } opened 是否展开
- * @param { object } namespace 命名空间对象
- * @returns { Promise }
- */
-export const updateNamespaceStatus = (opened, namespace) => {
-  return http.patch('/namespace/' + namespace.id + '/opened', {
-    experiment_id: namespace.experiment_id?.id,
-    project_id: namespace.project_id?.id,
-    opened
-  })
-}
-
-/**
- * 更新chart状态，置顶、隐藏或正常显示
- * @param { object } chart 图表对象
- * @param { int } status 状态码，0为正常，1为置顶，-1为隐藏
- * @returns { Promise } Promise对象，最终可返回更新后的图表组织结构
- */
-export const updateChartStatus = async (chart, status) => {
-  const { data } = await http.patch('/chart/' + chart.id + '/status', {
-    status
-  })
-  return data
-}
diff --git a/vue/src/api/http.js b/vue/src/api/http.js
deleted file mode 100644
index 1f48a1da..00000000
--- a/vue/src/api/http.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import axios from 'axios'
-
-// axios对象实例
-const http = axios.create({
-  // baseURL不设置，会自动使用当前域名
-  baseURL: import.meta.env.VITE_BASE_URL,
-  timeout: 60000
-})
-
-// 请求拦截器
-http.interceptors.request.use(
-  async (req) => {
-    console.log('[request] ', req.method, req.url, req.data || req.params || '')
-
-    return req
-  },
-  (error) => {
-    console.log('[request error] ', error)
-    return Promise.reject(error)
-  }
-)
-
-// 响应拦截器
-http.interceptors.response.use(
-  // 成功拦截
-  (resp) => {
-    // 打印响应信息
-    console.log('[response] ', resp.config.url, resp.data)
-    const data = resp.data
-    return data
-  },
-  // 失败拦截
-  (error) => {
-    // 判断连接是否超时
-    if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1) {
-      console.log('[response error] ', 'timeout')
-    } else console.log('[response error] ', error.response?.status, error.response?.data, error)
-    return Promise.reject(error.response)
-  }
-)
-
-export default http
diff --git a/vue/src/assets/font/JetBrainsMono-Regular.ttf b/vue/src/assets/font/JetBrainsMono-Regular.ttf
deleted file mode 100644
index dff66cc5..00000000
Binary files a/vue/src/assets/font/JetBrainsMono-Regular.ttf and /dev/null differ
diff --git a/vue/src/assets/font/SourceSansPro-Regular.ttf b/vue/src/assets/font/SourceSansPro-Regular.ttf
deleted file mode 100644
index ffe27865..00000000
Binary files a/vue/src/assets/font/SourceSansPro-Regular.ttf and /dev/null differ
diff --git a/vue/src/assets/iconfont.js b/vue/src/assets/iconfont.js
deleted file mode 100644
index c0fa8a85..00000000
--- a/vue/src/assets/iconfont.js
+++ /dev/null
@@ -1,1 +0,0 @@
-window._iconfont_svg_string_4357075='<svg><symbol id="icon-loading" viewBox="0 0 1024 1024"><path d="M512 96a416 416 0 0 0 0 832V824a312 312 0 1 1 312-312h104A416 416 0 0 0 512 96z"  ></path></symbol><symbol id="icon-pinned" viewBox="0 0 1024 1024"><path d="M648.704 130.773333c8.490667 3.669333 16.213333 8.917333 22.698667 15.445334l191.573333 191.744a73.173333 73.173333 0 0 1-22.186667 118.613333l-67.84 30.037333-127.36 127.488-10.069333 140.245334a73.130667 73.130667 0 0 1-124.714667 46.421333l-123.648-123.776-210.730666 211.712-51.84-51.626667 210.858666-211.84-127.914666-128a73.130667 73.130667 0 0 1 46.293333-124.629333l144.213333-10.794667 125.098667-125.226666 29.397333-67.754667a73.173333 73.173333 0 0 1 96.170667-38.058667z"  ></path></symbol><symbol id="icon-more" viewBox="0 0 1024 1024"><path d="M144 512a80 80 0 1 0 160 0 80 80 0 0 0-160 0zM432 512a80 80 0 1 0 160 0 80 80 0 0 0-160 0zM720 512a80 80 0 1 0 160 0 80 80 0 0 0-160 0z"  ></path></symbol><symbol id="icon-pin1" viewBox="0 0 1024 1024"><path d="M711.626105 57.397895c10.725053 4.634947 20.48 11.264 28.672 19.509894l241.987369 242.202948a92.429474 92.429474 0 0 1-28.025263 149.827368l-85.692632 37.941895-160.87579 161.037474-12.719157 177.152a92.375579 92.375579 0 0 1-157.534316 58.637473l-156.186948-156.348631-266.186105 267.425684-65.482105-65.212632 266.347789-267.587368-161.576421-161.684211a92.375579 92.375579 0 0 1 58.47579-157.426526l182.16421-13.635368 158.019369-158.181053 37.133473-85.584842a92.429474 92.429474 0 0 1 121.478737-48.074105z m-36.702316 84.776421l-44.139789 101.753263-194.613895 194.937263-216.495158 16.114526 383.137685 383.407158 15.198315-211.429052 197.362527-197.632 101.537684-44.948211-241.987369-242.202947z"  ></path></symbol><symbol id="icon-smooth" viewBox="0 0 1024 1024"><path d="M533.12 10.666667a480 480 0 1 1 0 960 480 480 0 0 1 0-960z m0 96a384 384 0 1 0 0 768 384 384 0 0 0 0-768z"  ></path><path d="M449.066667 529.066667h38.4v38.4h-38.4v-38.4z m75.264 105.642666H389.12v-154.112c-22.357333-51.456-42.325333-124.373333-42.325333-220.288h58.666666c0 97.194667 23.04 167.338667 45.056 212.48h100.864v109.824c2.432 1.28 5.077333 2.816 7.68 4.138667 42.368 22.784 115.2 50.133333 222.933334 50.133333V695.466667c-120.064 0-203.861333-30.421333-257.536-60.8h-0.085334z"  ></path></symbol><symbol id="icon-chart" viewBox="0 0 1075 1024"><path d="M913.6128 792.0128H225.5872V167.936a8.0384 8.0384 0 0 0-7.9872-7.9872H161.5872A8.0384 8.0384 0 0 0 153.6 167.9872V856.064c0 4.4032 3.584 7.9872 7.9872 7.9872h752.0256c4.4032 0 7.9872-3.584 7.9872-7.9872v-56.0128a8.0384 8.0384 0 0 0-7.9872-7.9872z"  ></path><path d="M331.4176 637.696c3.072 3.072 8.0896 3.072 11.264 0l138.3424-137.5744 127.5904 128.3584c3.072 3.072 8.192 3.072 11.264 0l275.456-275.3024c3.072-3.072 3.072-8.192 0-11.264l-39.6288-39.6288a8.0384 8.0384 0 0 0-11.264 0L614.4 532.1728 487.0144 404.0704a8.0384 8.0384 0 0 0-11.3152 0l-183.808 182.6816c-3.072 3.072-3.072 8.192 0 11.264l39.5264 39.7312z"  ></path></symbol><symbol id="icon-charts" viewBox="0 0 1024 1024"><path d="M962.56 860.2112H137.472V111.9744A9.6256 9.6256 0 0 0 128 102.4H60.7744A9.6256 9.6256 0 0 0 51.2 111.9744V936.96c0 5.2736 4.3008 9.6256 9.5744 9.6256H962.56a9.6256 9.6256 0 0 0 9.6256-9.6256V869.888a9.6256 9.6256 0 0 0-9.6256-9.6256z"  ></path><path d="M234.7008 755.0976c3.0208 4.3008 8.9088 5.3248 13.312 2.304l191.5904-134.2464 124.4672 177.8176c3.072 4.2496 9.0624 5.3248 13.3632 2.304l381.7472-268.9024a9.6256 9.6256 0 0 0 2.304-13.312l-38.7072-54.9376a9.6256 9.6256 0 0 0-13.312-2.304l-318.8224 224.5632L466.432 510.7712a9.6256 9.6256 0 0 0-13.3632-2.304l-254.5664 178.176a9.6256 9.6256 0 0 0-2.304 13.4144l38.5536 54.9888z"  ></path><path d="M220.7232 520.1408c3.7376 3.6864 9.728 3.6864 13.568 0l165.8368-165.0176 152.9856 153.9584a9.6256 9.6256 0 0 0 13.568 0l330.24-330.0864a9.6256 9.6256 0 0 0 0-13.568l-47.5136-47.5136a9.6256 9.6256 0 0 0-13.5168 0l-275.8144 275.712-152.7808-153.7536a9.6256 9.6256 0 0 0-13.5168 0l-220.416 219.136a9.6256 9.6256 0 0 0 0 13.5168l47.36 47.616z"  ></path></symbol><symbol id="icon-pause" viewBox="0 0 1024 1024"><path d="M688 288H336a48 48 0 0 0-48 48v352a48 48 0 0 0 48 48h352a48 48 0 0 0 48-48V336a48 48 0 0 0-48-48z"  ></path></symbol><symbol id="icon-play" viewBox="0 0 1024 1024"><path d="M817.088 484.96l-512-323.744c-9.856-6.24-22.336-6.624-32.512-0.992A31.993 31.993 0 0 0 256 188.256v647.328c0 11.648 6.336 22.4 16.576 28.032A31.82 31.82 0 0 0 288 867.584a32.107 32.107 0 0 0 17.088-4.928l512-323.616A31.976 31.976 0 0 0 832 512a31.976 31.976 0 0 0-14.912-27.04z"  ></path></symbol><symbol id="icon-eye-close" viewBox="0 0 1228 1024"><path d="M1041.408 273.92c-131.328 163.2768-262.5024 242.4832-393.8816 242.4832-131.84 0-268.4416-79.7696-409.6-243.712a47.9744 47.9744 0 1 0-72.704 62.5664c157.696 183.296 318.464 277.0944 482.304 277.0944 164.4032 0 320.7168-94.3616 468.6336-278.3232a47.9744 47.9744 0 0 0-74.752-60.1088z"  ></path><path d="M303.616 461.6704l-91.4432 105.728a34.304 34.304 0 0 0 51.8656 44.8512l91.392-105.728a34.304 34.304 0 1 0-51.8656-44.8512z m184.5248 124.0576l-36.5568 146.2272a34.304 34.304 0 1 0 66.5088 16.64l36.5568-146.2272a34.304 34.304 0 0 0-66.5088-16.64z m265.3184 5.9904l51.4048 146.176a34.304 34.304 0 0 0 64.6656-22.7328l-51.4048-146.176a34.304 34.304 0 0 0-64.6656 22.7328z m186.5216-124.5184l105.728 105.728a34.304 34.304 0 0 0 48.4864-48.4864l-105.728-105.728a34.304 34.304 0 0 0-48.4864 48.4864z"  ></path></symbol><symbol id="icon-eye" viewBox="0 0 1228 1024"><path d="M614.4 832c-187.7504 0-355.2256-98.7136-502.0672-291.584A46.7456 46.7456 0 0 1 102.4 512c0-10.0352 3.4304-19.9168 9.9328-28.416C259.1744 290.7136 426.7008 192 614.4 192c187.7504 0 355.2256 98.7136 502.1184 291.584A46.7456 46.7456 0 0 1 1126.4 512a46.7456 46.7456 0 0 1-9.8816 28.416c-146.8928 192.8704-314.368 291.584-502.1184 291.584zM614.4 256c-155.392 0-299.3664 83.8656-432.128 256 132.7616 172.1344 276.736 256 432.128 256 155.4432 0 299.3664-83.8656 432.128-256-132.7616-172.1344-276.6848-256-432.128-256z m0 405.3504c-99.84 0-180.7872-66.8672-180.7872-149.3504 0-82.432 80.896-149.3504 180.7872-149.3504 99.84 0 180.7872 66.8672 180.7872 149.3504 0 82.432-80.896 149.3504-180.736 149.3504z m0-64c57.088 0 103.3216-38.2464 103.3216-85.3504S671.488 426.6496 614.4 426.6496c-57.0368 0-103.3216 38.2464-103.3216 85.3504s46.2848 85.3504 103.3216 85.3504z"  ></path></symbol><symbol id="icon-experiment" viewBox="0 0 1024 1024"><path d="M882.580211 421.295158a406.258526 406.258526 0 0 0-87.309474-129.509053A406.312421 406.312421 0 0 0 507.149474 172.463158a406.258526 406.258526 0 0 0-288.121263 119.322947A406.258526 406.258526 0 0 0 99.705263 579.907368c0 120.724211 53.032421 234.388211 145.408 312.050527l1.616842 1.293473a29.480421 29.480421 0 0 0 18.701474 6.790737h483.543579c6.844632 0 13.473684-2.425263 18.755368-6.790737l1.562948-1.293473a406.905263 406.905263 0 0 0 145.30021-312.050527c0-54.972632-10.778947-108.328421-32.013473-158.61221z m-148.58779 409.6H280.252632a337.542737 337.542737 0 0 1-111.454316-250.98779 335.764211 335.764211 0 0 1 99.112421-239.184842 336.087579 336.087579 0 0 1 239.184842-99.166315 335.764211 335.764211 0 0 1 239.184842 99.166315 336.087579 336.087579 0 0 1 99.166316 239.184842c0 96.040421-40.474947 186.906947-111.562105 251.041685z"  ></path><path d="M608.579368 453.955368a7.275789 7.275789 0 0 0-10.24 0L521.377684 530.863158a50.876632 50.876632 0 1 0 22.851369 85.207579 50.822737 50.822737 0 0 0 13.20421-49.205895l76.8-76.853895a7.275789 7.275789 0 0 0 0-10.24l-25.707789-25.761684zM487.154526 361.633684h40.04379c3.988211 0 7.275789-3.233684 7.275789-7.275789v-72.757895a7.275789 7.275789 0 0 0-7.275789-7.275789h-40.04379a7.275789 7.275789 0 0 0-7.275789 7.275789v72.757895c0 4.042105 3.233684 7.275789 7.275789 7.275789z m236.490106 198.332632v39.989895c0 3.988211 3.233684 7.275789 7.275789 7.275789h72.757895c3.988211 0 7.221895-3.287579 7.221895-7.275789v-40.04379a7.275789 7.275789 0 0 0-7.221895-7.275789h-72.757895a7.275789 7.275789 0 0 0-7.275789 7.275789z m11.533473-179.415579l-28.294737-28.240842a7.275789 7.275789 0 0 0-10.24 0l-51.523368 51.469473a7.275789 7.275789 0 0 0 0 10.24l28.294737 28.294737a7.275789 7.275789 0 0 0 10.24 0l51.523368-51.469473a7.275789 7.275789 0 0 0 0-10.24z m-417.091368-28.240842a7.275789 7.275789 0 0 0-10.24 0l-28.348632 28.240842a7.275789 7.275789 0 0 0 0 10.293895l51.523369 51.469473a7.275789 7.275789 0 0 0 10.24 0l28.294737-28.294737a7.275789 7.275789 0 0 0 0-10.24l-51.469474-51.469473z m-38.265263 200.326737h-72.757895a7.275789 7.275789 0 0 0-7.27579 7.275789v40.04379c0 3.988211 3.233684 7.275789 7.27579 7.275789h72.757895c3.988211 0 7.275789-3.287579 7.275789-7.275789v-40.04379a7.275789 7.275789 0 0 0-7.275789-7.275789z"  ></path></symbol><symbol id="icon-runs" viewBox="0 0 1024 1024"><path d="M51.2 152.0128A100.864 100.864 0 0 1 152.0128 51.2H871.936A100.864 100.864 0 0 1 972.8 152.0128V871.936A100.8128 100.8128 0 0 1 871.9872 972.8H152.064A100.8128 100.8128 0 0 1 51.2 871.9872V152.064z m374.4256 273.6128v460.8h446.3616a14.3872 14.3872 0 0 0 14.4384-14.4384V425.6256h-460.8z m460.8-86.4256V152.0128a14.3872 14.3872 0 0 0-14.4384-14.3872H425.6256v201.5744h460.8zM137.6256 425.6256v446.3616c0 7.936 6.4 14.4384 14.336 14.4384h187.2384v-460.8H137.5744z m201.5744-86.4256V137.5744H152.0128a14.3872 14.3872 0 0 0-14.3872 14.4384v187.1872h201.5744z"  ></path></symbol><symbol id="icon-success" viewBox="0 0 1024 1024"><path d="M515.072 40.384C256.768 40.384 46.592 250.624 46.592 508.928c0 258.368 210.176 468.544 468.48 468.544 258.368 0 468.544-210.176 468.544-468.48C983.68 250.56 773.44 40.32 515.2 40.32z m0 875.648a407.552 407.552 0 0 1-407.04-407.04 407.552 407.552 0 0 1 407.04-407.168 407.552 407.552 0 0 1 407.104 407.104 407.552 407.552 0 0 1-407.04 407.104z"  ></path><path d="M678.272 363.2L451.968 589.504 352 489.408a30.72 30.72 0 1 0-43.456 43.456l121.792 121.792a30.72 30.72 0 0 0 43.456 0l248-248a30.72 30.72 0 1 0-43.52-43.456z"  ></path></symbol><symbol id="icon-error" viewBox="0 0 1024 1024"><path d="M512 42.688a469.312 469.312 0 1 1 0 938.624A469.312 469.312 0 0 1 512 42.688z m0 64a405.376 405.376 0 1 0 0 810.688A405.376 405.376 0 0 0 512 106.624z m-104.704 256a8.512 8.512 0 0 1 6.016 2.432L512 463.808l98.688-98.688a8.512 8.512 0 0 1 6.08-2.496h66.304a8.512 8.512 0 0 1 6.08 14.592l-131.84 131.84 137.6 137.664a8.576 8.576 0 0 1-6.016 14.592H622.528a8.512 8.512 0 0 1-6.016-2.496L512 554.304l-104.512 104.512a8.512 8.512 0 0 1-6.016 2.56H335.104a8.576 8.576 0 0 1-6.016-14.656l137.664-137.6-131.84-131.84a8.512 8.512 0 0 1 6.016-14.592h66.368z"  ></path></symbol><symbol id="icon-circle-fill" viewBox="0 0 1024 1024"><path d="M512 512m-384 0a384 384 0 1 0 768 0 384 384 0 1 0-768 0Z"  ></path></symbol><symbol id="icon-model" viewBox="0 0 1075 1024"><path d="M539.5456 921.6c-5.12 0-10.24-1.28-14.8992-3.8912l-330.496-189.952a29.7984 29.7984 0 0 1-14.9504-25.856V322.2016c0-10.5984 5.7344-20.48 14.9504-25.8048l330.496-189.952a29.7984 29.7984 0 0 1 29.8496 0l330.496 189.952c9.216 5.376 14.9504 15.104 14.9504 25.8048v379.8528c0 10.5472-5.7344 20.48-14.9504 25.8048l-330.496 189.8496a30.5152 30.5152 0 0 1-14.9504 3.8912z m-300.6464-236.6976l300.6464 172.7488 300.6464-172.7488v-345.6l-300.6464-172.7488-300.6464 172.7488v345.6z m300.6464-143.1552a29.952 29.952 0 0 1-14.8992-3.9936L287.8976 401.8688a29.696 29.696 0 0 1-11.008-40.704 30.0032 30.0032 0 0 1 40.8576-10.9056l236.7488 135.9872a29.696 29.696 0 0 1-14.9504 55.552z m0 272.1792a29.7984 29.7984 0 0 1-29.8496-29.7472v-254.8736l-221.5424-127.488s-0.1536 0-0.1536-0.1024L194.2528 347.8528A29.696 29.696 0 0 1 183.2448 307.2a29.952 29.952 0 0 1 40.8576-10.9056l330.496 189.952c9.216 5.376 14.9504 15.104 14.9504 25.8048v272.0256a30.0032 30.0032 0 0 1-30.0032 29.7984z m0 107.6736a29.7984 29.7984 0 0 1-29.8496-29.696V512c0-16.384 13.3632-29.696 29.8496-29.696 16.4864 0 29.8496 13.312 29.8496 29.696v379.8528a29.696 29.696 0 0 1-29.8496 29.696z m0-379.8016a29.696 29.696 0 0 1-14.9504-55.5008l330.5472-190.0032a30.0032 30.0032 0 0 1 40.8064 10.9056 29.696 29.696 0 0 1-10.9568 40.6528l-330.496 190.0032a29.184 29.184 0 0 1-14.9504 3.8912z"  ></path></symbol><symbol id="icon-logs" viewBox="0 0 1075 1024"><path d="M238.9504 298.6496a42.6496 42.6496 0 1 0 0-85.2992 42.6496 42.6496 0 0 0 0 85.2992zM409.6 224a32 32 0 0 0 0 64h426.6496a32 32 0 1 0 0-64H409.6zM409.6 736a32 32 0 1 0 0 64h426.6496a32 32 0 1 0 0-64H409.6zM377.6 512c0-17.664 14.336-32 32-32h426.6496a32 32 0 1 1 0 64H409.6A32 32 0 0 1 377.6 512zM238.9504 554.6496a42.6496 42.6496 0 1 0 0-85.2992 42.6496 42.6496 0 0 0 0 85.2992zM281.6 768a42.6496 42.6496 0 1 1-85.3504 0A42.6496 42.6496 0 0 1 281.6 768z"  ></path></symbol><symbol id="icon-download" viewBox="0 0 1024 1024"><path d="M943.766588 737.581176c-1.204706-28.912941-20.178824-44.935529-45.17647-44.333176-24.696471 0.602353-41.080471 16.865882-43.369412 46.441412-1.746824 21.805176-0.301176 43.791059-0.783059 65.65647-1.204706 54.874353-13.010824 67.584-67.945412 68.668236-52.826353 1.024-105.773176 0.361412-158.659764 0.361411-122.157176 0-244.314353 0.060235-366.531765-0.12047-84.148706-0.060235-91.437176-7.348706-91.858824-89.148235 0-14.576941 0.662588-29.274353-0.602353-43.730824-2.891294-30.840471-17.347765-46.863059-41.803294-48.188235-26.383059-1.566118-45.658353 16.384-47.646117 48.188235-1.807059 29.033412-0.783059 58.247529-0.602353 87.461647 0.602353 94.448941 42.465882 136.854588 136.914823 137.336471 98.484706 0.602353 196.969412 0.180706 295.454118 0.180706h87.521882c74.752 0 149.564235 1.024 224.316236-0.421648 65.897412-1.204706 114.085647-39.333647 119.627294-97.159529 4.216471-43.369412 2.951529-87.521882 1.14447-131.192471z"  ></path><path d="M179.139765 447.186824l282.081882 282.142117 11.625412 11.565177c15.058824 15.36 32.768 23.973647 54.151529 15.902117a45.296941 45.296941 0 0 0 16.564706-11.143529c2.951529-2.048 5.842824-4.276706 8.432941-6.806588 48.308706-46.983529 254.313412-253.650824 301.899294-301.417412 5.12-5.180235 9.999059-10.661647 14.034824-16.685177 12.950588-19.215059 11.203765-38.068706-4.216471-54.874353-15.661176-17.227294-35.418353-17.950118-54.874353-8.432941-9.456941 4.698353-17.287529 13.251765-24.997647 20.901647-31.984941 31.744-147.877647 147.516235-225.40047 225.15953V239.495529c0-36.442353 1.204706-72.944941-0.602353-109.266823-1.746824-33.792-18.070588-51.019294-44.513883-51.862588-27.105882-0.903529-43.489882 15.600941-47.164235 48.850823-1.385412 12.649412-0.602353 25.479529-0.662588 38.249412v420.321882l-12.528941 5.481412-72.282353-71.981176c-50.236235-50.236235-99.870118-101.135059-151.009883-150.40753-23.070118-22.287059-49.874824-22.648471-68.246588-5.12-19.817412 18.853647-19.094588 41.321412 2.469647 67.704471 4.577882 5.662118 10.059294 10.541176 15.23953 15.661176z"  ></path></symbol><symbol id="icon-sidebar" viewBox="0 0 1024 1024"><path d="M116.14 761.521h788.995v78.926H116.14v-78.926zM550.058 485.37h355.078v78.926H550.057v-78.926z m0-276.152h355.078v78.926H550.057v-78.926zM130.818 406.443L354.061 183.29l55.81 55.81L186.63 462.255l-55.81-55.81z"  ></path><path d="M351.688 629.686L128.445 406.443l55.81-55.81L407.41 573.875l-55.723 55.81z"  ></path></symbol><symbol id="icon-trash" viewBox="0 0 1024 1024"><path d="M602.232471 753.061647a30.057412 30.057412 0 0 1-30.117647-30.117647V391.529412a30.057412 30.057412 0 1 1 60.235294 0v331.233882a30.177882 30.177882 0 0 1-30.117647 30.298353zM421.586824 753.061647a30.057412 30.057412 0 0 1-30.117648-30.117647V391.529412a30.057412 30.057412 0 1 1 60.235295 0v331.233882a30.177882 30.177882 0 0 1-30.117647 30.298353zM903.529412 240.941176H752.941176V180.705882c0-49.874824-40.116706-90.352941-89.630117-90.352941H361.351529A90.352941 90.352941 0 0 0 271.058824 180.705882v60.235294H120.470588a30.057412 30.057412 0 1 0 0 60.235295H903.529412a30.057412 30.057412 0 1 0 0-60.235295zM331.294118 180.705882c0-16.504471 13.552941-30.117647 30.117647-30.117647h301.899294c16.504471 0 29.455059 13.251765 29.455059 30.117647v60.235294H331.233882V180.705882z"  ></path><path d="M722.823529 933.647059H301.357176a90.352941 90.352941 0 0 1-90.352941-90.352941V391.228235a30.057412 30.057412 0 1 1 60.235294 0v452.186353a30.117647 30.117647 0 0 0 30.117647 30.117647h421.707295c16.685176 0 30.117647-13.432471 30.117647-30.117647V392.312471a30.057412 30.057412 0 1 1 60.235294 0V843.294118a90.714353 90.714353 0 0 1-90.473412 90.352941z"  ></path></symbol><symbol id="icon-modify" viewBox="0 0 1024 1024"><path d="M65.250485 1015.627294a40.176941 40.176941 0 0 1 0-79.751529h893.409882a40.176941 40.176941 0 0 1 0 79.751529H65.250485zM801.084838 78.787765l89.931294 89.991529a63.849412 63.849412 0 0 1 0 89.99153l-570.488471 568.621176a63.849412 63.849412 0 0 1-45.357176 18.492235H156.567191a31.924706 31.924706 0 0 1-31.924706-31.924706V692.103529a63.849412 63.849412 0 0 1 18.492235-45.296941l568.018824-568.018823a63.849412 63.849412 0 0 1 89.991529 0zM204.333779 698.488471v67.644235h67.644235l552.056471-552.056471-67.644236-68.246588-552.05647 552.658824z"  ></path></symbol><symbol id="icon-info" viewBox="0 0 1024 1024"><path d="M512 958.0032c-245.9136 0-446.0032-200.0384-446.0032-446.0032 0-245.9136 200.0896-446.0032 445.952-446.0032 246.016 0 446.0544 200.0384 446.0544 446.0032S757.9648 958.0032 512 958.0032zM512 130.048c-210.688 0-382.0032 171.3664-382.0032 382.0032 0 210.6368 171.3152 382.0032 381.952 382.0032 210.688 0 382.0544-171.3664 382.0544-382.0032 0-210.6368-171.3664-382.0032-382.0032-382.0032z"  ></path><path d="M464.0256 304.0256a48.0256 48.0256 0 1 0 96 0 48.0256 48.0256 0 0 0-96.0512 0zM512 768a32 32 0 0 1-32-32V448a32 32 0 0 1 64 0v288c0 17.7152-14.336 32-32 32z"  ></path></symbol><symbol id="icon-close" viewBox="0 0 1024 1024"><path d="M587.264 506.197333l397.141333-397.226666a52.053333 52.053333 0 0 0 0-73.130667L982.186667 33.621333a51.968 51.968 0 0 0-73.130667 0L511.914667 431.445333 114.858667 33.621333a52.053333 52.053333 0 0 0-73.130667 0L39.594667 35.84a51.029333 51.029333 0 0 0 0 73.130667l397.056 397.226666-397.056 397.226667a52.053333 52.053333 0 0 0 0 73.130667l2.133333 2.218666a51.968 51.968 0 0 0 73.130667 0L512 581.546667l397.141333 397.226666a51.968 51.968 0 0 0 73.130667 0l2.133333-2.133333a52.053333 52.053333 0 0 0 0-73.130667L587.264 506.197333z"  ></path></symbol><symbol id="icon-zoom" viewBox="0 0 1024 1024"><path d="M429.91 872.79h-211.2l232.106-232.107a43.947 43.947 0 1 0-62.208-62.166L152.747 814.208V604.8a43.99 43.99 0 0 0-87.979 0v312.021c0 24.278 19.67 43.947 43.99 43.947h320.98a43.99 43.99 0 0 0 0.129-87.979zM915.882 64.852H594.9a43.99 43.99 0 0 0 0 87.979h211.115l-232.15 232.021a43.947 43.947 0 1 0 62.209 62.208l235.69-235.733v209.365a43.99 43.99 0 0 0 87.979 0V108.715a43.819 43.819 0 0 0-43.904-43.947z"  ></path></symbol><symbol id="icon-config" viewBox="0 0 1024 1024"><path d="M750.657741 768.65536c-55.33696 6.67648-105.92256-30.5152-112.59904-83.968-6.67648-53.41184 32.44032-103.05536 87.77728-109.73184 55.33696-6.67648 105.92256 30.55616 112.59904 83.968 6.67648 53.4528-32.44032 103.05536-87.77728 109.7728z m6.67648 48.66048c83.02592-10.48576 142.1312-83.968 131.6864-164.08576-10.52672-80.15872-85.89312-137.4208-167.936-126.93504-83.02592 10.52672-142.17216 83.968-131.6864 164.12672 9.54368 80.15872 84.91008 137.4208 167.936 126.89408z m-271.9744-79.17568a19.78368 19.78368 0 0 1-5.69344-9.54368l-5.7344-47.7184c0-8.6016 6.67648-16.22016 15.27808-17.2032l26.70592-3.76832a29.696 29.696 0 0 0 23.87968-21.95456l9.54368-34.36544a28.2624 28.2624 0 0 0-8.6016-30.5152l-20.97152-16.22016c-7.65952-5.7344-8.6016-16.22016-2.8672-23.87968l28.59008-35.26656a25.43616 25.43616 0 0 1 13.39392-6.67648 16.54784 16.54784 0 0 1 13.35296 2.8672l20.97152 16.1792c8.6016 6.71744 23.87968 6.71744 32.44032 0.98304l31.49824-18.14528a28.42624 28.42624 0 0 0 15.27808-27.648l-2.8672-26.74688c-0.98304-4.75136 0.94208-8.6016 3.80928-12.41088a17.69472 17.69472 0 0 1 11.4688-6.67648l46.73536-5.7344a15.89248 15.89248 0 0 1 19.08736 13.39392l3.80928 27.648c0.94208 11.4688 11.4688 21.01248 22.9376 22.9376l35.26656 9.50272c11.4688 3.85024 23.83872 0 31.49824-8.6016l16.22016-20.97152c2.8672-3.80928 7.61856-4.75136 12.41088-5.7344a16.5888 16.5888 0 0 1 13.35296 2.8672l36.2496 26.74688c7.61856 5.7344 8.6016 16.22016 2.8672 23.83872l-16.22016 20.97152a25.8048 25.8048 0 0 0-0.94208 31.5392l18.10432 30.5152c5.7344 10.48576 16.22016 17.16224 28.63104 16.22016l26.70592-3.85024c4.79232-0.94208 9.54368 0 12.41088 2.8672 3.80928 2.8672 5.7344 6.67648 6.67648 11.4688l2.8672 44.8512a17.03936 17.03936 0 0 1-14.336 19.08736l-29.57312 5.7344a29.696 29.696 0 0 0-23.83872 21.9136l-9.54368 34.36544a28.2624 28.2624 0 0 0 8.6016 30.5152l20.97152 16.22016c7.65952 5.7344 8.6016 16.22016 2.8672 23.87968l-27.648 34.32448c-5.7344 7.65952-17.2032 8.6016-24.82176 2.8672l-21.01248-16.22016c-8.6016-6.67648-23.83872-6.67648-32.44032-0.94208l-31.45728 18.10432a28.34432 28.34432 0 0 0-16.22016 27.68896l2.8672 26.70592c0.94208 4.79232-0.98304 8.6016-3.85024 12.41088a17.69472 17.69472 0 0 1-11.4688 6.67648l-46.6944 5.7344c-4.79232 0.94208-9.58464 0-12.41088-2.8672a17.69472 17.69472 0 0 1-6.67648-11.4688l-2.8672-26.70592a27.40224 27.40224 0 0 0-21.95456-22.89664l-35.30752-9.54368a29.81888 29.81888 0 0 0-31.49824 8.6016l-16.22016 20.97152c-5.7344 7.65952-17.2032 8.6016-24.7808 2.8672l-36.29056-26.70592c-7.61856-5.7344-8.6016-16.22016-2.8672-23.83872l16.22016-21.01248a25.8048 25.8048 0 0 0 0.98304-31.49824l-18.14528-30.5152a30.59712 30.59712 0 0 0-28.63104-16.22016l-27.648 2.8672a15.1552 15.1552 0 0 1-12.41088-2.8672z"  ></path><path d="M811.729101 95.0272H108.486861A89.16992 89.16992 0 0 0 19.767501 183.78752v656.42496c0 48.66048 40.05888 88.76032 88.71936 88.76032h374.04672v-101.1712H177.217741a52.96128 52.96128 0 0 1-52.51072-52.4288v-351.15008c0-28.63104 23.87968-52.46976 52.51072-52.46976h565.78048c26.74688 0 48.70144 20.02944 51.52768 45.79328h104.98048v-234.7008a87.53152 87.53152 0 0 0-87.77728-87.81824zM218.218701 269.63968a21.58592 21.58592 0 0 1-21.9136 21.95456H144.777421a21.58592 21.58592 0 0 1-21.95456-21.95456V222.90432c0-12.41088 9.54368-21.95456 21.95456-21.95456H196.305101c12.36992 0 21.9136 9.54368 21.9136 21.95456v46.73536z m234.7008 0a21.58592 21.58592 0 0 1-21.9136 21.95456H305.995981a21.58592 21.58592 0 0 1-21.9136-21.95456V222.90432c0-12.41088 9.54368-21.95456 21.9136-21.95456h125.00992c12.41088 0 21.95456 9.54368 21.95456 21.95456v46.73536z"  ></path></symbol><symbol id="icon-down" viewBox="0 0 1024 1024"><path d="M736 480a32.042667 32.042667 0 0 0-45.312 0l-167.381333 167.381333a16.042667 16.042667 0 0 1-22.613334 0L333.312 480a32.042667 32.042667 0 0 0-45.312 45.312l178.688 178.688a64 64 0 0 0 90.496 0l178.816-178.688a32.042667 32.042667 0 0 0 0-45.312z"  ></path></symbol><symbol id="icon-search" viewBox="0 0 1024 1024"><path d="M942.144 855.36l-152.96-152.96a401.408 401.408 0 0 0-607.616-520.896 401.152 401.152 0 0 0 0 567.488 401.28 401.28 0 0 0 520.832 40.128l152.96 152.96a61.568 61.568 0 0 0 86.848 0 61.44 61.44 0 0 0 0-86.72z m-253.696-166.912a315.52 315.52 0 1 1-446.208-446.208 315.52 315.52 0 0 1 446.208 446.208z"  ></path></symbol><symbol id="icon-home" viewBox="0 0 1024 1024"><path d="M293.888 948.352v-290.944A72.704 72.704 0 0 1 366.72 584.704h290.88a72.704 72.704 0 0 1 72.768 72.704v290.944h218.176V336L512 75.52 75.712 335.744v612.48h218.24zM38.528 273.408L474.88 13.056a72.704 72.704 0 0 1 74.432 0l436.352 260.352a72.704 72.704 0 0 1 35.52 62.592v612.352a72.768 72.768 0 0 1-72.704 72.704H75.712a72.704 72.704 0 0 1-72.704-72.704V336A72.704 72.704 0 0 1 38.4 273.408z m328.128 384v290.944h290.88v-290.944H366.72z"  ></path></symbol><symbol id="icon-cloud" viewBox="0 0 1024 1024"><path d="M788.48 834.56H235.52a235.52 235.52 0 0 1-48.128-465.92 327.68 327.68 0 0 1 648.704 0 235.52 235.52 0 0 1-48.128 465.92h0.512zM235.52 465.92a133.12 133.12 0 0 0 0 266.24h552.96a133.12 133.12 0 1 0 0-266.24h-51.2v-51.2a225.28 225.28 0 0 0-450.56 0v51.2h-51.2z"  ></path></symbol><symbol id="icon-help" viewBox="0 0 1024 1024"><path d="M953.276415 0c14.847879 0 26.879782 11.519906 26.879781 25.599792v972.792096a26.303786 26.303786 0 0 1-26.879781 25.599792H90.883422A26.303786 26.303786 0 0 1 64.00364 998.391888V25.599792c0-14.079886 12.095902-25.599792 26.943781-25.599792h862.328994z m-80.831344 107.775124H171.778764v808.441432h700.666307V107.775124z"  ></path><path d="M789.501745 0v477.820118l-179.71054-62.719491-179.58254 62.719491V0h359.29308z m-107.839124 107.775124H537.983789v218.174228l71.807416-24.959798 71.871416 24.959798V107.775124z"  ></path></symbol><symbol id="icon-pin" viewBox="0 0 1024 1024"><path d="M885.312 347.392l-272.64-272.64a36.608 36.608 0 0 0-52.032 0L382.4 253.12A364.672 364.672 0 0 0 113.28 330.88a36.736 36.736 0 0 0-3.008 54.656l201.088 201.088-238.336 238.08a17.472 17.472 0 0 0-5.12 10.88l-3.776 41.152c-0.96 10.432 7.296 19.264 17.6 19.264 0.576 0 1.088 0 1.664-0.128l41.152-3.712a17.472 17.472 0 0 0 10.88-5.12l238.336-238.336 200.96 201.024a36.608 36.608 0 0 0 54.72-2.944 365.504 365.504 0 0 0 77.696-269.376l178.24-178.24a36.672 36.672 0 0 0 0-51.84z m-234.688 173.952l-27.136 27.072 4.224 38.08a287.616 287.616 0 0 1-33.664 170.304L203.392 365.888a286.208 286.208 0 0 1 170.24-33.664l38.08 4.224 27.136-27.136 147.904-147.968 211.904 211.904-148.032 148.096z"  ></path></symbol><symbol id="icon-copy" viewBox="0 0 1024 1024"><path d="M702.8224 0H144.2816A93.0816 93.0816 0 0 0 51.2 93.0816v651.6736h93.0816V93.0816h558.592V0z m139.6224 186.1632h-512a93.0816 93.0816 0 0 0-93.0816 93.1328v651.6224c0 51.4048 41.6768 93.0816 93.1328 93.0816h512a93.1328 93.1328 0 0 0 93.0816-93.0816V279.296a93.1328 93.1328 0 0 0-93.1328-93.1328z m-512 93.1328h512v651.6224h-512V279.296z"  ></path></symbol><symbol id="icon-link" viewBox="0 0 1024 1024"><path d="M607.957333 417.834667a192.128 192.128 0 0 0-19.584-16.896 38.826667 38.826667 0 1 0-47.146666 61.226666c3.669333 2.986667 7.253333 5.973333 10.666666 9.386667l3.157334 3.2c40.277333 40.192 28.288 109.525333-11.989334 149.802667l-171.904 171.818666a103.424 103.424 0 0 1-146.048 0l-3.2-3.2a103.253333 103.253333 0 0 1 0-146.005333l75.946667-75.946667a42.24 42.24 0 1 0-49.92-68.010666c-0.085333 0-0.085333 0-0.128-0.085334l-0.810667 0.768a42.496 42.496 0 0 0-8.021333 7.509334l-78.933333 73.813333a191.317333 191.317333 0 0 0 0 269.781333l3.242666 3.2a191.36 191.36 0 0 0 269.696 0l171.818667-171.861333c74.069333-74.197333 80.384-191.189333 6.272-265.344l-3.114667-3.157333z"  ></path><path d="M855.594667 165.802667l-3.2-3.2a191.274667 191.274667 0 0 0-269.653334 0L410.88 334.464c-74.197333 74.197333-78.250667 181.333333-4.096 255.573333l3.157333 3.072c3.370667 3.370667 6.826667 6.528 10.453334 9.6a36.693333 36.693333 0 0 0 8.96 7.296l0.085333 0.085334a36.693333 36.693333 0 0 0 51.285333-47.786667c-4.821333-10.922667-13.909333-17.536-20.181333-23.808l-3.157333-3.114667c-40.277333-40.277333-24.96-98.816 15.317333-139.093333l171.946667-171.818667a103.253333 103.253333 0 0 1 145.92 0l3.242666 3.157334c40.277333 40.277333 40.277333 105.813333 0 146.048l-75.690666 75.776a41.813333 41.813333 0 0 0 24.874666 75.52 41.685333 41.685333 0 0 0 21.930667-6.229334l0.170667 0.256 1.152-1.066666a41.984 41.984 0 0 0 9.557333-8.832l79.786667-73.6a191.232 191.232 0 0 0 0-269.653334z"  ></path></symbol><symbol id="icon-setting" viewBox="0 0 1024 1024"><path d="M662.3744 108.9536l118.6816 69.5296c-5.632 34.304-13.824 111.5136 16.896 165.7344 31.744 55.808 105.3696 87.9616 137.0624 99.2256 3.072 19.968 6.144 48.64 6.144 69.0688 0 18.944-3.072 46.592-6.144 69.0688-32.256 11.776-104.8576 43.4688-137.0624 99.7376-31.232 54.7328-23.04 131.9424-16.896 165.7344l-118.6816 69.5296c-28.16-23.04-88.9856-67.5328-150.3744-67.5328-61.9008 0-123.7504 45.056-150.8864 67.584l-119.1936-69.12c5.632-34.2528 13.824-111.4624-16.896-165.6832-31.6928-55.808-105.3184-88.0128-137.0624-99.2256-3.072-19.968-6.144-48.64-6.144-69.12 0-18.8928 3.072-46.4896 6.144-69.0176 32.256-11.776 104.8576-43.4688 137.1136-99.7376 31.232-54.7328 22.528-131.4304 16.896-165.7344l118.6304-69.5296c28.16 23.04 88.9856 67.4816 150.3744 67.4816 61.952 0 123.8016-45.0048 151.3984-67.9936z m-2.048-86.9888c-14.848 0-30.1568 5.632-40.9088 15.872-15.872 14.848-69.5808 57.2928-107.9296 57.2928-38.3488 0-92.0576-42.496-107.9296-57.2928a60.3136 60.3136 0 0 0-63.8976-11.264L184.6272 117.6064l-3.072 2.048c-17.92 13.312-27.0848 38.912-22.4768 61.44 5.12 20.9408 14.336 89.4976-5.12 123.2384C135.0144 338.1248 71.68 363.6736 51.2 370.3296c-20.48 6.656-38.4 26.624-41.984 48.0768C8.704 420.9664 0 474.112 0 513.024c0 38.912 8.704 92.5696 9.216 94.6176 3.584 21.504 21.504 41.984 41.9328 48.128 20.48 6.656 83.8656 32.7168 102.8096 65.9456 18.944 33.792 9.728 101.7856 5.12 123.2896-5.12 22.016 4.608 47.5648 22.528 61.3376l154.9824 91.5968 3.072 1.536a60.3136 60.3136 0 0 0 63.9488-11.264c15.8208-14.848 69.5296-57.2928 107.8784-57.2928 38.4 0 92.1088 42.496 107.9296 57.2928 10.752 9.728 26.112 15.36 40.96 15.36 7.68 0 15.872-1.536 22.9888-4.608l154.9824-90.5728 3.072-2.048c17.92-13.312 27.136-39.3728 22.528-61.3376-5.12-20.992-14.336-89.5488 5.12-123.2896 18.944-33.792 82.3296-59.8528 102.8096-65.9968 20.48-6.656 38.3488-26.624 41.9328-48.0768 0.512-2.56 9.216-55.7568 9.216-94.6176 0-38.912-8.704-92.5696-9.216-94.6176-3.584-21.504-21.504-41.984-41.984-48.128-20.48-6.656-83.8656-32.7168-102.8096-65.9456-18.944-33.792-9.728-101.7856-5.12-123.2896 5.12-22.016-4.608-47.5648-22.4768-61.3888L686.4384 28.16l-3.072-1.536c-7.168-3.072-15.36-4.608-23.04-4.608z"  ></path><path d="M511.488 369.8176a143.6672 143.6672 0 0 1 143.2576 143.2064 143.616 143.616 0 0 1-143.2576 143.2064 143.616 143.616 0 0 1-143.2064-143.2064A143.616 143.616 0 0 1 511.488 369.8176z m0-81.8688a225.0752 225.0752 0 1 0 0 450.1504 225.0752 225.0752 0 0 0 0-450.1504z"  ></path></symbol><symbol id="icon-github" viewBox="0 0 1024 1024"><path d="M512.265011 32.19456A491.52 491.52 0 0 0 20.294451 524.16512c0 217.4976 140.9024 401.57184 336.40448 466.7392 24.41216 4.87424 33.3824-10.60864 33.3824-23.63392 0-11.38688-0.8192-50.50368-0.8192-91.21792-136.8064 29.32736-165.35552-58.65472-165.35552-58.65472-21.95456-57.01632-54.55872-71.68-54.55872-71.68-44.81024-30.14656 3.2768-30.14656 3.2768-30.14656 49.68448 3.2768 75.73504 50.50368 75.73504 50.50368 43.99104 74.9568 114.85184 53.78048 143.36 40.71424 4.096-31.744 17.08032-53.73952 30.96576-65.9456-109.1584-11.42784-224.01024-53.78048-224.01024-242.72896 0-53.78048 19.53792-97.77152 50.50368-131.97312-4.9152-12.20608-21.99552-62.6688 4.9152-130.29376 0 0 41.49248-13.06624 135.168 50.46272a476.32384 476.32384 0 0 1 123.00288-16.26112c41.53344 0 83.88608 5.69344 123.00288 16.26112 93.67552-63.488 135.168-50.46272 135.168-50.46272 26.91072 67.584 9.8304 118.08768 4.9152 130.29376 31.78496 34.2016 50.50368 78.2336 50.50368 131.97312 0 188.94848-114.85184 230.48192-224.82944 242.72896 17.94048 15.48288 33.42336 44.76928 33.42336 91.21792 0 65.9456-0.8192 118.90688-0.8192 135.168 0 13.1072 8.97024 28.54912 33.3824 23.67488 195.50208-65.16736 336.40448-249.2416 336.40448-466.7392 0.8192-272.05632-219.9552-491.9296-491.15136-491.9296z"  ></path><path d="M206.785331 738.38592c-0.8192 2.4576-4.9152 3.2768-8.15104 1.6384-3.2768-1.6384-5.69344-4.9152-4.096-7.3728 0.8192-2.41664 4.9152-3.23584 8.192-1.6384 3.2768 1.6384 4.87424 4.9152 4.096 7.3728z m19.53792 21.99552c-2.4576 2.4576-7.33184 0.8192-9.74848-2.4576-3.2768-3.2768-4.096-8.15104-1.6384-10.56768 2.4576-2.4576 6.5536-0.8192 9.78944 2.4576 3.2768 4.05504 4.096 8.92928 1.6384 10.56768z m19.57888 28.50816c-3.2768 2.4576-8.192 0-10.60864-4.096-3.2768-4.096-3.2768-9.74848 0-11.38688 3.2768-2.4576 8.15104 0 10.60864 4.096 3.2768 4.05504 3.2768 8.92928 0 11.38688z m26.86976 27.68896c-2.4576 3.2768-8.15104 2.4576-13.02528-1.6384-4.096-4.096-5.7344-9.78944-2.4576-12.20608 2.4576-3.2768 8.15104-2.4576 13.02528 1.6384 4.096 3.2768 4.9152 8.92928 2.4576 12.20608z m36.6592 15.48288c-0.8192 4.096-6.5536 5.69344-12.24704 4.096-5.69344-1.6384-8.92928-6.5536-8.11008-9.8304 0.8192-4.05504 6.51264-5.69344 12.20608-4.05504 5.7344 1.6384 8.97024 5.7344 8.15104 9.78944z m39.89504 3.2768c0 4.05504-4.9152 7.29088-10.56768 7.29088-5.7344 0-10.60864-3.2768-10.60864-7.33184 0-4.096 4.9152-7.33184 10.60864-7.33184s10.56768 3.2768 10.56768 7.3728z m37.4784-6.5536c0.8192 4.096-3.2768 8.15104-8.97024 8.97024-5.7344 0.8192-10.56768-1.6384-11.38688-5.7344-0.8192-4.05504 3.2768-8.11008 8.92928-8.92928 5.7344-0.8192 10.60864 1.6384 11.42784 5.7344z"  ></path></symbol></svg>',function(o){var a=(a=document.getElementsByTagName("script"))[a.length-1],l=a.getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var t,c,h,i,e,m=function(a,l){l.parentNode.insertBefore(a,l)};if(l&&!o.__iconfont__svg__cssinject__){o.__iconfont__svg__cssinject__=!0;try{document.write("<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>")}catch(a){console&&console.log(a)}}t=function(){var a,l=document.createElement("div");l.innerHTML=o._iconfont_svg_string_4357075,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?m(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(c=function(){document.removeEventListener("DOMContentLoaded",c,!1),t()},document.addEventListener("DOMContentLoaded",c,!1)):document.attachEvent&&(h=t,i=o.document,e=!1,s(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,d())})}function d(){e||(e=!0,h())}function s(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(s,50)}d()}}(window);
\ No newline at end of file
diff --git a/vue/src/assets/logo.ico b/vue/src/assets/logo.ico
deleted file mode 100644
index 5e164631..00000000
Binary files a/vue/src/assets/logo.ico and /dev/null differ
diff --git a/vue/src/assets/swanlab.svg b/vue/src/assets/swanlab.svg
deleted file mode 100644
index e7b8dfb1..00000000
--- a/vue/src/assets/swanlab.svg
+++ /dev/null
@@ -1,1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file
diff --git a/vue/src/business/README.md b/vue/src/business/README.md
deleted file mode 100644
index 0256fabc..00000000
--- a/vue/src/business/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# business
-
-本文件夹存放一些共享业务模块，方便跨页面复用。
diff --git a/vue/src/business/config-editor/ConfigEditor.vue b/vue/src/business/config-editor/ConfigEditor.vue
deleted file mode 100644
index ed7c69e3..00000000
--- a/vue/src/business/config-editor/ConfigEditor.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<template>
-  <SLButton
-    class="px-3 py-1.5 border-none !text-dimmest rounded"
-    theme="primary"
-    hollow
-    :disabled="disabled"
-    :disabled-tip="$t('common.config-editor.not-allowed')"
-    @click="showModal = true"
-  >
-    <SLIcon icon="modify" class="w-4 h-4" />
-  </SLButton>
-  <SLModal class="px-10 py-6" max-w="550" v-model="showModal">
-    <!-- 如果后续项目和实验的可修改内容发生改变，这样容易重构一些 -->
-    <EditorWrap :type="type" @confirm="confirm"></EditorWrap>
-  </SLModal>
-</template>
-
-<script setup>
-/**
- * @description: 编辑项目/实验信息
- * 目前文字部分只适配了项目和实验，如果之后需要使用这套弹窗模板修改别的部分的信息
- * 只需要增加i18n的文字适配，点击后的操作使用 modify 这个 emit 暴露了出去
- * @file: InfoEditor.vue
- * @since: 2023-12-30 23:55:02
- **/
-import { ref } from 'vue'
-import SLModal from '@swanlab-vue/components/SLModal.vue'
-import EditorWrap from './EditorWrap.vue'
-import SLIcon from '@swanlab-vue/components/SLIcon.vue'
-
-// 是否展示弹窗
-const showModal = ref(false)
-
-defineProps({
-  type: {
-    type: String,
-    validator: (value) => {
-      // 目前type只适配了project和experiment两种模式
-      return ['project', 'experiment'].includes(value)
-    }
-  },
-  disabled: {
-    type: Boolean,
-    default: false
-  }
-})
-
-const emits = defineEmits(['modify'])
-
-/**
- * 点击修改后触发的确认函数
- * @param {obj} newV 需要修改的值
- * @param.name {string}
- * @param.description {string}
- *
- * 这里使用了promise，是因为 emit 触发的 modify 很可能是异步的，而 emit 触发是同步的
- * 该组件在点击确认按钮后会有两个缓冲操作：
- * 1. 按钮显示处理中
- * 2. 弹窗并不立即消失
- * 可以看到，触发 modify 的时候会传递两个参数，一个是修改后的值，一个是回调函数
- * 只有在外部的处理函数调用回调的同时，才会结束缓冲操作
- */
-const confirm = async (newV, callback) => {
-  emits('modify', newV, () => {
-    showModal.value = false
-    callback()
-  })
-}
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/business/config-editor/DeleteButton.vue b/vue/src/business/config-editor/DeleteButton.vue
deleted file mode 100644
index 14422d2c..00000000
--- a/vue/src/business/config-editor/DeleteButton.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<template>
-  <SLButton @click="click" theme="negative" hollow :disabled-tip="$t('common.delete.not-allowed')">
-    <div class="text-sm flex items-center gap-2">
-      <SLIcon icon="trash" class="w-4 h-4" />
-      <span class="md:block hidden">
-        {{ $t('common.delete.button') }}
-      </span>
-    </div>
-  </SLButton>
-</template>
-
-<script setup>
-/**
- * @description: 实验/项目删除套件
- * @file: SLDelete.vue
- * @since: 2024-01-04 14:27:27
- *
- * 套件主要含有三部分：
- * 1. 适配于 type 的描述部分：不同的删除弹窗大概率会有不同的标题和内容
- * 2. 确认删除弹窗
- * 3. 取消/确认按钮的回调
- **/
-
-import SLButton from '@swanlab-vue/components/SLButton.vue'
-import SLIcon from '@swanlab-vue/components/SLIcon.vue'
-import { confirm } from '@swanlab-vue/components/confirm'
-import { t } from '@swanlab-vue/i18n'
-
-const props = defineProps({
-  type: String,
-  validator: (value) => {
-    // 目前type只适配了project和experiment两种模式
-    return ['project', 'experiment'].includes(value)
-  }
-})
-
-const emits = defineEmits(['confirm', 'cancel'])
-
-const click = () => {
-  confirm(t(`common.delete.title.${props.type}`), t(`common.delete.content.${props.type}`), {}).then(() => {
-    emits('confirm')
-  })
-}
-</script>
-
-<style lang="scss" scoped>
-button {
-  @apply px-3  flex items-center rounded-lg border-none h-8;
-  border-color: var(--outline-default) !important;
-  @media (min-width: 768px) {
-    border-style: solid;
-  }
-}
-</style>
diff --git a/vue/src/business/config-editor/EditorWrap.vue b/vue/src/business/config-editor/EditorWrap.vue
deleted file mode 100644
index 0f951c3c..00000000
--- a/vue/src/business/config-editor/EditorWrap.vue
+++ /dev/null
@@ -1,196 +0,0 @@
-<template>
-  <form class="w-full" @submit.prevent="save">
-    <h1 class="text-xl font-semibold">{{ $t(`common.config-editor.title.${type}`) }}</h1>
-    <!-- 修改名称 -->
-    <div class="relative pt-4">
-      <h2 class="font-semibold pb-3">{{ $t(`common.config-editor.sub-title.${type}.name`) }}</h2>
-      <!-- 名称不可为空，且只能是英文字符、数字、下划线、中划线 -->
-      <input
-        type="text"
-        class="input"
-        v-model="info.name"
-        :placeholder="`edit your ${type} name here`"
-        :maxlength="max_name_len"
-        pattern="^(?!\s+$).*$"
-        required
-      />
-      <!-- 数量限制 -->
-      <p
-        class="absolute bottom-[-20px] text-xs right-0"
-        :class="info.name.length === 0 || info.name.length === max_name_len ? 'text-negative-default' : ''"
-      >
-        {{ `${info.name.length} / ${max_name_len}` }}
-      </p>
-      <!-- 提示信息 -->
-      <span class="tip text-negative-default">{{ errors.name }}</span>
-    </div>
-    <!-- 修改描述信息 -->
-    <div class="relative">
-      <h2 class="font-semibold pt-5 pb-3">{{ $t(`common.config-editor.sub-title.${type}.desc`) }}</h2>
-      <textarea
-        class="input"
-        rows="10"
-        v-model="info.description"
-        :placeholder="`edit your ${type} description here`"
-        :maxlength="max_description_len"
-      ></textarea>
-      <!-- 数量限制 -->
-      <p
-        class="absolute bottom-[-20px] text-xs right-0"
-        :class="info.description?.length === max_description_len ? 'text-negative-default' : ''"
-      >
-        {{ `${info.description ? info.description.length : 0} / ${max_description_len}` }}
-      </p>
-      <!-- 提示信息 -->
-      <span class="tip text-negative-default">{{ errors.description }}</span>
-    </div>
-    <!-- 确认按钮 -->
-    <div class="flex justify-end pt-10">
-      <SLButton type="submit" theme="primary" class="p-2 rounded flex items-center" :disabled="handling">
-        <SLLoading size="4" v-if="handling" />
-        <span>{{ $t(`common.config-editor.save.${handling ? 'savimg' : 'default'}`) }}</span>
-      </SLButton>
-    </div>
-  </form>
-</template>
-
-<script setup>
-/**
- * @description: 编辑项目/实验信息得弹窗布局
- * @file: EditorWrap.vue
- * @since: 2023-12-31 10:30:01
- **/
-import { useProjectStore, useExperimentStore } from '@swanlab-vue/store'
-import { ref } from 'vue'
-import SLLoading from '@swanlab-vue/components/SLLoading.vue'
-import { message } from '@swanlab-vue/components/message'
-import { t } from '@swanlab-vue/i18n'
-import { computed } from 'vue'
-
-const projectStore = useProjectStore()
-const experimentStore = useExperimentStore()
-
-const props = defineProps({
-  type: String
-})
-
-const emits = defineEmits(['confirm'])
-
-// ---------------------------------- 系统参数 ----------------------------------
-
-// v-model 绑定到两个输入栏
-const info = ref({
-  name: props.type === 'project' ? projectStore.name : experimentStore.name,
-  description: props.type === 'project' ? projectStore.description : experimentStore.description
-})
-
-// 错误信息
-const errors = ref({
-  name: '',
-  description: ''
-})
-
-// ---------------------------------- 参数限制 ----------------------------------
-
-const max_name_len = 100
-const max_description_len = 255
-
-const checkName = computed(() => {
-  const name = info.value.name
-  // 判断字符串长度是否超过100个字符
-  if (name.length > max_name_len) return false
-  return true
-})
-
-/**
- * 描述校验
- * 描述不能超过255个字符，可以包含任何字符，前后空格自动去除
- */
-const checkDescription = computed(() => {
-  if (!info.value.description) return true
-  if (info.value.description?.length > max_description_len) {
-    return false
-  }
-  return true
-})
-
-// ---------------------------------- 重新设置 ----------------------------------
-
-// 是否在处理中
-const handling = ref(false)
-
-/**
- * 保存修改
- *
- * 1. 清除错误信息
- * 2. 判断是否一点东西没改
- * 3. 实验模式时，实验名是不能重复的
- * 4. 触发确认函数并显示处理中
- */
-const save = async () => {
-  // 清空错误信息
-  errors.value = {
-    name: '',
-    description: ''
-  }
-
-  // 检查格式要求
-  if (props.type === 'project' && !checkName.value) {
-    message.warning('invalid project name')
-    return (errors.value.name = 'too long or invalid characters')
-  } else if (!checkName.value) {
-    message.warning('invalid experiment name')
-    return (errors.value.name = 'too long or invalid characters')
-  }
-  if (!checkDescription.value) {
-    message.warning('invalid description')
-    return (errors.value.description = 'invalid description')
-  }
-
-  // 判断是否一点没变
-  if (info.value.name.trim() === projectStore.name && info.value.description === projectStore.description) {
-    return (errors.value.name = 'nothing changed in project config')
-  } else if (
-    info.value.name.trim() === experimentStore.name &&
-    info.value.description === experimentStore.description
-  ) {
-    return (errors.value.name = 'nothing changed in experiment config')
-  }
-
-  // 实验模式中，校验实验名是否重复
-  let duplicated = false
-  if (props.type === 'experiment') {
-    projectStore.experiments.forEach((expr) => {
-      // 如果重复，提示错误信息，但是注意实验名称可以和原来的一样
-      if (info.value.name === expr.name && info.value.name !== experimentStore.name) {
-        duplicated = true
-        errors.value.name = 'The experiment name is duplicated'
-      }
-    })
-  }
-  if (duplicated) return
-
-  handling.value = true
-
-  emits('confirm', info.value, () => {
-    handling.value = false
-    message.success(t('common.config-editor.save.success'))
-  })
-}
-</script>
-
-<style lang="scss" scoped>
-.input {
-  @apply w-full p-2 text-sm outline-none border rounded-lg bg-higher;
-}
-
-.tip {
-  @apply absolute bottom-[-20px] left-0 text-xs;
-}
-
-button[type='submit'] {
-  &:disabled {
-    @apply pointer-events-none cursor-not-allowed opacity-50 gap-2;
-  }
-}
-</style>
diff --git a/vue/src/business/config-editor/README b/vue/src/business/config-editor/README
deleted file mode 100644
index 97ca2d53..00000000
--- a/vue/src/business/config-editor/README
+++ /dev/null
@@ -1,1 +0,0 @@
-在此处存放与项目、实验配置相关的共享组件
diff --git a/vue/src/charts/ChartsDashboard.vue b/vue/src/charts/ChartsDashboard.vue
deleted file mode 100644
index 31c1efc8..00000000
--- a/vue/src/charts/ChartsDashboard.vue
+++ /dev/null
@@ -1,229 +0,0 @@
-<template>
-  <!-- 图表顶层，嵌入全局功能 -->
-  <div class="charts-pannel">
-    <!-- 搜索框 -->
-    <div class="w-60 mr-2">
-      <SLSearch :placeholder="$t('chart.search.placeholder')" reverse @search="search" @input="search" />
-    </div>
-    <SmoothButton @smooth="handleSmooth" :default-method-id="1" />
-  </div>
-  <!-- 每一个namespace对应一个图表容器 -->
-  <ChartsContainer
-    v-for="(group, index) in nowChartsGroupList"
-    :ref="(el) => setChartsRefList(el, index)"
-    :key="group.id"
-    :label="getNamespaces(group)"
-    :charts="getCharts(group)"
-    :index="index"
-    :opened="!!group.opened"
-    :isPinned="group.id === -1"
-    :isHidden="group.id === -2"
-    @switch="(opened) => debouncedHandleSwitch(opened, group)"
-    @pin="pin"
-    @unpin="unpin"
-    @hide="hide"
-    @unhide="unhide"
-  />
-</template>
-
-<script setup>
-/**
- * @description: 图表面板，组织图表
- * @file: ChartsDashboard.vue
- * @since: 2024-03-04 19:30:29
- **/
-import ChartsContainer from './components/ChartsContainer.vue'
-import { t } from '@swanlab-vue/i18n'
-import SmoothButton from './components/SmoothButton.vue'
-import { debounces } from '@swanlab-vue/utils/common'
-import SLSearch from '@swanlab-vue/components/SLSearch.vue'
-import { provide } from 'vue'
-
-const props = defineProps({
-  // 整个图表列表集合
-  groups: {
-    type: Array,
-    required: true
-  },
-  // 更新图表状态
-  updateChartStatus: {
-    type: Function,
-    required: true
-  },
-  // 更新命名空间状态
-  updateNamespaceStatus: {
-    type: Function,
-    required: true
-  },
-  // 媒体获取对象
-  media: {
-    type: Object,
-    required: true
-  },
-  // 图表默认颜色
-  defaultColor: {
-    type: String,
-    required: true
-  },
-  // 获取某一个数据的颜色
-  getColor: {
-    type: Function,
-    required: true
-  },
-  // 订阅数据更新
-  subscribe: {
-    type: Function,
-    required: true
-  },
-  // 取消订阅数据更新
-  unsubscribe: {
-    type: Function,
-    default: () => {}
-  },
-  // 图表跳转实验前缀
-  experimentPrefix: {
-    type: String,
-    default: '/experiment/'
-  }
-})
-const chartsGroupList = ref(props.groups)
-watch(
-  () => props.groups,
-  (newVal) => {
-    // 当groups发生变化时，更新chartsGroupList
-    chartsGroupList.value = newVal
-  }
-)
-const searchChartsGroupList = ref(null)
-
-const chartsRefList = ref([])
-
-const nowChartsGroupList = computed(() => {
-  return searchChartsGroupList.value ? searchChartsGroupList.value : chartsGroupList.value
-})
-
-const setChartsRefList = (el, index) => {
-  chartsRefList.value[index] = el
-  chartsRefList.value.length = chartsGroupList.value.length
-}
-
-// ---------------------------------- 获取namespace名称 ----------------------------------
-/**
- * 将组名进行一些翻译
- * @param { Object } namespace 命名空间配置
- */
-const getNamespaces = (namespace) => {
-  // console.log(name)
-  if (namespace.name === 'default') return t('chart.label.default')
-  if (namespace.id === -1) return t('chart.label.pinned')
-  if (namespace.id === -2) return t('chart.label.hidden')
-  else return namespace.name
-}
-
-// ---------------------------------- 获取图表列表 ----------------------------------
-/**
- * 根据group中的chart_id获取chart配置，生成一个数组
- */
-const getCharts = (group) => {
-  return group.charts
-}
-
-// ---------------------------------- 处理图表平滑事件 ----------------------------------
-const smoothMethod = ref(null)
-const handleSmooth = (method) => {
-  // 遍历chartsRefList中的所有chartRefList，调用其smooth方法
-  chartsRefList.value.forEach((chartsRef) => {
-    chartsRef.chartRefList.forEach((chartRef) => {
-      chartRef?.smooth(method)
-    })
-  })
-  smoothMethod.value = method
-}
-// 暂时先以provide的方式传递smoothMethod，后续会存储在后端，通过接口获取
-provide('smoothMethod', smoothMethod)
-
-// ---------------------------------- 在此处处理命名空间打开和关闭 ----------------------------------
-const changeNamespaceStatus = (opened, namespace) => {
-  // console.log('namespace click', opened)
-  // 向后端更新展开状态
-  props.updateNamespaceStatus(opened, namespace)
-}
-
-const debouncedHandleSwitch = debounces(changeNamespaceStatus, 300)
-
-const changeChartStatus = async (chart, status) => {
-  const data = await props.updateChartStatus(chart, status)
-  chartsGroupList.value = data.groups
-}
-
-// ---------------------------------- 处理pin事件 ----------------------------------
-/**
- * 图表置顶
- * @param { Object } chart 图表配置
- */
-const pin = (chart) => {
-  changeChartStatus(chart, 1)
-}
-
-/**
- * 图表取消置顶
- * @param { Object } chart 图表配置
- */
-const unpin = (chart) => {
-  changeChartStatus(chart, 0)
-}
-
-// ---------------------------------- 处理hide事件 ----------------------------------
-/**
- * 图表隐藏
- * @param { Object } chart 图表配置
- */
-const hide = (chart) => {
-  changeChartStatus(chart, -1)
-}
-
-/**
- * 图表取消隐藏
- * @param { Object } chart 图表配置
- */
-const unhide = (chart) => {
-  changeChartStatus(chart, 0)
-}
-
-// ---------------------------------- 处理搜索图表事件 ----------------------------------
-const search = (value) => {
-  if (!value) return (searchChartsGroupList.value = null)
-  // newGroup中滤除不包含value的chart，需要注意的是不能直接修改原数组，否则会影响到原数组
-  const newGroups = []
-  for (const group of chartsGroupList.value) {
-    if (group.charts.some((chart) => chart.name.toLowerCase().includes(value.toLowerCase()))) {
-      newGroups.push({ ...group, charts: [...group.charts] })
-    }
-  }
-  newGroups.forEach((group) => {
-    group.charts = [...group.charts]
-    group.charts = group.charts.filter((chart) => {
-      return chart.name.toLowerCase().includes(value.toLowerCase())
-    })
-  })
-
-  searchChartsGroupList.value = newGroups
-}
-
-// ---------------------------------- 依赖注入 ----------------------------------
-provide('media', props.media)
-provide('defaultColor', props.defaultColor)
-provide('getColor', props.getColor)
-provide('$on', props.subscribe)
-provide('$off', props.unsubscribe)
-provide('experimentPrefix', props.experimentPrefix)
-
-console.log(props.experimentPrefix)
-</script>
-
-<style lang="scss" scoped>
-.charts-pannel {
-  @apply sticky px-4 py-3 bg-higher flex justify-between top-0 border-b;
-  z-index: 30;
-}
-</style>
diff --git a/vue/src/charts/README.md b/vue/src/charts/README.md
deleted file mode 100644
index 93b4e7e4..00000000
--- a/vue/src/charts/README.md
+++ /dev/null
@@ -1,170 +0,0 @@
-# 这里提供了 SwanLab 所有支持的图表类型
-
-每个图表类型被抽象为一个组件,但是在外部通过本部分提供的[ChartsDashboard](./ChartsDashboard.vue)组件统一调用，所需的就是传入chart配置  
-> 一个ChartsContainer组件代表一个命名空间
-作为数据驱动的图表仪表盘，每一个图表都接受一个图表配置。与此同时，为了与业务逻辑解藕，一些图表显示行为、图表配置将由外部传入，这一些原则将与全局组件的设计保持一致。
-
-## 使用图像仪表盘
-
-图像仪表盘及[ChartsDashboard](./ChartsDashboard.vue)组件，将接收图表组织配置（groups）以及一些其他的配置。
-如果需要使用图像仪表盘，你需要确保通过props传入了以下内容：
-
-1. `groups`: 代表图表组织配置
-2. `updateChartStatus`: 更新图表状态的方法
-3. `updateNamespaceStatus`: 更新图表配置的方法
-4. `media`: 媒体查询对象
-5. `getColor`: 获取颜色的方法
-6. `defaultColor`: 默认颜色
-
-接下来逐个说明这些props。
-
-### groups
-
-代表图表组织配置，包含namespace等内容，具体可以看对接的api文档，这里不再赘述。
-
-### updateChartStatus
-
-更新图表状态（置顶还是隐藏或者恢复正常状态），接受图表对象和状态吗，例如：
-
-```js
-/**
- * 更新chart状态，置顶、隐藏或正常显示
- * @param { object } chart 图表对象
- * @param { int } status 状态码，0为正常，1为置顶，-1为隐藏
- * @returns { Promise } Promise对象，最终可返回更新后的图表组织结构
- */
-export const updateChartStatus = async (chart, status) => {
-  const { data } = await http.patch('/chart/' + chart.id + '/status', {
-    status
-  })
-  return data
-}
-```
-
-### updateNamespaceStatus
-
-更新命名空间状态（开启还是关闭），接收命名空间对象和开启关闭参数，例如：
-
-```js
-/**
- * 更新命名空间展开状态
- * @param { boolean } opened 是否展开
- * @param { object } namespace 命名空间对象
- * @returns { Promise }
- */
-export const updateNamespaceStatus = (opened, namespace) => {
-  return http.patch('/namespace/' + namespace.id + '/opened', {
-    experiment_id: namespace.experiment_id?.id,
-    project_id: namespace.project_id?.id,
-    opened
-  })
-}
-```
-
-### media
-
-获取媒体文件，输入文件名、实验id和数据名称，从后端获取，例如:
-
-```js
-/**
- * 获取媒体文件，获取blob对象
- * 返回promise，如果成功，返回blob对象，否则返回错误信息
- */
-export const media = {
-  /**
-   * 获取媒体文件，获取blob对象
-   * 返回promise，如果成功，返回blob对象，否则返回错误信息
-   * @param { string } data 即文件名，即数据中的data字段
-   * @param { string } experiment_id 实验id
-   * @param { string } tag 数据名称
-   * @returns { Promise<Blob> }
-   */
-  get: (data, experiment_id, tag) => {
-    return new Promise((resolve, reject) => {
-      http
-        .get('/media/' + data, { params: { tag, experiment_id }, responseType: 'blob' })
-        .then((res) => {
-          resolve(res)
-        })
-        .catch((err) => {
-          // 如果blob的type为application/json，说明是后端返回的错误信息，需要解析
-          if (err.data.type === 'application/json') {
-            const reader = new FileReader()
-            reader.readAsText(err.data)
-            reader.onload = () => {
-              reject(JSON.parse(reader.result))
-            }
-          } else reject(err)
-        })
-    })
-  }
-}
-```
-
-### defaultColor
-
-图表默认颜色，为十六进制颜色值，例如：`#000000`，主要用于展示音频等图表的颜色。
-
-### getColor
-
-获取颜色的方法，接收一个source名称和当前source的index，返回一个颜色值。
-
-### 订阅与取消订阅
-
-图表仪表盘需要订阅数据以展示，因此需要传入订阅和取消订阅的方法。前者用于获取数据，后者用于避免不必要的数据请求。
-
-对于订阅函数而言，图表内部将会传入以下几个参数来期望外部实现数据订阅：
-| 属性 | 类型 | 说明 |
-| --- | --- | --- |
-| sources | array<string> | 即某个图表的sources字段 |
-| cid | any | 某个图表的id |
-| callback | function | 图表内部的回调函数，当数据请求完毕时应该执行此函数 |
-
-对于回调函数而言，他应该接收以下几个参数：
-| 属性 | 类型 | 说明 |
-| --- | --- | --- |
-| source | string | 告知图表是sources字段中的哪个源发生了更新 |
-| data | object | 更新后的数据，详见下方的data |
-| error | any | 错误信息 |
-
-## 图表组件
-
-所有的图表组件都将有统一的props输入：
-| 属性 | 类型 | 说明 |
-| --- | --- | --- |
-| title | String | 图表标题 |
-| chart | Object | 图表配置 |
-| index | Number | 图表在当前命名空间中的排序 |
-
-所有的图表也将有通用的api，通过defineExpose暴露给父组件：
-| 方法 | 类型 | 说明 |
-| --- | --- | --- |
-| render | Function | 渲染图表 |
-| change | Function | 重渲染图表 |
-| zoom | Function | 放大图表 |
-| smooth | Function | 图表平滑 |
-
-其中，render、change和zoom将接受一个参数，即图表数据，其数据格式为：
-
-```js
-const data = {
-    "数据名称": {
-        list:[
-            {
-              data: "数据",
-              step: "步骤"，
-              create_time: "创建时间"
-            },
-            ...
-        ],
-        ... // 一些其他的配置字段
-    }
-    ...
-}
-```
-
-这些数据由组件使用者提供，通过依赖注入的方式为内部组件提供订阅数据、更新数据的能力。
-
-本模块不再遵守vue单向数据流规范，但是也不鼓励暴露数据，而应该暴露api，通过api来完成逻辑的处理。
-
-大致的组件模版可以输入`vue-chart`，由代码片段生成。
diff --git a/vue/src/charts/components/ChartContainer.vue b/vue/src/charts/components/ChartContainer.vue
deleted file mode 100644
index 68516083..00000000
--- a/vue/src/charts/components/ChartContainer.vue
+++ /dev/null
@@ -1,346 +0,0 @@
-<template>
-  <section
-    class="chart-container"
-    :class="chartComponent.class"
-    @mouseenter="handleMouseEnter"
-    @mouseleave="handleMouseLeave"
-  >
-    <!-- 错误 -->
-    <div class="flex flex-col justify-center grow text-dimmer gap-2" v-if="status === 'error'">
-      <SLIcon class="mx-auto h-5 w-5" icon="error" />
-      <p class="text-center text-xs">{{ $t('chart.error') }}</p>
-    </div>
-    <template v-else>
-      <!-- 图表相关控制按钮 -->
-      <div class="chart-panel">
-        <!-- 取消置顶的按钮 -->
-        <PanelButton
-          class="!text-positive-default"
-          icon="pinned"
-          click-icon="loading"
-          click-class="animate-spin"
-          :tip="$t('chart.unpin')"
-          @click="unpin"
-          v-if="isPinned"
-        />
-        <template v-if="hover && !unknown && !isAllError">
-          <!-- 置顶 -->
-          <PanelButton
-            icon="pin"
-            :tip="$t('chart.pin')"
-            @click="pin"
-            click-icon="loading"
-            click-class="animate-spin"
-            v-if="!isPinned"
-          />
-          <!-- 放大功能 -->
-          <PanelButton icon="zoom" :tip="$t('chart.zoom')" @click="zoom" />
-          <!-- 更多功能 -->
-          <ExportImage :index="index" :chart="chart" :smoothMethod="smoothMethod" v-model="downloadModal" />
-          <SLMenu menu-class="flex-shrink-0" class="right-0 top-4">
-            <PanelButton icon="more" :tip="$t('chart.more')" />
-            <template #pop>
-              <SLMenuItems>
-                <SLMenuItem class="py-1" @click="unhide" v-if="isHidden">
-                  {{ $t('chart.unhide') }}
-                </SLMenuItem>
-                <SLMenuItem class="py-1" @click="hide" v-else>
-                  {{ $t('chart.hide') }}
-                </SLMenuItem>
-                <SLMenuItem class="py-1" v-if="isLineChart" @click="downloadModal = true">
-                  {{ $t('chart.charts.share.download.button') }}
-                </SLMenuItem>
-              </SLMenuItems>
-            </template>
-          </SLMenu>
-        </template>
-      </div>
-      <component :index="index" ref="chartRef" :is="chartComponent.type" :title="chart.name" :chart="chart" />
-    </template>
-    <!-- 加载中 -->
-    <div class="h-full w-full top-0 left-0 bg-default flex items-center justify-center absolute rounded" v-if="loading">
-      <SLLoading />
-    </div>
-  </section>
-</template>
-
-<script setup>
-/**
- * @description: 图表容器，用于包裹并渲染图表组件
- * @file: ChartContainer.vue
- * @since: 2023-12-12 21:01:26
- **/
-import { inject } from 'vue'
-import { onUnmounted, computed, watch, onMounted } from 'vue'
-import { ref } from 'vue'
-import SLIcon from '@swanlab-vue/components/SLIcon.vue'
-// import SLLoading from '@swanlab-vue/components/SLLoading.vue'
-import { addTaskToBrowserMainThread } from '@swanlab-vue/utils/browser'
-import LineChart from '../package/LineChart.vue'
-import AudioChart from '../package/AudioChart.vue'
-import TextChart from '../package/TextChart.vue'
-import UnknownChart from '../package/UnknownChart.vue'
-import PanelButton from './PanelButton.vue'
-import { debounce } from '@swanlab-vue/utils/common'
-import ImageChart from '../package/ImageChart.vue'
-import SLLoading from '@swanlab-vue/components/SLLoading.vue'
-import SLMenu from '@swanlab-vue/components/menu/SLMenu.vue'
-import SLMenuItems from '@swanlab-vue/components/menu/SLMenuItems.vue'
-import SLMenuItem from '@swanlab-vue/components/menu/SLMenuItem.vue'
-import ExportImage from '../components/ExportImage.vue'
-
-const props = defineProps({
-  chart: {
-    type: Object,
-    required: true
-  },
-  index: {
-    type: Number,
-    required: true
-  },
-  isPinned: {
-    type: Boolean,
-    required: true
-  },
-  isHidden: {
-    type: Boolean,
-    required: true
-  }
-})
-
-const cid = props.chart.id
-const source = props.chart.source
-const emit = defineEmits(['pin', 'unpin', 'hide', 'unhide'])
-
-// ---------------------------------- 判断是否处于hover状态 ----------------------------------
-const hover = ref(false)
-const handleMouseEnter = () => (hover.value = true)
-const handleMouseLeave = () => (hover.value = false)
-
-// ---------------------------------- 控制chart显示状态 ----------------------------------
-// 是否全部数据都错
-const isAllError = computed(() => {
-  return Object.keys(props.chart.error || {}).length === source.length
-})
-
-const status = ref(props.chart.error ? 'success' : undefined)
-const loading = ref(!isAllError.value)
-
-const unknown = ref(false)
-/**
- * @description: 根据chart的type，返回对应的chart组件
- * @return {Object} {type: ChartComponent, class: String}, 其中type为图表组件，class为图表组件的class，class可能不存在
- * @example: {type: LineChart, class: 'line-chart'} 或者 {type: UnknownChart}
- */
-const chartComponent = computed(() => {
-  switch (props.chart.type) {
-    case 'default':
-      return {
-        type: LineChart,
-        class: 'line-chart'
-      }
-    case 'line':
-      return {
-        type: LineChart,
-        class: 'line-chart'
-      }
-    case 'audio':
-      return {
-        type: AudioChart,
-        class: 'audio-chart'
-      }
-    case 'image':
-      return {
-        type: ImageChart,
-        class: 'image-chart'
-      }
-    case 'text':
-      return {
-        type: TextChart,
-        class: 'text-chart'
-      }
-    default:
-      return {
-        type: UnknownChart
-      }
-  }
-})
-
-// 监听chartComponent的变化，如果变化为UnknownChart，则unknown为true
-// 如果放在chartComponent的computed中会带来额外副作用
-watch(
-  chartComponent,
-  (newVal) => {
-    unknown.value = newVal.type === UnknownChart
-  },
-  {
-    immediate: true
-  }
-)
-
-// 判断是否为Promise或者AsyncFunction
-function isPromiseAndAsyncFunction(func) {
-  return (
-    (func instanceof Promise ||
-      (func !== null &&
-        func !== undefined &&
-        typeof func === 'object' &&
-        typeof func.then === 'function' &&
-        typeof func.catch === 'function')) &&
-    func.constructor &&
-    func.constructor.name === 'AsyncFunction'
-  )
-}
-// ---------------------------------- 订阅 ----------------------------------
-let data = {}
-provide('data', data)
-// 是否已经渲染，用于控制执行render方法还是change方法
-let hasInited = false
-const $off = inject('$off')
-// 如果props.chart.error存在，则不订阅
-onMounted(() => {
-  // error的key数量不等于source的长度
-  !isAllError.value &&
-    inject('$on')(source, cid, (tag, _tagData, error) => {
-      // 异步回调（其实是同步），用于控制图表的显示状态
-      return new Promise((resolve, reject) => {
-        if (error) {
-          status.value = 'error'
-          loading.value = false
-          reject(error)
-        } else {
-          data[tag] = _tagData
-          // 判断data的key数量是否和source长度相同
-          if (Object.keys(data).length === source.length) {
-            // 渲染
-            addTaskToBrowserMainThread(() => {
-              if (!hasInited) {
-                // 判断render方法是否为Promise，如果是，则等待渲染完成再resolve并设置status为success
-                render(data)
-              } else change(data)
-              hasInited = true
-              resolve()
-            })
-          }
-        }
-      })
-    })
-})
-
-const isChartError = computed(() => {
-  return props.chart.error && Object.keys(props.chart.error).length > 0
-})
-
-// 卸载时取消订阅
-isChartError.value ||
-  onUnmounted(() => {
-    $off(source, cid)
-  })
-
-// ---------------------------------- 图表对象控制 ----------------------------------
-const chartRef = ref(null)
-// 渲染功能
-const render = debounce(() => {
-  // 判断是否为Promise，如果是，则等待渲染完成再resolve并设置loading为false
-  // console.log('isPromise(chartRef.value.render)', isPromise(chartRef.value.render), chartRef.value.render)
-  const r = chartRef.value?.render
-  if (isPromiseAndAsyncFunction(r)) r(data).finally(() => (loading.value = false))
-  else {
-    r && r(data)
-    loading.value = false
-  }
-}, 100)
-// 重渲染功能
-const change = debounce(() => {
-  chartRef.value.change(data)
-}, 100)
-// 放大功能
-const zoom = () => {
-  chartRef.value.zoom(data)
-}
-
-// ---------------------------------- 图表显示/隐藏tooltip ----------------------------------
-
-const showTooltip = (...args) => {
-  chartRef.value?.showTooltip && chartRef.value.showTooltip(...args)
-}
-
-const hideTooltip = () => {
-  chartRef.value?.hideTooltip && chartRef.value.hideTooltip()
-}
-
-// ---------------------------------- 图表加粗 ----------------------------------
-const thicken = (...args) => {
-  chartRef.value?.thicken && chartRef.value.thicken(...args)
-}
-
-const thin = (...args) => {
-  chartRef.value?.thin && chartRef.value.thin(...args)
-}
-
-// ---------------------------------- 图表平滑方法 ----------------------------------
-const smoothMethod = ref(null)
-const smooth = (method) => {
-  smoothMethod.value = method
-  chartRef.value?.smooth && chartRef.value.smooth(method)
-}
-
-// ---------------------------------- 图表置顶 ----------------------------------
-const pin = () => {
-  emit('pin', props.chart)
-}
-const unpin = () => {
-  emit('unpin', props.chart)
-}
-
-// ---------------------------------- 图表隐藏 ----------------------------------
-
-const hide = () => {
-  emit('hide', props.chart)
-}
-
-const unhide = () => {
-  emit('unhide', props.chart)
-}
-
-// ---------------------------------- 下载折线图 ----------------------------------
-
-const downloadModal = ref(false)
-
-const isLineChart = computed(() => {
-  if (isChartError.value) return false
-  return props.chart?.type === 'default' || props.chart?.type === 'line'
-})
-
-// ---------------------------------- 暴露组件对象 ----------------------------------
-defineExpose({
-  chartRef,
-  smooth,
-  thicken,
-  thin,
-  showTooltip,
-  hideTooltip
-})
-</script>
-
-<style lang="scss" scoped>
-.chart-container {
-  @apply w-full h-auto min-h-[288px] border rounded relative bg-default;
-  @apply px-3 py-4;
-  @apply flex-col flex justify-between;
-}
-
-.chart-panel {
-  @apply absolute top-1 right-2 h-4;
-  @apply flex justify-end items-center gap-2;
-}
-// ---------------------------------- 图表样式 ----------------------------------
-.line-chart {
-  @apply col-span-3 lg:col-span-1;
-}
-.audio-chart,
-.image-chart,
-.text-chart {
-  @apply col-span-3;
-}
-</style>
diff --git a/vue/src/charts/components/ChartsContainer.vue b/vue/src/charts/components/ChartsContainer.vue
deleted file mode 100644
index 82f95068..00000000
--- a/vue/src/charts/components/ChartsContainer.vue
+++ /dev/null
@@ -1,124 +0,0 @@
-<template>
-  <!-- 容器 -->
-  <div class="charts-container" ref="chartsContainerRef">
-    <!-- 控制栏 -->
-    <div class="flex items-center">
-      <!-- 关闭、展开按钮 -->
-      <button class="flex items-center gap-1 relative" @click="handleNamespaceClick">
-        <SLIcon icon="down" class="w-6 h-6 absolute -left-6" :class="{ '-rotate-90': !isExpand }" />
-        {{ label }}
-        <span class="px-3 bg-highest text-sm rounded-full ml-2 text-difault">{{ charts.length || 0 }}</span>
-      </button>
-      <!-- 中间其他操作区 -->
-      <div class="grow"></div>
-      <!-- 添加图表的按钮 -->
-    </div>
-    <!-- 图表网格 -->
-    <div class="charts-slot" v-show="isExpand" ref="chartsSlotRef">
-      <ChartContainer
-        v-for="(chart, index) in charts"
-        :key="chart.id"
-        :chart="chart"
-        :index="index"
-        :isPinned="isPinned"
-        :isHidden="isHidden"
-        :ref="(el) => setChartRefList(el, index)"
-        @pin="pin"
-        @unpin="unpin"
-        @hide="hide"
-        @unhide="unhide"
-      />
-    </div>
-  </div>
-</template>
-
-<script setup>
-import SLIcon from '@swanlab-vue/components/SLIcon.vue'
-import ChartContainer from './ChartContainer.vue'
-import { ref, provide } from 'vue'
-/**
- * @description: 图表容器，用于包裹图表组件，一个图表容器对应一个namespace
- * @file: ChartsContainer.vue
- * @since: 2023-12-09 15:35:28
- **/
-const props = defineProps({
-  label: {
-    type: String,
-    required: true
-  },
-  charts: {
-    type: Array,
-    required: true
-  },
-  // namespace的索引
-  index: {
-    type: Number,
-    required: true
-  },
-  opened: {
-    type: Boolean,
-    required: true
-  },
-  isPinned: {
-    type: Boolean,
-    required: true
-  },
-  isHidden: {
-    type: Boolean,
-    required: true
-  }
-})
-const emit = defineEmits(['switch', 'pin', 'unpin', 'hide', 'unhide'])
-// ---------------------------------- 控制展开和关闭的状态 ----------------------------------
-const isExpand = ref(props.opened)
-
-const handleNamespaceClick = () => {
-  isExpand.value = !isExpand.value
-  emit('switch', isExpand.value)
-}
-
-// ---------------------------------- charts组件列表 ----------------------------------
-const chartRefList = ref([])
-
-const setChartRefList = (el, index) => {
-  chartRefList.value[index] = el
-  chartRefList.value.length = props.charts.length
-}
-
-// ---------------------------------- 将charts组建传递给子组件 ----------------------------------
-provide('chartRefList', chartRefList)
-
-defineExpose({
-  chartRefList
-})
-
-// ---------------------------------- 图表置顶、隐藏、取消置顶、取消隐藏 ----------------------------------
-const pin = (chart) => {
-  emit('pin', chart)
-}
-
-const unpin = (chart) => {
-  emit('unpin', chart)
-}
-
-const hide = (chart) => {
-  emit('hide', chart)
-}
-
-const unhide = (chart) => {
-  emit('unhide', chart)
-}
-</script>
-
-<style lang="scss" scoped>
-.charts-container {
-  @apply py-6 px-8;
-  &:not(:last-child) {
-    @apply border-b;
-  }
-
-  .charts-slot {
-    @apply grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6 mt-4.5;
-  }
-}
-</style>
diff --git a/vue/src/charts/components/DownloadButton.vue b/vue/src/charts/components/DownloadButton.vue
deleted file mode 100644
index 01123479..00000000
--- a/vue/src/charts/components/DownloadButton.vue
+++ /dev/null
@@ -1,15 +0,0 @@
-<template>
-  <SLButton class="p-1.5 rounded border flex items-center justify-cente h-7.5 w-7.5">
-    <SLIcon class="text-dimmer w-full h-full" icon="download" />
-  </SLButton>
-</template>
-
-<script setup>
-/**
- * @description: 下载按钮
- * @file: DownloadButton.vue
- * @since: 2024-02-01 14:16:10
- **/
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/charts/components/ExportImage.vue b/vue/src/charts/components/ExportImage.vue
deleted file mode 100644
index ef8974c3..00000000
--- a/vue/src/charts/components/ExportImage.vue
+++ /dev/null
@@ -1,157 +0,0 @@
-<template>
-  <SLModal max-w="890" class="overflow-hidden" v-model="downloadModal" escExit>
-    <h1 class="text-lg px-5 font-semibold py-4 border-b">
-      {{ $t('chart.charts.share.download.png.title') }}
-    </h1>
-    <div class="flex gap-5 px-5 py-6 md:flex-row flex-col">
-      <div class="param">
-        <span>{{ $t('chart.charts.share.download.png.name') }}:</span>
-        <input
-          type="text"
-          class="w-40 text-sm border px-2 py-1 rounded ml-2"
-          :placeholder="placeholder"
-          v-model="name"
-        />
-      </div>
-      <div class="param">
-        <span>{{ $t('chart.charts.share.download.png.width') }}:</span>
-        <InputSlideBar min="200" max="1200" v-model="width" />
-      </div>
-      <div class="param">
-        <span>{{ $t('chart.charts.share.download.png.height') }}:</span>
-        <InputSlideBar min="200" max="1200" v-model="height" />
-      </div>
-    </div>
-    <!-- 图像主体 -->
-    <div class="mx-auto mb-6 py-4" :style="{ width: `${width}px` }">
-      <div class="relative w-full h-full" ref="chartRef">
-        <LineChart :index="index" ref="lineChartRef" :title="chart.name" :chart="chart" />
-      </div>
-    </div>
-    <div class="flex justify-end p-5 pt-0 gap-5">
-      <SLButton
-        theme="primary"
-        class="py-1.5 px-3 rounded"
-        @click="download"
-        :text="$t('chart.charts.share.download.png.download')"
-      />
-    </div>
-  </SLModal>
-</template>
-
-<script setup>
-/**
- * @description:
- * @file: ExportImage.vue
- * @since: 2024-05-14 16:22:09
- **/
-import SLModal from '@swanlab-vue/components/SLModal.vue'
-import SLButton from '@swanlab-vue/components/SLButton.vue'
-import LineChart from '../package/LineChart.vue'
-import { generateFileNameWithTime } from '@swanlab-vue/utils/common'
-import html2canvas from 'html2canvas'
-import { addTaskToBrowserMainThread } from '@swanlab-vue/utils/browser'
-import InputSlideBar from './InputSlideBar.vue'
-
-const props = defineProps({
-  chart: {
-    type: Object,
-    required: true
-  },
-  index: {
-    type: Number,
-    required: true
-  },
-  modelValue: {
-    type: Boolean,
-    default: false
-  },
-  smoothMethod: {
-    type: Object,
-    default: null
-  }
-})
-const emit = defineEmits(['update:modelValue'])
-const data = inject('data')
-/**
- * @type {import('vue').Ref} lineChartRef
- */
-const lineChartRef = ref(null)
-/**
- * @type {import('vue').Ref<HTMLDivElement>} chartRef
- */
-const chartRef = ref(null)
-
-const downloadModal = computed({
-  get() {
-    // 显示时渲染图表
-    if (props.modelValue) {
-      lineChartRef.value?.render(data)
-      props.smoothMethod && lineChartRef.value?.smooth(props.smoothMethod)
-    } else {
-      // 关闭时清空长宽数值
-      width.value = originalWidth
-      height.value = originalHeight
-    }
-    return props.modelValue
-  },
-  set(val) {
-    emit('update:modelValue', val)
-  }
-})
-
-// ---------------------------------- 名称、长宽等属性 ----------------------------------
-
-const placeholder = generateFileNameWithTime('SwanLab-Chart').split('.')[0]
-const name = ref('')
-const originalWidth = 600
-const originalHeight = 400
-const width = ref(originalWidth)
-const height = ref(originalHeight)
-
-// ---------------------------------- 设置动态拦截高度设置 ----------------------------------
-
-watch([height, downloadModal, lineChartRef], (newVal) => {
-  // 设置高度
-  if (!lineChartRef.value) return
-  if (!newVal[1]) return
-  const h = newVal[0]
-  console.log(h)
-  /**
-   * @type {setOriginalChartHeight} lineChartRef.value.setHeight
-   */
-  addTaskToBrowserMainThread(() => {
-    lineChartRef.value.setHeight(h)
-  })
-})
-
-// ---------------------------------- 下载按钮 ----------------------------------
-
-const download = () => {
-  html2canvas(chartRef.value).then(async (canvas) => {
-    // 新建canvas，为canvas本身增加一圈白色边框
-    const borderWidth = 32
-    const newCanvas = document.createElement('canvas')
-    const ctx = newCanvas.getContext('2d')
-    newCanvas.width = canvas.width + borderWidth * 2
-    newCanvas.height = canvas.height + borderWidth * 2
-    ctx.fillStyle = '#fff'
-    ctx.fillRect(0, 0, newCanvas.width, newCanvas.height)
-    ctx.drawImage(canvas, borderWidth, borderWidth)
-    // 创建一个临时链接元素
-    const link = document.createElement('a')
-    link.href = newCanvas.toDataURL('image/png')
-    link.download = `${name.value || placeholder}.png`
-    link.click()
-  })
-}
-</script>
-
-<style lang="scss" scoped>
-.param {
-  @apply flex items-center text-dimmer;
-  span {
-    @apply w-20 md:w-auto block;
-  }
-}
-</style>
diff --git a/vue/src/charts/components/InputSlideBar.vue b/vue/src/charts/components/InputSlideBar.vue
deleted file mode 100644
index a1725ab0..00000000
--- a/vue/src/charts/components/InputSlideBar.vue
+++ /dev/null
@@ -1,87 +0,0 @@
-<template>
-  <SLMenu corner-class="left-3" menu-class="ml-2" menu-min-width="200">
-    <template #default="{ open }">
-      <input
-        type="number"
-        class="w-16 text-sm"
-        :value="value"
-        @click="(e) => handleClickInput(e, open)"
-        @change="handleInputChange"
-        @keydown.enter="handleInputChange"
-      />
-    </template>
-    <template #pop>
-      <div class="px-4">
-        <SLSlideBar is-int v-model="value" :max="Number(max)" :min="Number(min)" />
-      </div>
-    </template>
-  </SLMenu>
-</template>
-
-<script setup>
-import SLSlideBar from '@swanlab-vue/components/SLSlideBar.vue'
-import SLMenu from '@swanlab-vue/components/menu/SLMenu.vue'
-
-/**
- * @description: 数字输入滑动条
- * @file: InputSlideBar.vue
- * @since: 2024-05-17 18:06:08
- **/
-
-const props = defineProps({
-  modelValue: {
-    type: Number,
-    required: true
-  },
-  min: {
-    type: String,
-    required: true
-  },
-  max: {
-    type: String,
-    required: true
-  }
-})
-
-const emit = defineEmits(['update:modelValue'])
-
-const value = computed({
-  get: () => props.modelValue,
-  set: (val) => {
-    if (val < Number(props.min)) {
-      emit('update:modelValue', Number(props.min))
-    } else if (val > Number(props.max)) {
-      emit('update:modelValue', Number(props.max))
-    } else {
-      emit('update:modelValue', val)
-    }
-  }
-})
-
-// ---------------------------------- handleInputChange ----------------------------------
-const handleInputChange = (e) => {
-  const val = Number(e.target.value)
-  if (val < Number(props.min)) {
-    emit('update:modelValue', Number(props.min))
-  } else if (val > Number(props.max)) {
-    emit('update:modelValue', Number(props.max))
-  } else {
-    emit('update:modelValue', val)
-  }
-}
-
-// ---------------------------------- handleClickInput ----------------------------------
-const handleClickInput = (e, open) => {
-  // console.log('handleClickInput', e, open)
-  if (open) {
-    // console.log('阻止关闭')
-    e.stopPropagation()
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-input {
-  @apply border px-2 py-1 rounded;
-}
-</style>
diff --git a/vue/src/charts/components/LineChartLegend.vue b/vue/src/charts/components/LineChartLegend.vue
deleted file mode 100644
index 600f1a54..00000000
--- a/vue/src/charts/components/LineChartLegend.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<template>
-  <div class="lc-legend">
-    <div
-      class="lc-legend-item"
-      :style="{ color: item.color }"
-      v-for="item in items"
-      :key="item.name"
-      @mouseenter="handleMouseenter(item)"
-      @mouseleave="handleMouseleave(item)"
-      @click="handleClick(item)"
-    >
-      <RouterLink :to="experimentPrefix + item.experiment_id">
-        {{ item.name }}
-      </RouterLink>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import { inject } from 'vue'
-
-/**
- * @description: 折线图图例组件
- * @file: LineChartLegend.vue
- * @since: 2024-02-27 15:02:13
- **/
-
-defineProps({
-  items: {
-    type: Array,
-    default: () => []
-  }
-})
-// console.log('items', props.items)
-
-const experimentPrefix = inject('experimentPrefix')
-
-const emit = defineEmits(['hoverin', 'hoverout'])
-
-// ---------------------------------- 悬浮事件 ----------------------------------
-const handleMouseenter = (item) => {
-  emit('hoverin', item)
-}
-const handleMouseleave = (item) => {
-  emit('hoverout', item)
-}
-
-// ---------------------------------- 点击事件 ----------------------------------
-const handleClick = (item) => {
-  emit('click', item)
-}
-</script>
-
-<style lang="scss" scoped>
-.lc-legend {
-  max-height: 28px;
-  min-height: 16px;
-  @apply overflow-y-auto flex flex-wrap px-3 mb-1 gap-y-0.5 gap-x-2 leading-none text-[12px];
-  @apply justify-center;
-  .lc-legend-item {
-    @apply flex flex-shrink-0 items-center hover:brightness-75;
-    &:before {
-      content: '';
-      display: inline-block;
-      width: 8px;
-      height: 2px;
-      margin-top: 2px;
-      margin-right: 4px;
-      background-color: currentColor;
-    }
-  }
-}
-</style>
diff --git a/vue/src/charts/components/LineChartTooltip.vue b/vue/src/charts/components/LineChartTooltip.vue
deleted file mode 100644
index 08f280b3..00000000
--- a/vue/src/charts/components/LineChartTooltip.vue
+++ /dev/null
@@ -1,186 +0,0 @@
-<template>
-  <div class="lc-tooltip" ref="toolTipRef" v-show="isShow" :key="key">
-    <!-- <div class="lc-tooltip-item-zoom" v-if="detail">
-      <p class="lc-tooltip-color font-semibold !text-sm"></p>
-      <p class="lc-tooltip-step font-semibold !text-sm">{{ $t('chart.charts.share.step') }}</p>
-      <p class="lc-tooltip-value font-semibold !text-sm">{{ $t('chart.charts.share.value') }}</p>
-      <p class="lc-tooltip-time font-semibold !text-sm">{{ $t('chart.charts.share.time') }}</p>
-      <p class="lc-tooltip-tag font-semibold !text-sm">{{ $t('chart.charts.share.tag') }}</p>
-    </div> -->
-    <template v-if="detail && items.length">
-      <div class="lc-tooltip-item-zoom" v-for="item in items" :key="item.color" :style="{ color: item.color }">
-        <!-- 颜色 -->
-        <span class="lc-tooltip-color lc-tooltip-color-rect"></span>
-        <!-- 步数 -->
-        <span class="lc-tooltip-step">{{ item.data.index }}</span>
-        <!-- 数据 -->
-        <span class="lc-tooltip-value">{{ formatNumber2SN(item.data.data) }}</span>
-        <!-- 标签 -->
-        <span class="lc-tooltip-tag">{{ item.data.series }}</span>
-        <!-- 时间 -->
-        <span class="lc-tooltip-time">{{ formatTime(item.data.create_time) }}</span>
-      </div>
-    </template>
-    <template v-else-if="items.length">
-      <div class="lc-tooltip-item-no-zoom" v-for="item in items" :key="item.data.color" :style="{ color: item.color }">
-        <!-- 颜色 -->
-        <span class="lc-tooltip-color lc-tooltip-color-rect"></span>
-        <!-- 步数 -->
-        <span class="lc-tooltip-step">{{ item.data.index }}</span>
-        <!-- 数据 -->
-        <span class="lc-tooltip-value">{{ formatNumber2SN(item.data.data) }}</span>
-        <!-- 标签 -->
-        <span class="lc-tooltip-tag">{{ item.data.series }}</span>
-      </div>
-    </template>
-    <p class="lc-tooltip-tip">{{ tip }}</p>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 自定义折线图提示框
- * @file: LineChartTooltip.vue
- * @since: 2024-02-24 15:36:59
- **/
-import { ref, inject } from 'vue'
-import { isApple } from '@swanlab-vue/utils/browser'
-import { t } from '@swanlab-vue/i18n'
-
-const props = defineProps({
-  // 是否显示详细版
-  detail: {
-    type: Boolean,
-    default: false
-  }
-})
-
-const tip = isApple ? t('chart.charts.line.copy.apple') : t('chart.charts.line.copy.windows')
-// 提示框宽度，用于计算提示框位置，但是不直接使用
-const tooltipWidth = props.detail ? 400 : 256
-// 提示框数据
-const items = ref([])
-// 显示模式，分为详细版和简单版
-const toolTipRef = ref(null)
-const isShow = ref(false)
-const tooltipXOffset = 50
-const key = ref(null)
-/**
- * 显示提示框，传入父元素宽度和显示x轴位置，计算提示框位置
- */
-const show = (data, width, x) => {
-  // 去除其中带&smooth后缀的数据，屏蔽展示
-  data = data.filter((item) => !item.data.series.includes('&smooth'))
-  // console.log('show', data, width, x)
-  data.sort((a, b) => b.data.data - a.data.data)
-  // key.value = Math.random()
-  items.value = data
-  isShow.value = true
-  const left = parseFloat(x.split('px')[0])
-  // console.log(left + tooltipWidth, width)
-  if (left + tooltipWidth - 100 > width) {
-    toolTipRef.value.style.right = `${width - left + tooltipXOffset}px`
-    toolTipRef.value.style.left = 'auto'
-  } else {
-    toolTipRef.value.style.left = `${left + tooltipXOffset}px`
-    toolTipRef.value.style.right = 'auto'
-  }
-}
-
-/**
- * 隐藏提示框，直接隐藏
- */
-const hide = () => {
-  isShow.value = false
-}
-
-// ---------------------------------- 依赖获取 ----------------------------------
-const formatNumber2SN = inject('formatNumber2SN')
-const formatTime = inject('formatTime')
-
-// ---------------------------------- 暴露方法 ----------------------------------
-
-defineExpose({
-  show,
-  hide
-})
-</script>
-
-<style lang="scss" scoped>
-.lc-tooltip {
-  @apply py-2 px-3 absolute bg-default border rounded z-10;
-  min-width: 180px;
-  box-shadow: rgba(21, 24, 31, 0.16) 0px 12px 24px 0px;
-  visibility: visible;
-
-  p {
-    @apply text-xs text-default font-semibold;
-  }
-
-  .lc-tooltip-item-no-zoom,
-  .lc-tooltip-item-zoom {
-    @apply flex items-center gap-3;
-    &:not(:last-child) {
-      @apply mb-1.5;
-    }
-
-    .lc-tooltip-color {
-      @apply w-5 flex items-center flex-shrink-0;
-    }
-
-    .lc-tooltip-color-rect {
-      &::before {
-        content: '';
-        display: inline-block;
-        width: 20px;
-        height: 6px;
-        border-radius: 2px;
-        margin-right: 5px;
-        background-color: currentColor;
-      }
-    }
-  }
-
-  .lc-tooltip-tip {
-    @apply font-normal text-dimmest text-xs flex-shrink-0;
-  }
-}
-
-.lc-tooltip-item-no-zoom {
-  @apply text-xs;
-  .lc-tooltip-step {
-    @apply font-semibold flex-shrink-0;
-    &::after {
-      content: ':';
-      @apply font-semibold;
-    }
-  }
-
-  .lc-tooltip-value {
-    @apply text-left font-semibold whitespace-nowrap;
-  }
-
-  .lc-tooltip-tag {
-    @apply truncate;
-  }
-}
-
-.lc-tooltip-item-zoom {
-  // @apply text-xs;
-  .lc-tooltip-step {
-    @apply w-7;
-  }
-
-  .lc-tooltip-value {
-    @apply text-left;
-  }
-
-  .lc-tooltip-time {
-    @apply w-32;
-  }
-
-  .lc-tooltip-tag {
-    @apply truncate;
-  }
-}
-</style>
diff --git a/vue/src/charts/components/PanelButton.vue b/vue/src/charts/components/PanelButton.vue
deleted file mode 100644
index 4212de16..00000000
--- a/vue/src/charts/components/PanelButton.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<template>
-  <button class="pannel-button" v-tippy="{ content: tip }" @click="handleIconClick" :disabled="disabled">
-    <SLIcon class="h-full aspect-square" :class="pannelIconClass" :icon="pannelIcon" />
-  </button>
-</template>
-
-<script setup>
-import SLIcon from '@swanlab-vue/components/SLIcon.vue'
-
-/**
- * @description: chart的按钮样式
- * @file: PanelButton.vue
- * @since: 2023-12-12 21:43:02
- **/
-
-const props = defineProps({
-  icon: {
-    type: String,
-    required: true
-  },
-  tip: {
-    type: String,
-    required: true
-  },
-  // 点击后的icon名称，可以不传入，这样点击前后的icon不变
-  clickIcon: {
-    type: String,
-    required: false
-  },
-  // 点击后的icon样式，可以不传入，这样点击前后的icon样式不变
-  clickClass: {
-    type: String,
-    required: false
-  }
-})
-const pannelIcon = ref(props.icon)
-const pannelIconClass = ref('')
-const disabled = ref(false)
-
-const handleIconClick = () => {
-  if (disabled.value) return
-  if (props.clickIcon) {
-    pannelIcon.value = props.clickIcon
-    disabled.value = true
-  }
-  if (props.clickClass) {
-    pannelIconClass.value = props.clickClass
-    disabled.value = true
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.pannel-button {
-  @apply w-4 h-4;
-  @apply text-dimmer h-full p-0.5 rounded hover:bg-positive-dimmest hover:text-positive-highest;
-  &:disabled {
-    @apply cursor-progress opacity-60;
-  }
-}
-</style>
diff --git a/vue/src/charts/components/PlayButton.vue b/vue/src/charts/components/PlayButton.vue
deleted file mode 100644
index 346a729d..00000000
--- a/vue/src/charts/components/PlayButton.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-<template>
-  <SLButton class="p-1 rounded border flex items-center justify-center" :style="{ color }" @click="handlePlay">
-    <SLIcon class="h-5 w-5" icon="pause" v-if="modelValue" />
-    <SLIcon class="h-5 w-5" icon="play" v-else />
-  </SLButton>
-</template>
-
-<script setup>
-/**
- * @description: 播放按钮
- * @file: PlayButton.vue
- * @since: 2024-01-30 21:20:05
- **/
-import { ref } from 'vue'
-
-const props = defineProps({
-  color: {
-    type: String,
-    required: true
-  },
-  modelValue: {
-    type: Boolean,
-    required: true
-  }
-})
-
-const emit = defineEmits(['play', 'pause', 'update:modelValue'])
-
-const handlePlay = () => {
-  if (!props.modelValue) {
-    emit('play')
-    emit('update:modelValue', true)
-  } else {
-    emit('pause')
-    emit('update:modelValue', false)
-  }
-}
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/charts/components/SlideBar.vue b/vue/src/charts/components/SlideBar.vue
deleted file mode 100644
index 0a93746d..00000000
--- a/vue/src/charts/components/SlideBar.vue
+++ /dev/null
@@ -1,151 +0,0 @@
-<template>
-  <div class="chart-slide">
-    <p class="w-9">{{ reference }}</p>
-    <!-- <p>{{ min }}</p> -->
-    <div class="slide">
-      <SLSlideBar is-int :max="max" :min="min" v-model="_modelValue" :bar-color="barColor" />
-    </div>
-    <!-- <p>{{ max }}</p> -->
-    <!-- 输入框 -->
-    <div class="flex items-center relative">
-      <input type="number" :value="_modelValue" ref="input" @keyup.enter="handleChange" @blur="handleChange" />
-      <div class="w-3 flex-shrink-0 flex-col flex absolute right-1">
-        <SLIcon icon="down" class="w-full h-3 -rotate-180 -mb-1" @click="handleClickUp" />
-        <SLIcon icon="down" class="w-full aspect-square" @click="handleClickDown" />
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 封装全局slidebar组件，添加一些其他功能
- * @file: SlideBar.vue
- * @since: 2024-01-30 16:18:31
- **/
-import { computed, ref, onUnmounted } from 'vue'
-import SLSlideBar from '@swanlab-vue/components/SLSlideBar.vue'
-
-const props = defineProps({
-  max: {
-    type: Number,
-    required: true
-  },
-  min: {
-    type: Number,
-    required: true
-  },
-  modelValue: {
-    type: Number,
-    required: true
-  },
-  barColor: {
-    type: String,
-    required: true
-  },
-  reference: {
-    type: String,
-    default: 'Step'
-  },
-  // 通过方向键切换
-  turnByArrow: {
-    type: Boolean,
-    default: false
-  },
-  // 方向键的类型，false：左右键，true：上下键
-  verticalArrow: {
-    type: Boolean,
-    default: false
-  }
-})
-
-const emits = defineEmits(['update:modelValue', 'change', 'turn'])
-
-const input = ref(null)
-
-const _modelValue = computed({
-  get() {
-    return props.modelValue
-  },
-  set(value) {
-    if (value < props.min) {
-      return (_modelValue.value = props.min)
-    } else if (value > props.max) {
-      return (_modelValue.value = props.max)
-    }
-    emits('update:modelValue', value)
-    emits('change', value)
-    input.value.value = _modelValue.value
-  }
-})
-
-// ---------------------------------- 上下键增加/减少数字 ----------------------------------
-
-/**
- * 向后渐少
- */
-const handleClickDown = () => {
-  if (_modelValue.value > props.min) {
-    emits('turn', 'backward', _modelValue.value)
-  }
-}
-
-/**
- * 向前增加
- */
-const handleClickUp = () => {
-  if (_modelValue.value < props.max) {
-    emits('turn', 'forward', _modelValue.value)
-  }
-}
-
-// ---------------------------------- 当input输入结束时，再次赋值 ----------------------------------
-
-const handleChange = (e) => {
-  if (e.target.value == _modelValue.value) return
-  _modelValue.value = e.target.value
-}
-
-// ---------------------------------- 键盘左右切换 ----------------------------------
-
-const handleKeydown = (e) => {
-  if (!props.turnByArrow) return
-  if (e.key == (props.verticalArrow ? 'ArrowDown' : 'ArrowLeft')) {
-    handleClickDown()
-  } else if (e.key == (props.verticalArrow ? 'ArrowUp' : 'ArrowRight')) {
-    handleClickUp()
-  }
-}
-
-if (props.turnByArrow) {
-  document.addEventListener('keydown', handleKeydown)
-}
-
-onUnmounted(() => {
-  if (props.turnByArrow) {
-    document.removeEventListener('keydown', handleKeydown)
-  }
-})
-</script>
-
-<style lang="scss" scoped>
-.chart-slide {
-  @apply flex items-center justify-center w-full gap-2 text-dimmer select-none flex-nowrap;
-  .slide {
-    @apply max-w-[230px] w-full;
-  }
-  input[type='number']::-webkit-inner-spin-button,
-  input[type='number']::-webkit-outer-spin-button {
-    -webkit-appearance: none;
-    margin: 0;
-  }
-  input[type='number'] {
-    -moz-appearance: textfield;
-    appearance: textfield;
-  }
-}
-
-input {
-  @apply w-16 h-6 pl-1 pr-5 rounded border outline-none bg-transparent text-xs focus:border-primary-default;
-}
-</style>
diff --git a/vue/src/charts/components/SmoothButton.vue b/vue/src/charts/components/SmoothButton.vue
deleted file mode 100644
index 758554fe..00000000
--- a/vue/src/charts/components/SmoothButton.vue
+++ /dev/null
@@ -1,152 +0,0 @@
-<template>
-  <SLMenu class="right-0 w-96" corner-class="left-2.5" not-down>
-    <div class="smooth-button">
-      <SLIcon class="w-full h-full" icon="smooth" />
-    </div>
-    <template #pop>
-      <div class="flex select-none flex-col w-full px-4 text-sm font-semibold gap-4">
-        <div class="flex items-center gap-2">
-          <span class="w-20 flex-shrink-0">{{ $t('chart.charts.share.smooth.smoothing') }}</span>
-          <SLSlideBar
-            show-input
-            v-model="_value"
-            :max="nowMethod.max"
-            :min="nowMethod.min"
-            @turn="handleTurn"
-            @change="handleChange"
-            :disabled="nowMethod.disabled"
-            :is-int="nowMethod.isInt"
-          />
-        </div>
-        <div class="flex items-center gap-2">
-          <span class="w-20 flex-shrink-0">{{ $t('chart.charts.share.smooth.method') }}</span>
-          <SLMenu menu-class="w-full" items-class="overflow-auto bg-default" down down-position="top-2.5 right-2">
-            <div class="w-full border p-2 text-left rounded hover:bg-higher">{{ nowMethod.name }}</div>
-            <template #pop>
-              <SLMenuItems class="method-items">
-                <SLMenuItem
-                  class="method-item"
-                  v-for="method in methods"
-                  :key="method.id"
-                  @click="handleMethodClick(method)"
-                >
-                  {{ method.name }}
-                </SLMenuItem>
-              </SLMenuItems>
-            </template>
-          </SLMenu>
-        </div>
-      </div>
-    </template>
-  </SLMenu>
-</template>
-
-<script setup>
-/**
- * @description: 平滑按钮，包含菜单
- * @file: SmoothButton.vue
- * @since: 2024-03-04 21:13:50
- **/
-import SLIcon from '@swanlab-vue/components/SLIcon.vue'
-import SLSlideBar from '@swanlab-vue/components/SLSlideBar.vue'
-import SLMenu from '@swanlab-vue/components/menu/SLMenu.vue'
-import SLMenuItems from '@swanlab-vue/components/menu/SLMenuItems.vue'
-import SLMenuItem from '@swanlab-vue/components/menu/SLMenuItem.vue'
-import { debounce } from '@swanlab-vue/utils/common'
-
-const props = defineProps({
-  defaultMethodId: {
-    type: Number,
-    default: 0
-  }
-})
-
-// 当method被改变/值被改变时，向外触发事件：smooth
-const emit = defineEmits(['smooth'])
-
-const debounceSmooth = debounce(() => {
-  emit('smooth', {
-    id: nowMethodId.value,
-    value: _value.value
-  })
-}, 300)
-// ---------------------------------- 算法配置和切换 ----------------------------------
-const methods = [
-  {
-    id: 0,
-    name: 'No Smoothing',
-    disabled: true
-  },
-  {
-    id: 1,
-    name: 'Time Weighted EMA',
-    disabled: false,
-    min: 0,
-    max: 0.99
-  },
-  {
-    id: 2,
-    name: 'Running Average',
-    disabled: false,
-    min: 1,
-    max: 100,
-    isInt: true
-  },
-  {
-    id: 3,
-    name: 'Gaussian Smoothing',
-    disabled: false,
-    min: 1,
-    max: 100,
-    isInt: true
-  }
-]
-// 当前选中的算法
-const nowMethodId = ref(props.defaultMethodId)
-// 当前选中的算法的配置
-const nowMethod = computed({
-  get: () => {
-    // 找不到就返回第一个
-    return methods.find((item) => item.id === nowMethodId.value) || methods[0]
-  }
-})
-// 当前选中算法的值
-const _value = ref(0)
-// ---------------------------------- 当方法被点击 ----------------------------------
-const handleMethodClick = (method) => {
-  nowMethodId.value = method.id
-  _value.value = method.min || 0
-  debounceSmooth()
-}
-// ---------------------------------- 处理slidebar的change和turn事件 ----------------------------------
-// 不需要考虑最大最小值溢出的情况，因为slidebar组件已经处理了
-const handleTurn = (direction) => {
-  console.log('direction', direction)
-  if (direction === 'forward') {
-    // 向前+1或+0.01,保留两位小数
-    _value.value = nowMethod.value.isInt ? _value.value + 1 : Number((_value.value + 0.01).toFixed(2))
-  } else {
-    // 向后-1或-0.01
-    _value.value = nowMethod.value.isInt ? _value.value - 1 : Number((_value.value - 0.01).toFixed(2))
-  }
-  debounceSmooth()
-}
-const handleChange = (value) => {
-  _value.value = Number(value.toFixed(2))
-  debounceSmooth()
-}
-</script>
-
-<style lang="scss" scoped>
-.smooth-button {
-  @apply h-8 w-8 p-1 rounded text-dimmer hover:text-positive-default hover:bg-positive-dimmer;
-}
-
-.method-items {
-  .method-item {
-    &:hover {
-      @apply bg-higher;
-    }
-  }
-}
-</style>
diff --git a/vue/src/charts/modules/AudioModule.vue b/vue/src/charts/modules/AudioModule.vue
deleted file mode 100644
index 2846c167..00000000
--- a/vue/src/charts/modules/AudioModule.vue
+++ /dev/null
@@ -1,385 +0,0 @@
-<template>
-  <!-- 音频容器，完成响应式 -->
-  <div class="audios-container" :class="setGrid()" ref="audiosRef">
-    <div class="border p-2 rounded" v-for="(audio, index) in audios" :key="audio.title">
-      <!-- 多实验时，展示实验标题 -->
-      <div class="text-xs flex items-center pb-1" :title="audio.exp" v-if="audio.exp">
-        <div class="h-2 w-2 rounded-full shrink-0" :style="{ backgroundColor: audio.color }"></div>
-        <p class="pl-1 truncate">{{ audio.exp }}</p>
-      </div>
-      <div class="audio-container" :ref="(el) => handleAddAudio(index, el)">
-        <div class="flex items-center mt-2">
-          <PlayButton
-            v-model="playingList[index]"
-            :color="defaultColor"
-            @play="handlePlay(index)"
-            @pause="handlePause(index)"
-          />
-          <!-- 当前时间 -->
-          <p class="text-sm ml-1" v-if="audioRef[index]">{{ formatTime(index) }}</p>
-          <!-- 文件名称 -->
-          <p class="text-sm w-full text-center -ml-10">{{ audios[index].caption }}</p>
-          <!-- 下载按钮 -->
-          <DownloadButton @click="download(index)" />
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 音频模块，用于播放音频，渲染音频波形图，被AudioChart.vue调用
- * @file: AudioModule.vue
- * @since: 2024-01-30 22:44:56
- **/
-import { addTaskToBrowserMainThread } from '@swanlab-vue/utils/browser'
-import { ref, inject, reactive, onUnmounted, onMounted } from 'vue'
-import PlayButton from '../components/PlayButton.vue'
-import { debounce } from '@swanlab-vue/utils/common'
-import DownloadButton from '../components/DownloadButton.vue'
-const props = defineProps({
-  // 接受的音频数据，格式为 [{ audioBuffer: AudioBuffer, title: String, tag: String, caption: String } ]
-  audios: {
-    type: Object,
-    required: true
-  }
-})
-const defaultColor = inject('defaultColor')
-// const getColor = inject('getColor')
-
-// 所有音频容器
-const audiosRef = ref(null)
-// 单个音频容器集合, {index:{audio:channels, dom:HTMLDivElement, ...}}
-const audioRef = reactive({})
-// 用于控制子播放组件的播放状态
-const playingList = ref(props.audios.map(() => false))
-
-// ---------------------------------- 将dom元素和tag对应起来 ----------------------------------
-const handleAddAudio = (index, el) => {
-  if (audioRef[index]) return // 防止重复添加
-  audioRef[index] = {
-    channels: sample(props.audios[index].audioBuffer),
-    dom: el,
-    // 音频源
-    ...createAudioContext(props.audios[index].audioBuffer),
-    // 当前音频播放的位置,百分比，秒
-    offset: 0
-  }
-
-  // console.log(audioRef, props.audios)
-  // 挂载完成后，开始绘制波形图
-  addTaskToBrowserMainThread(() => {
-    draw(index)
-  })
-}
-
-// ---------------------------------- 绘制波形的工具函数 ----------------------------------
-/**
- * 抽样
- * @param {AudioBuffer} buffer 音频缓存
- * @returns {array} [positives, negatives] 正数据和负数据
- */
-const sample = (buffer) => {
-  // 左声道数据
-  const leftChannel = buffer.getChannelData(0)
-  // 右声道数据
-  const rightChannel = buffer.numberOfChannels > 1 ? buffer.getChannelData(1) : null
-  // 执行采样，例如每隔10个采样点取一个
-  // const sampleRate = buffer.sampleRate
-  const step = 10
-  const sampledLeftChannel = []
-  const sampledRightChannel = []
-  for (let i = 0; i < leftChannel.length; i += step) {
-    sampledLeftChannel.push(leftChannel[i])
-    if (rightChannel) {
-      sampledRightChannel.push(rightChannel[i])
-    }
-  }
-  return [sampledLeftChannel, sampledRightChannel]
-}
-
-const formatTime = (index) => {
-  const duration = audioRef[index].duration
-  const offset = audioRef[index].offset
-  const now = duration * offset
-  const minutes = Math.floor(now / 60)
-  const seconds = Math.floor(now % 60)
-  // duration格式化，例如 01:30
-  const minutes2 = Math.floor(duration / 60)
-  const seconds2 = Math.floor(duration % 60)
-
-  return (
-    `${minutes}:${seconds < 10 ? '0' + seconds : seconds}` +
-    '/' +
-    `${minutes2}:${seconds2 < 10 ? '0' + seconds2 : seconds2}`
-  )
-}
-
-// ---------------------------------- 绘制波形图 ----------------------------------
-
-/**
- * @description: 绘制波形图
- * @param {HTMLDivElement} dom 音频容器,dom 将会往里面添加 canvas
- * @param {AudioBuffer} audioBuffer 音频数据, 基于该数据绘制波形图
- * @param {Number} index 音频数据在 props.audios 中的索引
- * @param {Number} r 比例，用于控制波形图的高度，越大越高
- * @param {Number} a 比例，控制关键帧的高度，越大越高
- */
-const draw = (index, r = 2, a = 2 / 3) => {
-  const dom = audioRef[index].dom
-  // dom.style.width = audiosRef.value.offsetWidth / 2 + 'px'
-  const offset = audioRef[index].offset
-  const channels = audioRef[index].channels
-  const color = defaultColor
-  // 将color变淡一些,代表已经播放过的部分
-  const color2 = color + '99'
-  // console.log('positives', positives, 'negatives', negatives)
-  // 如果dom的第一个子元素是canvas，则清空画布
-  let canvas, ctx
-  const ratio = window.devicePixelRatio || 1
-  // 定义画布大小，需要考虑到设备问题
-  const width = Math.round(dom.offsetWidth * ratio)
-  const height = Math.round(dom.offsetHeight * ratio)
-  if (dom.firstChild && dom.firstChild.tagName === 'CANVAS') {
-    canvas = dom.firstChild
-    // 清空画布
-    ctx = canvas.getContext('2d')
-    ctx.clearRect(0, 0, canvas.width, canvas.height)
-    canvas.width = width
-    canvas.height = height
-    // console.log('clear, 复用canvas')
-  } else {
-    // 创建 canvas上下文
-    canvas = document.createElement('canvas')
-    // 设置画布大小
-    canvas.width = width
-    canvas.height = height
-    ctx = canvas.getContext('2d')
-    // 为每个canvas添加一个点击事件，用于控制播放
-    canvas.addEventListener('click', (event) => {
-      handleClickCanvas(event, index)
-    })
-    // 挂载 canvas
-    canvas.style.width = '100%'
-    canvas.style.height = '80%'
-    // 添加为第一个子元素
-    dom.insertBefore(canvas, dom.firstChild)
-  }
-  let x = 0
-  let y = height / 2
-  // let offset = 0
-  ctx.lineWidth = 1
-  ctx.fillStyle = color2
-  // 最大高度为画布高度的0.4倍
-  const h = height * 0.4
-  //额外延伸5个像素点
-  const o = 3
-  // 判断长度与画布宽度的关系，一像素对应多少数据
-  const step = channels[0].length / width
-  const baseHeight = 2
-  // 第多少个像素之前是变淡的颜色
-  const offsetIndex = Math.min(width - 1, Math.max(0, parseInt(offset * width)))
-  // 先从左往右绘制数据
-  let num
-
-  for (let k = 0; k < width; k++) {
-    // 方便调试
-    if (k * step > channels[0].length) throw new Error('k*step > channels[0].length')
-    ctx.strokeStyle = k <= offsetIndex ? color2 : color
-    // 如果当前处于变淡的区域边界，绘制一条长线段
-    if (k === offsetIndex) {
-      ctx.beginPath()
-      ctx.moveTo(x + k, y - h * a)
-      ctx.lineTo(x + k, y + h * a)
-      ctx.closePath()
-      ctx.stroke()
-      ctx.fillStyle = color
-    }
-    // ctx.beginPath()
-    // ctx.moveTo(x + k, y)
-    // // 将当前k对应的数据绘制到画布上
-    // // 设置高度
-    num = channels[0][parseInt(k * step)] * h * r
-    num = num > 0 ? num + baseHeight + o : num - baseHeight - o
-    const rectHeight = Math.abs(num) + o
-    const ry = num > 0 ? y - num : y - o
-
-    // ctx.lineTo(x + k, y - num)
-    // ctx.closePath()
-    // ctx.stroke()
-
-    // 画矩形
-    ctx.fillRect(x + k, ry, 2, rectHeight)
-  }
-
-  ctx.fill()
-}
-
-// 重新绘制所有波形图，用于前端样式的响应式
-const redraw = () => {
-  for (let i = 0; i < props.audios.length; i++) {
-    draw(i)
-  }
-}
-
-const debounceRedraw = debounce(redraw, 500)
-
-// ---------------------------------- offset更新 ----------------------------------
-
-function createAudioOffestUpdator(index) {
-  const now = Date.now()
-  let last = now
-  const oldOffset = audioRef[index].offset
-  let timer = null
-  const update = () => {
-    // 如果last和now相差小于15ms
-    // console.log('Date.now() - last < 50', Date.now() - last < 50)
-    if (Date.now() - last < 50) {
-      timer = requestAnimationFrame(update)
-      return
-    }
-    last = Date.now()
-    audioRef[index].offset = (Date.now() - now) / audioRef[index].duration / 1000 + oldOffset
-    // console.log('audioRef[tag].offset', audioRef[tag].offset, audioRef[tag].duration, oldOffset)
-    draw(index)
-    // 如果offset大于1，则停止更新
-    if (audioRef[index].offset >= 1) {
-      // console.log('播放结束')
-      destory()
-      playingList.value[index] = false
-      return
-    }
-    timer = requestAnimationFrame(update)
-  }
-
-  const destory = () => {
-    cancelAnimationFrame(timer)
-    audioRef[index].updator = null
-  }
-
-  return {
-    start: () => {
-      timer = requestAnimationFrame(update)
-    },
-    destory
-  }
-}
-
-// ---------------------------------- 控制某个音频的播放或者暂停 ----------------------------------
-
-/**
- * @description: 创建音频上下文
- */
-const createAudioContext = (audioBuffer) => {
-  // 假设 audioBuffer 是你的 AudioBuffer 对象
-  const audioContext = new (window.AudioContext || window.webkitAudioContext)()
-  const source = audioContext.createBufferSource()
-  // console.log('audioBuffer', audioBuffer)
-
-  // 将 AudioBuffer 设置为源
-  source.buffer = audioBuffer
-  const duration = audioBuffer.duration
-  // 连接到音频上下文的输出
-  source.connect(audioContext.destination)
-  return { source, duration }
-}
-
-const handlePlay = (index) => {
-  // console.log('handlePlay', tag, index, audioRef[tag])
-  // 如果已经存在source，且没有暂停，则返回
-  if (playingList.value[index]) return console.log('已经在播放了')
-  if (audioRef[index].offset >= 1) audioRef[index].offset = 0
-  // 如果存在source，且已经暂停，则重新生成一个新的source，并且设置offset为上次暂停的位置
-  audioRef[index].source = createAudioContext(props.audios[index].audioBuffer).source
-  // 将百分比转换为秒
-  const offset = audioRef[index].offset * audioRef[index].duration
-  // console.log('offset', offset, audioRef[tag].duration, audioRef[tag].offset)
-
-  audioRef[index].source.start(0, offset)
-  playingList.value[index] = true
-  // 更新offset
-  audioRef[index].updator = createAudioOffestUpdator(index)
-  audioRef[index].updator.start()
-}
-
-const handlePause = (index) => {
-  // console.log('handlePause')
-  // console.log(audioRef)
-  if (!audioRef[index].source) {
-    // console.log('audiosRef.value[tag].source', audiosRef.value[tag].source)
-    // 如果不存在source，则返回
-    return
-  }
-  // 暂停音频
-  audioRef[index].source.stop()
-  playingList.value[index] = false
-  audioRef[index].updator?.destory()
-}
-
-// ---------------------------------- 点击播放处理 ----------------------------------
-const handleClickCanvas = (event, index) => {
-  // 更新当前点击水平位置的百分比，重新绘制波形图
-  audioRef[index].offset = event.offsetX / event.target.offsetWidth
-  draw(index)
-  // 如果音频正在播放，删除原来的source，重新生成一个新的source，并且设置offset为点击位置的百分比
-  console.log('playingList.value[index]', playingList.value[index])
-  if (playingList.value[index]) {
-    audioRef[index].source.stop()
-    playingList.value[index] = false
-    audioRef[index].updator?.destory()
-    handlePlay(index, index)
-  }
-}
-
-// ---------------------------------- 响应式处理 ----------------------------------
-// 监听audiosRef的宽度变化，重新绘制波形图
-const observer = new ResizeObserver(() => {
-  debounceRedraw()
-})
-onMounted(() => {
-  observer.observe(audiosRef.value)
-})
-
-// ---------------------------------- 组件卸载，停止播放 ----------------------------------
-
-onUnmounted(() => {
-  for (const index in audioRef) {
-    try {
-      audioRef[index].source?.stop()
-      audioRef[index].updator?.destory()
-    } catch (_) {
-      continue
-    }
-  }
-  observer.disconnect()
-})
-
-// ---------------------------------- audioBuffer to blob ----------------------------------
-const download = (index) => {
-  const blob = props.audios[index].audioBlob
-  const filename = props.audios[index].title
-  // console.log('blob', blob, filename)
-  const a = document.createElement('a')
-  a.href = URL.createObjectURL(blob)
-  a.download = filename
-  a.click()
-}
-
-// ---------------------------------- 判断元素个数，如果超过1个，就设置grid-cols-2 ----------------------------------
-const setGrid = () => {
-  if (props.audios.length > 1) {
-    return 'grid-cols-2'
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.audios-container {
-  @apply w-full grid gap-4;
-  .audio-container {
-    @apply h-48 w-full flex flex-col relative text-dimmer;
-    /* 如果只有一个元素，沾满两格，否则只占一格 */
-  }
-}
-</style>
diff --git a/vue/src/charts/modules/ImageModule.vue b/vue/src/charts/modules/ImageModule.vue
deleted file mode 100644
index 60b06927..00000000
--- a/vue/src/charts/modules/ImageModule.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<template>
-  <div class="image-detail">
-    <div class="text-xs flex items-center justify-center pb-1 w-full" :title="name" v-if="name">
-      <div class="h-2 w-2 rounded-full shrink-0" :style="{ backgroundColor: color }"></div>
-      <p class="pl-1 truncate max-w-full">{{ name }}</p>
-    </div>
-    <div class="image-container" @click="$emit('zoom', filename, index)">
-      <div class="relative">
-        <img :src="imagesData" />
-        <DownloadButton class="image-download-button" @click.stop="$emit('download', filename)" />
-      </div>
-    </div>
-    <p class="text-xs w-full text-center truncate" v-if="caption">{{ caption }}</p>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 图像模块，负责单个图像显示和暴露下载事件
- * @file: ImageModule.vue
- * @since: 2024-03-17 22:11:10
- **/
-import DownloadButton from '../components/DownloadButton.vue'
-defineProps({
-  filename: String,
-  imagesData: String,
-  // 图像描述
-  caption: String,
-  index: [Number, String],
-  // 实验名称
-  name: String,
-  color: String,
-  // 是否为多实验
-  multi: Boolean
-})
-
-defineEmits(['zoom', 'download'])
-</script>
-
-<style lang="scss" scoped>
-.image-detail {
-  @apply relative h-full w-full;
-  max-height: 13rem;
-
-  .image-container {
-    @apply relative w-full grow inline-flex justify-center cursor-pointer;
-    height: calc(100% - 2.5rem);
-    img {
-      @apply my-auto object-contain;
-      height: 100%;
-    }
-    &:hover .image-download-button {
-      @apply block;
-    }
-  }
-}
-</style>
diff --git a/vue/src/charts/modules/README.md b/vue/src/charts/modules/README.md
deleted file mode 100644
index ebb5f800..00000000
--- a/vue/src/charts/modules/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# 模块
-
-封装一些自己写的图表渲染模块，方便使用。
diff --git a/vue/src/charts/modules/TextDetail.vue b/vue/src/charts/modules/TextDetail.vue
deleted file mode 100644
index 6e5cb1fd..00000000
--- a/vue/src/charts/modules/TextDetail.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-  <div class="text-detail">
-    <!-- infos -->
-    <div v-for="item in info_list" :key="item" class="sm:flex pb-3">
-      <span class="block w-48 font-semibold shrink-0">{{ $t(`chart.charts.text.titles.${item.key}`) }}:</span>
-      <span :title="item.value">{{ item.value }}</span>
-    </div>
-    <!-- text -->
-    <p class="font-semibold pb-2">{{ $t('chart.charts.text.titles.text') }}:</p>
-    <div class="p-4 min-h-[20vh] max-h-[55vh] overflow-y-auto border rounded bg-default">
-      <p v-for="text in data.text.split('\n')" :key="text">
-        <span v-if="text != ''">{{ text }}</span>
-        <br v-else />
-      </p>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 展示 text 详细信息，与弹窗中调用
- * @file: TextDetail.vue
- * @since: 2024-02-21 13:46:40
- **/
-import { computed } from 'vue'
-
-const props = defineProps({
-  data: {
-    type: Object,
-    required: true,
-    default: () => {}
-  }
-})
-
-const info_list = computed(() => {
-  const line = props.data.line
-  return [
-    {
-      key: 'tag',
-      value: props.data.tag
-    },
-    {
-      key: 'step',
-      value: line.index
-    },
-    {
-      key: 'caption',
-      value: props.data.caption || '-'
-    },
-    {
-      key: 'count',
-      value: props.data.text.replace(/[\r\n\s]/g, '').length
-    }
-  ]
-})
-</script>
-
-<style lang="scss" scoped>
-.text-detail {
-  @apply px-6 py-10 overflow-hidden bg-higher rounded-lg;
-}
-</style>
diff --git a/vue/src/charts/modules/TextModule.vue b/vue/src/charts/modules/TextModule.vue
deleted file mode 100644
index a3eb2439..00000000
--- a/vue/src/charts/modules/TextModule.vue
+++ /dev/null
@@ -1,285 +0,0 @@
-<template>
-  <div class="w-full h-full">
-    <!-- title: show in modal -->
-    <p class="w-full text-center py-6 text-xl font-semibold" v-if="modal">{{ tag }}</p>
-    <!-- header -->
-    <div class="w-full flex items-center bg-higher border-y">
-      <div class="caption">Caption</div>
-      <div class="text">Text</div>
-    </div>
-    <!-- body -->
-    <div class="w-full h-[310px] overflow-y-auto" :class="{ 'h-[60vh]': modal, 'border-b': data.length > 1 }">
-      <!-- line -->
-      <div class="line" v-for="(text, i) in tableData[currentIndex]?.data" :key="text + i" v-show="!skeleton">
-        <!-- caption -->
-        <div class="caption">{{ tableData[currentIndex]?.more[i].caption }}</div>
-        <!-- text -->
-        <div class="text" :title="text">{{ text }}</div>
-        <!-- zoom icon -->
-        <SLIcon
-          icon="zoom"
-          class="w-5 h-5 p-1 absolute right-3 cursor-pointer bg-default icon hidden transition-all"
-          @click="zoom(text, i)"
-        />
-      </div>
-      <!-- 骨架屏 -->
-      <div v-if="skeleton && data[currentIndex]">
-        <div class="flex items-center border-b border-dimmest" v-for="i in [1, 2, 3]" :key="i">
-          <div class="md:w-40 w-24 h-10 px-4 shrink-0 flex items-center border-r">
-            <span class="skeleton w-full h-1/2"></span>
-          </div>
-          <div class="flex items-center w-full h-10 ml-4"><span class="skeleton w-1/2 h-1/2"></span></div>
-        </div>
-      </div>
-    </div>
-    <!-- 翻页 -->
-    <SlideBar
-      class="pt-2"
-      v-model="currentPage"
-      :min="pages.minIndex"
-      :max="pages.maxIndex"
-      :bar-color="color"
-      @change="turnPage"
-      @turn="clickToTurn"
-      :key="pages.maxIndex"
-      v-if="data.length > 1"
-      :turn-by-arrow="modal && !isZoom"
-    />
-    <!-- 数据详情 -->
-    <SLModal max-w="1200" v-model="isZoom" esc-exit>
-      <TextDetail :data="current" />
-    </SLModal>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 文字表格，被TextChart.vue调用
- * @file: TextModule.vue
- * @since: 2024-02-20 20:06:45
- **/
-import { ref, inject, computed, watch } from 'vue'
-import SLModal from '@swanlab-vue/components/SLModal.vue'
-import TextDetail from './TextDetail.vue'
-import SlideBar from '../components/SlideBar.vue'
-
-const props = defineProps({
-  data: {
-    type: Object
-  },
-  tag: {
-    type: String,
-    default: ''
-  },
-  modal: {
-    type: Boolean
-  },
-  modelValue: {
-    type: Boolean
-  }
-})
-
-const emits = defineEmits(['getText', 'update:modelValue'])
-
-const color = inject('defaultColor')
-const skeleton = ref(false)
-
-const tableData = computed(() => {
-  const data = props.data
-  return data.map((item) => {
-    if (!Array.isArray(item.data)) {
-      item.data = [item.data]
-      item.more = [item.more]
-    }
-    return item
-  })
-})
-
-// ---------------------------------- 分页 ----------------------------------
-
-/**
- * 分页信息，包括 minIndex，maxIndex，sum
- * minIndex: 最小页码
- * maxIndex: 最大页码
- * sum: 总页数
- */
-const pages = computed(() => {
-  const minIndex = Math.min(...indexes.value)
-  const maxIndex = Math.max(...indexes.value)
-  return { minIndex, maxIndex, sum: indexes.value.length }
-})
-
-/**
- * 对下面三个变量：
- * indexes: 对应 steps
- * currentPage: 当前页码，与 step 对应
- * currentIndex: 当前索引，与数据在数组中的索引对应
- *
- * 这样做的原因是，step 并不一定均匀，而是单增随机的
- * 所以 currentPage 是当前页码
- * 而 currentIndex 是当前页码所对应的数组索引
- * 对应到 props.data 的 list 中，currentIndex 就是数组索引，currentPage 就是索引对应元素中，index 属性值
- *
- * 这二者的关系被抽离，通过 indexes 联系：
- * indexes 中，把 props.data.list 中每一项的 step 提出，按照原本顺序排列
- * 当有 currentIndex 时，可以通过 indexes[currentIndex] 找到数据对应的 step
- * 而有 currentPage 时，也可以通过找到其在 indexes 中的位置而知道 index,从而通过 props.data.list[index] 获取数据
- */
-const indexes = computed(() => {
-  return props.data.map((item) => item.index)
-})
-const currentPage = ref(pages.value.minIndex)
-const currentIndex = ref(0)
-
-onMounted(() => {
-  currentIndex.value = Math.max(indexes.value.length - 1, 0)
-  currentPage.value = indexes.value[currentIndex.value]
-})
-
-/**
- * 在翻页时找到有效页码和页码索引
- * @param {int} targetNumber 翻页页码
- * @param {boolean} next 当前页码是向前还是向后, true 为向后翻页
- * @param {boolean} isClick 当前翻页操作是否是通过点击上下翻页按钮触发
- */
-const findClosestNumber = (targetNumber, next, isClick) => {
-  // 将字符串转换为数字
-  const numericArray = indexes.value.map(Number)
-  // 计算每个数字与给定数字之间的距离
-  const distances = numericArray.map((num) => Math.abs(num - targetNumber))
-  // 找到距离最近的数字
-  const minDistance = Math.min(...distances)
-  let index = distances.indexOf(minDistance)
-  let number = numericArray[index]
-  if (number === targetNumber) return { index, number }
-  if (!isClick) return { index, number }
-  // 这个时候已经找到绝对值上最接近的数，但是需要判断是向前还是向后翻页
-  if ((next && index !== indexes.value.length - 1) || (!next && index !== 0)) {
-    index += next ? 1 : -1
-    return { index, number: numericArray[index] }
-  }
-
-  return { index, number }
-}
-
-/**
- * 翻页，同时展示骨架屏
- * @param {number} p 当前页面
- * @param {boolean} isClick 是否是通过点击上下翻页按钮而触发
- */
-const time = ref()
-const turnPage = (p, isClick) => {
-  // 获取有效页码，index 是页码对应数据数组中的索引，number 是页码
-  const { index, number } = findClosestNumber(p, isClick, isClick !== undefined)
-  currentIndex.value = index
-  currentPage.value = Number(number)
-  // 骨架屏
-  skeleton.value = true
-  if (time.value) clearTimeout(time.value)
-  time.value = setTimeout(() => {
-    skeleton.value = false
-  }, 400)
-}
-
-/**
- * 通过点击上下按钮翻页
- * @param {string} type 前后，前为 forward，页码增加，后为 backward，页码减小
- * @param {int} p 当前页码
- */
-const clickToTurn = (type, p) => {
-  if (type === 'forward') return turnPage(p + 1, true)
-  return turnPage(p - 1, false)
-}
-
-// ---------------------------------- 放大 ----------------------------------
-
-const isZoom = ref(false)
-
-const current = ref({})
-/**
- * 放大查看行详情
- * @param {string} text 文本内容
- * @param {int} i v-for 时的行号索引
- *
- * 因为一个 step 下可能有多条文本，而单行和多行时对应数据结构并不一样，所以需要分辨一下
- */
-const zoom = (text, i) => {
-  // 当前页面所有的信息
-  const line = props.data[currentIndex.value]
-  current.value = {
-    tag: props.tag,
-    line,
-    // 单行时，more.caption 是 string，多行时为 array
-    caption: Array.isArray(line?.more) ? line?.more[i]?.caption : line?.more?.caption,
-    text
-  }
-  isZoom.value = true
-}
-
-watch(
-  () => isZoom.value,
-  (v) => {
-    emits('update:modelValue', v)
-  }
-)
-</script>
-
-<style lang="scss" scoped>
-.caption {
-  @apply md:w-40 w-24 h-10 px-4 border-r truncate shrink-0 pt-2;
-}
-
-.text {
-  @apply w-full py-2 px-4 truncate;
-}
-
-.line {
-  @apply border-b border-dimmest flex items-center relative;
-
-  &:hover {
-    @apply bg-higher;
-    .icon {
-      @apply border border-dimmest rounded-sm hover:bg-highest hover:rounded block;
-    }
-  }
-}
-
-@-webkit-keyframes skeleton-ani {
-  0% {
-    left: 0;
-  }
-
-  to {
-    left: 100%;
-  }
-}
-
-@keyframes skeleton-ani {
-  0% {
-    left: 0;
-  }
-
-  to {
-    left: 100%;
-  }
-}
-
-.skeleton {
-  @apply block;
-  background-image: linear-gradient(-45deg, #f5f5f5 40%, #fff 55%, #f5f5f5 63%);
-  list-style: none;
-  background-size: 400% 100%;
-  background-position: 100% 50%;
-  animation: skeleton-animation 2s ease infinite;
-}
-
-@keyframes skeleton-animation {
-  0% {
-    background-position: 100% 50%;
-  }
-
-  100% {
-    background-position: 0 50%;
-  }
-}
-</style>
diff --git a/vue/src/charts/package/AudioChart.vue b/vue/src/charts/package/AudioChart.vue
deleted file mode 100644
index 1c06bd16..00000000
--- a/vue/src/charts/package/AudioChart.vue
+++ /dev/null
@@ -1,389 +0,0 @@
-<template>
-  <!-- 图表标题 -->
-  <p class="text-center font-semibold pb-1.5">{{ title }}</p>
-  <!-- 如果图表数据错误 -->
-  <div class="flex flex-col justify-center grow text-dimmer gap-2" v-if="error">
-    <SLIcon class="mx-auto h-5 w-5" icon="error" />
-    <p class="text-center text-xs">
-      <!-- 在此处显示错误信息 -->
-      <span v-if="!isMulti">{{ $t('chart.charts.audio.error', { type: error['data_class'], tag: source[0] }) }}</span>
-      <span v-else>
-        {{ $t('chart.error') }}
-      </span>
-    </p>
-  </div>
-  <!-- 如果图表数据正确 -->
-  <template v-else>
-    <!-- 在此处完成图表主体定义 -->
-    <div class="h-[230px] overflow-y-auto px-2">
-      <AudioModule :audios="audioData" :key="currentIndex" v-if="audioData && !loading" />
-      <div class="flex flex-col justify-center items-center h-full" v-if="loading">
-        <SLLoading />
-      </div>
-    </div>
-    <!-- 滑块 -->
-    <div class="md:h-8 md:flex md:gap-10 items-center justify-center mt-2">
-      <SlideBar
-        :class="{ 'md:!justify-end': isMulti }"
-        v-model="currentIndex"
-        :max="maxIndex"
-        :min="minIndex"
-        :bar-color="barColor"
-        :key="slideKey"
-        @turn="handleTurn"
-        v-if="maxIndex !== minIndex"
-      />
-      <SlideBar
-        :class="{ 'md:!justify-start': maxIndex !== minIndex }"
-        :style="{ visibility: maxInnerIndex || 'hidden' }"
-        v-model="currentInnerIndex"
-        :max="maxInnerIndex"
-        :min="minInnerIndex"
-        :bar-color="barColor"
-        :key="maxInnerIndex"
-        reference="Index"
-        @turn="handleTurnIndex"
-        v-if="isMulti"
-      />
-    </div>
-    <!-- 放大效果弹窗 -->
-    <SLModal
-      class="p-10 pt-14 overflow-hidden h-[70vh] flex flex-col justify-between"
-      max-w="-1"
-      v-model="isZoom"
-      esc-exit
-    >
-      <p class="absolute top-4 w-full pr-20 text-center font-semibold">{{ title }}</p>
-      <div class="overflow-y-auto px-2 h-full">
-        <AudioModule :audios="audioData" :key="currentIndex" v-if="audioData && !loading" />
-        <div class="flex flex-col justify-center items-center h-full" v-if="loading">
-          <SLLoading />
-        </div>
-      </div>
-      <!-- 滑块 -->
-      <div class="md:h-8 md:flex md:gap-10 items-center justify-center mt-2">
-        <SlideBar
-          :class="{ 'md:!justify-end': isMulti }"
-          v-model="currentIndex"
-          :max="maxIndex"
-          :min="minIndex"
-          :bar-color="barColor"
-          :key="slideKey"
-          @turn="handleTurn"
-          v-if="maxIndex !== minIndex"
-          :turn-by-arrow="isZoom"
-        />
-        <SlideBar
-          :class="{ 'md:!justify-start': maxIndex !== minIndex }"
-          :style="{ visibility: maxInnerIndex || 'hidden' }"
-          v-model="currentInnerIndex"
-          :max="maxInnerIndex"
-          :min="minInnerIndex"
-          :bar-color="barColor"
-          :key="maxInnerIndex"
-          reference="Index"
-          @turn="handleTurnIndex"
-          v-if="isMulti"
-          :turn-by-arrow="isZoom"
-          vertical-arrow
-        /></div
-    ></SLModal>
-  </template>
-</template>
-
-<script setup>
-/**
- * @description:
- * @file: AudioChart.vue
- * @since: 2024-03-09 19:22:22
- **/
-import SLModal from '@swanlab-vue/components/SLModal.vue'
-import SLIcon from '@swanlab-vue/components/SLIcon.vue'
-import SlideBar from '../components/SlideBar.vue'
-import AudioModule from '../modules/AudioModule.vue'
-import { ref, inject } from 'vue'
-import { debounce } from '@swanlab-vue/utils/common'
-
-// ---------------------------------- 配置 ----------------------------------
-
-const props = defineProps({
-  title: {
-    type: String,
-    required: true
-  },
-  chart: {
-    type: Object,
-    required: true
-  },
-  index: {
-    type: Number,
-    required: true
-  }
-})
-const media = inject('media')
-/**
- * 图表数据来源，为数组
- * 当isMulti为true时，数组元素为exp，即给该表提供数据的实验的名称
- * 当isMulti为false时，数组元素为tag，即给该表提供数据的tag
- */
-const sources = computed(() => {
-  return props.chart?.source || []
-})
-
-// 是否为多实验的图表，根据路由名称判断
-const isMulti = computed(() => {
-  return props.chart.multi
-})
-
-/**
- * 展示用的音频数据
- */
-const audioData = computed(() => {
-  const stepData = stepsData.value[currentIndex.value]
-  if (!stepData) return []
-  // 如果是单源
-  if (!isMulti.value) {
-    return stepData[sources.value[0]].map(({ filename, caption }) => {
-      return {
-        audioBuffer: audiosData[filename],
-        audioBlob: blobsData[filename],
-        title: filename,
-        caption
-      }
-    })
-  }
-  // 如果是多源
-  const data = []
-  for (const exp in stepData) {
-    const temp = stepData[exp][currentInnerIndex.value]
-    data.push({
-      audioBuffer: audiosData[temp.filename],
-      audioBlob: blobsData[temp.filename],
-      title: temp.filename,
-      caption: temp.caption,
-      color: getColor(exp, undefined, props.chart.id),
-      exp
-    })
-  }
-  return data
-})
-
-// ---------------------------------- 错误处理，如果chart.error存在，则下面的api都将不应该被执行 ----------------------------------
-
-const error = computed(() => {
-  if (!props.chart.error) return false
-  if (!isMulti.value) return props.chart.error[sources.value[0]]
-  // 如果是多实验，检查每个实验的error
-  for (const exp of sources.value) {
-    // 如果有错误，直接返回
-    if (props.chart.error[exp]) return true
-  }
-  return false
-})
-
-// ---------------------------------- 图表颜色配置 ----------------------------------
-
-// 后续需要适配不同的颜色，但是Line不支持css变量，考虑自定义主题或者js获取css变量完成计算
-const defaultColor = inject('defaultColor')
-const getColor = inject('getColor')
-
-// ---------------------------------- step 滑块 ----------------------------------
-
-// 已经滑动部分颜色，应该通过色盘计算得到
-const barColor = defaultColor
-// 当前滑块索引
-const __currentIndex = ref(0)
-// 最小索引
-const minIndex = ref(NaN)
-// 最大索引
-const maxIndex = ref(NaN)
-// slide的key
-const slideKey = ref(0)
-watch([maxIndex, minIndex], ([max, min]) => {
-  slideKey.value = max + '-' + min
-})
-const loading = ref(true)
-
-// 计算属性，拦截滑块索引变化
-const currentIndex = computed({
-  get: () => __currentIndex.value,
-  set: (val) => {
-    // 如果当前值存在于stepsData的key中，则直接赋值
-    if (val in stepsData.value) {
-      if (val === __currentIndex.value && loading.value === false) return
-      __currentIndex.value = val
-    } else {
-      // 寻找一个最接近的值
-      const keys = Array.from(Object.keys(stepsData.value))
-      const index = keys.findIndex((item) => item > val)
-      const num = Number(index === -1 ? keys[keys.length - 1] : keys[index])
-      if (num === __currentIndex.value) return
-      __currentIndex.value = num
-    }
-    loading.value = true
-    // 如果是多实验模式，将内部数据索引置 0
-    if (isMulti.value) currentInnerIndex.value = 0
-    // 获取数据
-    debounceGetAudiosData(stepsData.value[__currentIndex.value])
-  }
-})
-
-/**
- * step 滑块，点击上下按钮翻页
- */
-const handleTurn = (direction, value) => {
-  const keys = Array.from(Object.keys(stepsData.value))
-  const index = keys.findIndex((item) => item > value)
-  if (direction === 'forward') {
-    currentIndex.value = Number(index === -1 ? keys[keys.length - 1] : keys[index])
-  } else {
-    // 向下获取
-    if (index === -1) currentIndex.value = Number(keys[Math.max(0, keys.length - 2)])
-    else currentIndex.value = Number(keys[Math.max(0, index - 2)])
-  }
-}
-
-// ---------------------------------- 数据格式化 ----------------------------------
-
-// 关联 step 与数据
-const stepsData = ref({})
-// blob 转 audioBuffer 后的缓存
-const audiosData = reactive({})
-// blob缓存
-const blobsData = {}
-
-/**
- * 解析 log 数据
- */
-const changeData2Audio = (data) => {
-  let _maxIndex = 0
-  let _minIndex = Infinity
-  for (const source in data) {
-    if (data[source] === null) continue
-    // 获取最大和最小索引
-    _maxIndex = Math.max(Number(data[source].list[data[source].list.length - 1].index), _maxIndex)
-    _minIndex = Math.min(Number(data[source].list[0].index), _minIndex)
-    const experiment_id = props.chart.source_map[source]
-    for (const item of data[source].list) {
-      // 如果不存在当前 step
-      if (!stepsData.value[item.index]) stepsData.value[item.index] = {}
-      // 如果当前 step 下已经有该实验的信息，说明这是之前的数据，重复了，不需要额外添加
-      if (stepsData.value[item.index][source]) continue
-      // 对当前实验下的数据进行处理,向stepsData中添加数据, 初始化 step 下的实验存储
-      stepsData.value[item.index][source] = []
-      // 添加数据,如果data是字符串，则直接添加，如果是数组，则遍历添加
-      if (typeof item.data === 'string') {
-        stepsData.value[item.index][source].push({ filename: item.data, caption: item.more?.caption, experiment_id })
-      } else {
-        for (let i = 0; i < item.data.length; i++) {
-          stepsData.value[item.index][source].push({
-            filename: item.data[i],
-            caption: item.more[i]?.caption,
-            experiment_id
-          })
-        }
-      }
-    }
-  }
-  maxIndex.value = _maxIndex
-  minIndex.value = _minIndex
-}
-
-// ---------------------------------- 数据请求和解析 ----------------------------------
-
-const debounceGetAudiosData = debounce(async (stepData) => {
-  loading.value = true
-
-  const promises = []
-  // 遍历该 step 下的数据源
-  for (const source in stepData) {
-    // 如果数据源中没有东西，跳过
-    if (stepData[source] === null) continue
-    // 遍历该数据源下的数据
-    for (const { filename, experiment_id } of stepData[source]) {
-      if (audiosData[filename]) continue
-      // 没有缓存，需要请求
-      promises.push(
-        new Promise((resolve) => {
-          // const runid = isMulti.value ? run_id.value[source] : run_id.value
-          media.get(filename, experiment_id, props.title).then((blob) => {
-            blobsData[filename] = blob
-            resolve(transformBlob(blob, filename))
-          })
-        })
-      )
-    }
-  }
-
-  await Promise.all(promises)
-  loading.value = false
-}, 500)
-
-/**
- * 将 blob 转成 AudioBuffer
- */
-const transformBlob = async (blob, filename) => {
-  const arrayBuffer = await blob.arrayBuffer()
-  const audioContext = new (window.AudioContext || window.webkitAudioContext)()
-  return new Promise((resolve, reject) => {
-    audioContext.decodeAudioData(
-      arrayBuffer,
-      (audioBuffer) => {
-        audiosData[filename] = audioBuffer
-        resolve(audioBuffer)
-      },
-      (error) => {
-        error.value = error
-        reject(error)
-      }
-    )
-  })
-}
-
-// ---------------------------------- 多实验 ----------------------------------
-
-// index 进度条配置
-const maxInnerIndex = computed(() => {
-  let tempLength = 0
-  for (const exp in stepsData.value[currentIndex.value]) {
-    const l = stepsData.value[currentIndex.value][exp].length - 1
-    if (l > tempLength) tempLength = l
-  }
-  return tempLength
-})
-const minInnerIndex = ref(0)
-const currentInnerIndex = ref(minInnerIndex.value)
-
-const handleTurnIndex = (direction, value) => {
-  currentInnerIndex.value = direction === 'forward' ? value + 1 : value - 1
-}
-
-// ---------------------------------- 渲染、重渲染功能 ----------------------------------
-
-// 渲染
-const render = (data) => {
-  changeData2Audio(data)
-  currentIndex.value = maxIndex.value
-}
-// 重渲染
-const change = (data) => {
-  changeData2Audio(data)
-}
-
-// ---------------------------------- 放大功能 ----------------------------------
-// 是否放大
-const isZoom = ref(false)
-// 放大数据
-const zoom = () => {
-  isZoom.value = true
-}
-
-// ---------------------------------- 暴露api ----------------------------------
-defineExpose({
-  render,
-  change,
-  zoom
-})
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/charts/package/ImageChart.vue b/vue/src/charts/package/ImageChart.vue
deleted file mode 100644
index d6786f66..00000000
--- a/vue/src/charts/package/ImageChart.vue
+++ /dev/null
@@ -1,601 +0,0 @@
-<template>
-  <!-- 图表标题 -->
-  <p class="text-center font-semibold">{{ title }}</p>
-  <!-- 如果图表数据错误 -->
-  <div class="flex flex-col justify-center grow text-dimmer gap-2" v-if="error">
-    <SLIcon class="mx-auto h-5 w-5" icon="error" />
-    <p class="text-center text-xs">
-      <!-- 在此处显示错误信息 -->
-      <span v-if="!isMulti">{{ $t('chart.charts.image.error', { type: error['data_class'], tag: source[0] }) }}</span>
-      <span v-else>{{ $t('chart.error') }}</span>
-    </p>
-  </div>
-  <!-- 如果图表数据正确 -->
-  <template v-else>
-    <!-- 在此处完成图表主体定义 -->
-    <div class="image-content image-content-no-zoom" ref="imageContentRef">
-      <div class="flex flex-col justify-center items-center h-full" v-if="loading">
-        <SLLoading />
-      </div>
-      <!-- 加载完成 -->
-      <!-- 单实验图表 -->
-      <div
-        class="images-container"
-        :style="setGrid(stepsData[currentIndex][source[0]].length)"
-        v-if="!loading && !isMulti"
-      >
-        <ImageModule
-          v-for="(s, index) in stepsData[currentIndex][source[0]]"
-          :key="index"
-          :filename="s.filename"
-          :imagesData="imagesData[s.filename].url"
-          :caption="s.caption"
-          :index="index"
-          @zoom="handleClickZoom"
-          @download="download"
-        />
-      </div>
-      <!-- 多实验图表 -->
-      <div v-if="!loading && isMulti" class="images-container" :style="setGrid(visiableSources.length)">
-        <!-- 多实验图表 -->
-        <ImageModule
-          v-for="(s, name) in stepsData[currentIndex]"
-          :key="name"
-          :filename="s[currentInnerIndex].filename"
-          :imagesData="imagesData[s[currentInnerIndex].filename].url"
-          :caption="s[currentInnerIndex].caption"
-          :index="name"
-          :name="name"
-          :color="getColor(name, undefined, chart.id)"
-          multi
-          @zoom="handleClickZoom"
-          @download="download"
-        />
-      </div>
-    </div>
-    <div class="md:h-8 md:flex md:gap-10 items-center justify-center mt-2">
-      <SlideBar
-        :class="{ 'md:!justify-end': isMulti }"
-        v-model="currentIndex"
-        :max="maxIndex"
-        :min="minIndex"
-        :bar-color="barColor"
-        :key="slideKey"
-        @turn="handleTurn"
-        v-if="maxIndex !== minIndex"
-      />
-      <SlideBar
-        :class="{ 'md:!justify-start': maxIndex !== minIndex }"
-        :style="{ visibility: maxInnerIndex || 'hidden' }"
-        v-model="currentInnerIndex"
-        :max="maxInnerIndex"
-        :min="minInnerIndex"
-        :bar-color="barColor"
-        :key="maxInnerIndex"
-        reference="Index"
-        @turn="handleTurnIndex"
-        v-if="isMulti"
-      />
-    </div>
-    <!-- 放大效果弹窗 -->
-    <SLModal class="p-10 pt-0 overflow-hidden" max-w="-1" v-model="isZoom" @onExit="exitByEsc">
-      <p class="text-center mt-4 mb-10 text-2xl font-semibold">{{ title }}</p>
-      <div class="image-content image-content-zoom">
-        <div class="flex flex-col justify-center items-center h-full" v-if="loading">
-          <SLLoading />
-        </div>
-        <!-- 加载完成 -->
-        <!-- 单实验图表 -->
-        <div
-          class="images-container"
-          :style="setGrid(stepsData[currentIndex][source[0]].length)"
-          v-if="!loading && !isMulti"
-        >
-          <ImageModule
-            v-for="(s, index) in stepsData[currentIndex][source[0]]"
-            :key="index"
-            :filename="s.filename"
-            :imagesData="imagesData[s.filename].url"
-            :caption="s.caption"
-            :index="index"
-            @zoom="handleClickZoom"
-            @download="download"
-          />
-        </div>
-        <!-- 多实验图表 -->
-        <div v-if="!loading && isMulti" class="images-container" :style="setGrid(visiableSources.length)">
-          <!-- 多实验图表 -->
-          <ImageModule
-            v-for="(s, name) in stepsData[currentIndex]"
-            :key="name"
-            :filename="s[currentInnerIndex].filename"
-            :imagesData="imagesData[s[currentInnerIndex].filename].url"
-            :caption="s[currentInnerIndex].caption"
-            :index="name"
-            :name="name"
-            :color="getColor(name, undefined, chart.id)"
-            multi
-            @zoom="handleClickZoom"
-            @download="download"
-          />
-        </div>
-      </div>
-      <div class="md:h-8 md:flex md:gap-10 items-center justify-center mt-2">
-        <SlideBar
-          :class="{ 'md:!justify-end': isMulti }"
-          v-model="currentIndex"
-          :max="maxIndex"
-          :min="minIndex"
-          :bar-color="barColor"
-          :key="slideKey"
-          @turn="handleTurn"
-          v-if="maxIndex !== minIndex"
-          :turn-by-arrow="isZoom && !isSingleZoom"
-        />
-        <SlideBar
-          :class="{ 'md:!justify-start': maxIndex !== minIndex }"
-          :style="{ visibility: maxInnerIndex || 'hidden' }"
-          v-model="currentInnerIndex"
-          :max="maxInnerIndex"
-          :min="minInnerIndex"
-          :bar-color="barColor"
-          :key="maxInnerIndex"
-          reference="index"
-          @turn="handleTurnIndex"
-          v-if="isMulti"
-          :turn-by-arrow="isZoom && !isSingleZoom && isMulti"
-          vertical-arrow
-        />
-      </div>
-    </SLModal>
-    <!-- 额外的放大功能，点击某个图像，放大显示 -->
-    <SLModal
-      class="w-full flex justify-center min-h-[calc(100vh-8rem)] p-14 relative"
-      max-w="-1"
-      v-model="isSingleZoom"
-      close-on-overlay-click
-      @onExit="exitByEsc"
-    >
-      <!-- 标题 -->
-      <p class="w-full overflow-hidden text-center text-lg font-semibold absolute top-5">
-        <span v-if="isMulti">{{ visiableSources[currentSingleImageIndex] }} / </span>
-        {{ signleZoomFilename }}
-      </p>
-      <!-- 图片 -->
-      <div class="image-single-zoom">
-        <!-- 上一张图片 -->
-        <SLIcon icon="down" class="icon rotate-90" @click="handleSingleChange({ key: 'ArrowLeft' })"></SLIcon>
-        <!-- 当前图片 -->
-        <div class="relative mx-5 select-none">
-          <img :src="imagesData[signleZoomFilename].url" class="w-full" />
-          <DownloadButton class="image-download-button" @click.stop="download(signleZoomFilename)" />
-        </div>
-        <!-- 下一张图片 -->
-        <SLIcon icon="down" class="icon -rotate-90" @click="handleSingleChange({ key: 'ArrowRight' })"></SLIcon>
-      </div>
-      <p class="w-full text-center absolute bottom-5 select-none">
-        {{
-          `${currentSingleImageIndex + 1} / ${
-            isMulti ? visiableSources.length : stepsData[currentIndex][source[0]].length
-          }`
-        }}
-      </p>
-    </SLModal>
-  </template>
-</template>
-
-<script setup>
-/**
- * @description: 图像图表组件
- * @file: ImageChart.vue
- * @since: 2024-02-03 13:24:57
- **/
-import SLModal from '@swanlab-vue/components/SLModal.vue'
-import SLIcon from '@swanlab-vue/components/SLIcon.vue'
-import { ref, inject, watch, computed } from 'vue'
-import SlideBar from '../components/SlideBar.vue'
-import { debounce } from '@swanlab-vue/utils/common'
-import DownloadButton from '../components/DownloadButton.vue'
-import ImageModule from '../modules/ImageModule.vue'
-
-// ---------------------------------- 配置 ----------------------------------
-
-const props = defineProps({
-  title: {
-    type: String,
-    required: true
-  },
-  chart: {
-    type: Object,
-    required: true
-  },
-  index: {
-    type: Number,
-    required: true
-  }
-})
-const media = inject('media')
-/**
- * 图表数据来源，为数组
- * 当isMulti为true时，数组元素为exp，即给该表提供数据的实验的名称
- * 当isMulti为false时，数组元素为tag，即给该表提供数据的tag
- */
-const source = computed(() => {
-  return props.chart.source
-})
-// 是否为多实验的图表，根据路由名称判断
-const isMulti = computed(() => {
-  return props.chart.multi
-})
-
-// ---------------------------------- 错误处理，如果chart.error存在，则下面的api都将不应该被执行 ----------------------------------
-
-const error = computed(() => {
-  if (!props.chart.error) return false
-  if (!isMulti.value) return props.chart.error[source.value[0]]
-  // 如果是多实验，检查每个实验的error
-  for (const exp of source.value) {
-    // 如果有错误，直接返回
-    if (props.chart.error[exp]) return true
-  }
-  return false
-})
-
-// ---------------------------------- 图表颜色配置 ----------------------------------
-// 后续需要适配不同的颜色，但是Line不支持css变量，考虑自定义主题或者js获取css变量完成计算
-const getColor = inject('getColor')
-const defaultColor = inject('defaultColor')
-// ---------------------------------- 组件渲染逻辑 ----------------------------------
-// 已经滑动部分颜色，应该通过色盘计算得到
-const barColor = defaultColor
-// 当前滑块索引
-const __currentIndex = ref(0)
-// 最小索引
-const minIndex = ref(undefined)
-// 最大索引
-const maxIndex = ref(undefined)
-// slide的key
-const slideKey = ref(0)
-watch([maxIndex, minIndex], ([max, min]) => {
-  slideKey.value = max + '-' + min
-})
-const loading = ref(true)
-const imageContentRef = ref(null)
-
-// 计算属性，拦截滑块索引变化
-const currentIndex = computed({
-  get: () => __currentIndex.value,
-  set: (val) => {
-    // 如果当前值存在于stepsData的key中，则直接赋值
-    if (val in stepsData.value) {
-      if (val === __currentIndex.value && loading.value === false) return
-      __currentIndex.value = val
-    } else {
-      // 寻找一个最接近的值
-      const keys = Array.from(Object.keys(stepsData.value))
-      const index = keys.findIndex((item) => item > val)
-      const num = Number(index === -1 ? keys[keys.length - 1] : keys[index])
-      if (num === __currentIndex.value) return
-      __currentIndex.value = num
-    }
-    loading.value = true
-    imageContentRef.value.height = imageContentRef.value.offsetHeight + 'px'
-    // 如果是多实验模式，将内部数据索引置 0
-    if (isMulti.value) currentInnerIndex.value = 0
-    // 获取数据
-    debounceGetImagesData(stepsData.value[__currentIndex.value])
-  }
-})
-
-// 事件处理，触发slideBar的turn事件
-const handleTurn = (direction, value) => {
-  const keys = Array.from(Object.keys(stepsData.value))
-  const index = keys.findIndex((item) => item > value)
-  if (direction === 'forward') {
-    currentIndex.value = Number(index === -1 ? keys[keys.length - 1] : keys[index])
-  } else {
-    // 向下获取
-    if (index === -1) currentIndex.value = Number(keys[Math.max(0, keys.length - 2)])
-    else currentIndex.value = Number(keys[Math.max(0, index - 2)])
-  }
-}
-
-// 在多实验图表时完成 index 的翻页
-const handleTurnIndex = (direction, value) => {
-  currentInnerIndex.value = direction === 'forward' ? value + 1 : value - 1
-}
-
-// 布局处理,一共length列，最多显示8列
-const setGrid = (length) => {
-  if (length <= 8) {
-    return 'grid-template-columns:' + 'repeat(' + length + ', minmax(0, 1fr))'
-  } else {
-    return 'grid-template-columns:' + 'repeat(8, minmax(0, 1fr))'
-  }
-}
-
-// ---------------------------------- 多实验适配 ----------------------------------
-
-// index 进度条配置
-const maxInnerIndex = computed(() => {
-  let tempLength = 0
-  for (const exp in stepsData.value[currentIndex.value]) {
-    const l = stepsData.value[currentIndex.value][exp].length - 1
-    if (l > tempLength) tempLength = l
-  }
-  return tempLength
-})
-const minInnerIndex = ref(0)
-const currentInnerIndex = ref(minInnerIndex.value)
-
-// 同时获取多个实验的图像数据
-const getMultiImagesData = async (stepData) => {
-  const promises = []
-  for (const exp in stepData) {
-    for (const { filename, experiment_id } of stepData[exp]) {
-      if (imagesData[filename]) continue
-      // 没有缓存，发起请求
-      promises.push(
-        new Promise((resolve) => {
-          media.get(filename, experiment_id, props.title).then((blob) => resolve(transformBlob(blob, filename)))
-        })
-      )
-    }
-  }
-
-  await Promise.all(promises)
-}
-
-/**
- * 多实验图表时，当前 step 中含有的实验名数组
- */
-const visiableSources = computed(() => {
-  const temp = []
-  for (const key in stepsData.value[currentIndex.value]) {
-    temp.push(key)
-  }
-  return temp
-})
-
-// ---------------------------------- 数据格式化 ----------------------------------
-// 前端映射数据，不包含图像，数据格式：{step: {tag: [{filename: string, caption: string}]}}
-const stepsData = ref({})
-// 图像数据缓存, 数据格式：{String<filename>: {blob: Blob, url: string<base64>}}
-const imagesData = {}
-
-/**
- * 获取图像数据
- * step 发生改变后触发获取
- */
-const getImagesData = async (stepData) => {
-  if (!isMulti.value) await getSingleImageData(stepData)
-  else await getMultiImagesData(stepData)
-  loading.value = false
-  imageContentRef.value.height = ''
-}
-
-/**
- * 单数据源
- */
-const getSingleImageData = async (stepData) => {
-  const tag = source.value[0]
-  const promises = []
-  for (const { filename, experiment_id } of stepData[tag]) {
-    if (!imagesData[filename]) {
-      promises.push(
-        new Promise((resolve) => {
-          media.get(filename, experiment_id, tag).then((blob) => resolve(transformBlob(blob, filename)))
-        })
-      )
-    }
-  }
-  await Promise.all(promises)
-}
-
-/**
- * 将 blob 转成图片
- */
-const transformBlob = (blob, filename) => {
-  return new Promise((resolve) => {
-    // blob转换为图像base64
-    const reader = new FileReader()
-    reader.onload = function () {
-      imagesData[filename] = { blob, url: reader.result }
-      resolve()
-    }
-    reader.readAsDataURL(blob)
-  })
-}
-
-/**
- * 防抖函数，防止请求过于频繁
- */
-const debounceGetImagesData = debounce(getImagesData, 500)
-
-/**
- * 将后端传回的数据转换为图像数据，存储到imageStepsData中
- * 后端返回的数据格式为：{exp_name: {..., list:[{data: string or list<strinf>, more: object or list<object>, index: string<number>, create_time: string}]}}
- * 主要工作：
- * 1. 将有用数据按 step 提取到 stepsData
- * 2. 找出最大最小 step
- *
- * @param { Object } data 后端返回的数据
- */
-const changeData2Image = (data) => {
-  // 遍历数据，将数据转换为图像数据
-  let _maxIndex = 0
-  let _minIndex = Infinity
-  for (const source in data) {
-    if (data[source] === null) continue
-    // 遍历实验下的数据
-    _maxIndex = Math.max(Number(data[source].list[data[source].list.length - 1].index), _maxIndex)
-    _minIndex = Math.min(Number(data[source].list[0].index), _minIndex)
-    const experiment_id = props.chart.source_map[source]
-    for (const item of data[source].list) {
-      // 如果不存在当前 step
-      if (!stepsData.value[item.index]) stepsData.value[item.index] = {}
-      // 对当前实验下的数据进行处理,向stepsData中添加数据, 初始化 step 下的实验存储
-      if (stepsData.value[item.index][source]) continue
-      stepsData.value[item.index][source] = []
-      // 添加数据,如果data是字符串，则直接添加，如果是数组，则遍历添加
-      if (typeof item.data === 'string') {
-        stepsData.value[item.index][source].push({ filename: item.data, caption: item.more?.caption, experiment_id })
-      } else {
-        for (let i = 0; i < item.data.length; i++) {
-          stepsData.value[item.index][source].push({
-            filename: item.data[i],
-            caption: item.more[i]?.caption,
-            experiment_id
-          })
-        }
-      }
-    }
-  }
-  maxIndex.value = _maxIndex
-  minIndex.value = _minIndex
-}
-
-// ---------------------------------- 渲染、重渲染功能 ----------------------------------
-
-// 渲染
-const render = (data) => {
-  // 数据格式化
-  changeData2Image(data)
-  // console.log('图像数据：', stepsData)
-  currentIndex.value = maxIndex.value
-}
-// 重渲染
-const change = (data) => {
-  // 数据格式化
-  changeData2Image(data)
-}
-
-// ---------------------------------- 放大功能 ----------------------------------
-// 是否放大
-const isZoom = ref(false)
-// 放大数据
-const zoom = () => {
-  isZoom.value = true
-}
-
-// ---------------------------------- 点击某个图像，放大 ----------------------------------
-
-const isSingleZoom = ref(false)
-const signleZoomFilename = ref()
-// 当前单个图像的索引
-const currentSingleImageIndex = ref(0)
-// 点击某个图像，放大
-const handleClickZoom = (filename, index) => {
-  signleZoomFilename.value = filename
-  isSingleZoom.value = true
-  currentSingleImageIndex.value = isMulti.value ? visiableSources.value.indexOf(index) : index
-}
-
-// ---------------------------------- ESC 退出弹窗 ----------------------------------
-
-// 通过 esc 按键关闭弹窗
-const exitByEsc = () => {
-  // 如果两个弹窗都有，只关闭单图弹窗
-  if (isZoom.value && isSingleZoom.value) return (isSingleZoom.value = false)
-  // 剩下的情况都是只有一个弹窗，全部关闭
-  isZoom.value = false
-  isSingleZoom.value = false
-}
-
-// ---------------------------------- 左右方向键翻页 ----------------------------------
-
-// 方向键切换单图 - 回调
-const handleSingleChange = ({ key }) => {
-  const isSingle = !isMulti.value
-  const images = isSingle ? stepsData.value[currentIndex.value][source.value[0]] : stepsData.value[currentIndex.value]
-
-  if (
-    (key === 'ArrowRight' &&
-      currentSingleImageIndex.value < (isSingle ? images.length - 1 : visiableSources.value.length - 1)) ||
-    (key === 'ArrowLeft' && currentSingleImageIndex.value > 0)
-  ) {
-    currentSingleImageIndex.value += key === 'ArrowRight' ? 1 : -1
-  }
-
-  const filename = isSingle
-    ? images[currentSingleImageIndex.value].filename
-    : images[visiableSources.value[currentSingleImageIndex.value]][currentInnerIndex.value].filename
-
-  signleZoomFilename.value = filename
-}
-
-// 订阅通过左右方向键切换单图
-watch(
-  () => isSingleZoom.value,
-  (newVal) => {
-    if (newVal) {
-      window.addEventListener('keyup', handleSingleChange)
-    } else {
-      window.removeEventListener('keyup', handleSingleChange)
-    }
-  }
-)
-
-// ---------------------------------- 点击下载按钮下载 ----------------------------------
-
-const download = (filename) => {
-  // 拿到blob数据
-  const blob = imagesData[filename].blob
-  // 创建a标签
-  const a = document.createElement('a')
-  // 创建url
-  const url = window.URL.createObjectURL(blob)
-  // 设置a标签
-  a.href = url
-  a.download = filename
-  // 模拟点击
-  a.click()
-}
-
-// ---------------------------------- 暴露api ----------------------------------
-defineExpose({
-  render,
-  change,
-  zoom
-})
-</script>
-
-<style lang="scss" scoped>
-.image-content {
-  @apply mt-1 p-2 w-full rounded-sm relative min-h-[224px];
-  .images-container {
-    @apply grid gap-3 h-full;
-  }
-}
-.image-content-no-zoom {
-  @apply h-56 overflow-y-auto overflow-x-clip;
-}
-
-.image-content-zoom {
-  @apply h-[calc(100vh-19rem)] overflow-y-auto overflow-x-clip;
-}
-
-.image-single-zoom {
-  @apply flex items-center justify-between w-full;
-  &:hover .image-download-button {
-    @apply block;
-  }
-
-  .icon {
-    @apply w-10 h-10 cursor-pointer border rounded-full opacity-20 transition-all;
-
-    &:hover {
-      @apply opacity-100;
-    }
-  }
-}
-</style>
-
-<style lang="scss">
-.image-download-button {
-  @apply opacity-75 absolute top-1 right-1 hidden;
-  width: 1.25rem !important;
-  height: 1.25rem !important;
-  padding: 0.125rem !important;
-}
-</style>
diff --git a/vue/src/charts/package/LineChart.vue b/vue/src/charts/package/LineChart.vue
deleted file mode 100644
index 526eae95..00000000
--- a/vue/src/charts/package/LineChart.vue
+++ /dev/null
@@ -1,731 +0,0 @@
-<template>
-  <div class="flex flex-col justify-between grow" ref="lineChartRef">
-    <!-- 图表标题 -->
-    <p class="text-center font-semibold select-none">{{ title }}</p>
-    <div class="flex flex-col justify-center grow text-dimmer gap-2" v-if="error">
-      <SLIcon class="mx-auto h-5 w-5" icon="error" />
-      <p class="text-center text-sm">
-        {{ $t('chart.charts.line.error', { type: error['data_class'], tag: source[0] }) }}
-      </p>
-    </div>
-    <template v-else>
-      <!-- 图表主体 -->
-      <LineChartLegend
-        :items="legend"
-        :key="legend.length"
-        @hoverin="(item) => legendHoverin(item, false)"
-        @hoverout="(item) => legendHoverout(item, false)"
-        v-if="legend && multi"
-      />
-      <div class="relative">
-        <!-- x轴坐标单位 -->
-        <p class="absolute right-2 bottom-6 text-xs text-dimmer scale-90 select-none">{{ xTitle }}</p>
-        <LineChartTooltip ref="tooltipRef" />
-        <div class="overflow-hidden" ref="g2Ref"></div>
-      </div>
-      <!-- 放大效果 -->
-      <SLModal class="p-10 pt-0 overflow-hidden" max-w="-1" v-model="isZoom" esc-exit>
-        <p class="text-center mt-4 mb-10 text-2xl font-semibold">{{ title }}</p>
-        <LineChartLegend
-          :items="legend"
-          @hoverin="(item) => legendHoverin(item, true)"
-          @hoverout="(item) => legendHoverout(item, true)"
-          v-if="legend && multi"
-        />
-        <div class="relative" ref="g2ZoomRef">
-          <p class="absolute right-2 bottom-6 text-xs text-dimmer scale-90 select-none">{{ xTitle }}</p>
-          <LineChartTooltip ref="tooltipZoomRef" />
-        </div>
-      </SLModal>
-    </template>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 折线图表
- * @file: LineChart.vue
- * @since: 2023-12-25 20:17:19
- **/
-import SLModal from '@swanlab-vue/components/SLModal.vue'
-import SLIcon from '@swanlab-vue/components/SLIcon.vue'
-import { Line, G2 } from '@antv/g2plot'
-import * as UTILS from './utils'
-import { ref, inject, computed, onUnmounted, provide } from 'vue'
-import { addTaskToBrowserMainThread, copyTextToClipboard } from '@swanlab-vue/utils/browser'
-import { formatNumber2SN } from '@swanlab-vue/utils/common'
-import { t } from '@swanlab-vue/i18n'
-import { getTimes } from '@swanlab-vue/utils/time'
-import { isApple } from '@swanlab-vue/utils/browser'
-import { message } from '@swanlab-vue/components/message'
-import makeSmooth from './smooth'
-import { needSmooth } from './smooth'
-import LineChartTooltip from '../components/LineChartTooltip.vue'
-import LineChartLegend from '../components/LineChartLegend.vue'
-import '@swanlab-vue/docs'
-
-// ---------------------------------- 配置 ----------------------------------
-const props = defineProps({
-  title: {
-    type: String,
-    required: true
-  },
-  chart: {
-    type: Object,
-    required: true
-  },
-  index: {
-    type: Number,
-    required: true
-  }
-})
-
-// ---------------------------------- 错误处理，如果chart.error存在，则下面的api都将不应该被执行 ----------------------------------
-// 数据源 array
-const source = props.chart.source
-// source的长度如果等于error的长度，说明所有数据都有问题,取第一个的error即可
-const error = ref(
-  props.chart.error && source.length === Object.keys(props.chart.error).length ? props.chart.error[source[0]] : null
-)
-// 图表模式，multi或者single
-const multi = props.chart.multi
-// 当前命名空间下除了自己的所有chartRef
-const chartRefList = inject('chartRefList')
-const chartRefListExceptSelf = computed(() => {
-  // 将列表中除了props.index的所有chartRef过滤出来
-  return chartRefList.value.filter((_, i) => i !== props.index)
-})
-// 平滑配置
-let smoothMethod = inject('smoothMethod')?.value || undefined
-// ---------------------------------- 图表样式配置 ----------------------------------
-// 后续需要适配不同的颜色，但是Line不支持css变量，考虑自定义主题或者js获取css变量完成计算
-const getColor = inject('getColor')
-const defaultColor = inject('defaultColor')
-const rootStyle = getComputedStyle(document.documentElement)
-// 边框颜色，通过js获取css变量值
-const borderColor = rootStyle.getPropertyValue('--outline-default')
-// 网格线颜色，通过js获取css变量值
-const gridColor = rootStyle.getPropertyValue('--outline-dimmest')
-// 十字准线颜色，通过js获取css变量值
-const crosshairsColor = rootStyle.getPropertyValue('--primary-dimmest')
-// 线段默认宽度
-const lineWidth = 1.5
-// 线段加粗宽度
-const thickerLineWidth = 3.5
-
-// ---------------------------------- 样式注册，数据点样式注册，如果是最后一个，会放大 ----------------------------------
-G2.registerShape('point', 'last-point', {
-  draw(cfg, container) {
-    const point = { x: cfg.x, y: cfg.y }
-    // console.log('point', cfg.data)
-    const shape = container.addShape('circle', {
-      name: 'point',
-      attrs: {
-        x: point.x,
-        y: point.y,
-        fill: cfg.color || 'red',
-        opacity: cfg?.data?._last ? 1 : 0,
-        r: 3
-      }
-    })
-    return shape
-  }
-})
-
-// ---------------------------------- 组件渲染逻辑 ----------------------------------
-// 组件对象
-const lineChartRef = ref()
-const g2Ref = ref()
-const g2ZoomRef = ref()
-const tooltipRef = ref()
-const tooltipZoomRef = ref()
-// 参考字段和显示名称
-const reference = props.chart.reference
-// 拿到参考系，未来图表可能有不同的x轴依据，比如step、time等，这里需要根据设置的reference来决定
-const { xField, xTitle } = UTILS.refrence2XField[reference]
-// 默认y轴的依据key是data
-const yField = 'data'
-const seriesField = 'series'
-const colorField = 'type'
-/**
- * 创建图表函数
- * @param { HTMLElement } dom 图表挂载的dom
- * @param { Array } data 图表数据
- * @param { Object } config 图表的一些其他配置
- * @param { bool } zoom 是否放大
- */
-const createChart = (dom, data, config = {}, zoom = false) => {
-  if (!dom) return
-  const c = new Line(dom, {
-    data,
-    // 默认的x轴依据key为step
-    xField,
-    // 默认的y轴依据key为data
-    yField,
-    // 多数据的时候，需要设置seriesField，单数据也可以设置，但是不希望出现label
-    // seriesField,
-    colorField,
-    // 自己写图例
-    legend: false,
-    // 多数据的时候颜色通过回调拿到
-    color: ({ series }) => {
-      // 是否有&smooth后缀
-      const smooth = series.includes('&smooth')
-      series = series.replace('&smooth', '')
-      // 如果没有&smooth后缀，直接返回颜色
-      const color = getColor(series, source.indexOf(series), props.chart.id)
-      if (!smooth) return color
-      else return UTILS.transparentColor(color)
-    },
-    point: {
-      shape: 'last-point'
-    },
-    lineStyle: {
-      lineWidth
-    },
-    // 坐标轴相关
-    xAxis: {
-      // 自定义坐标轴的刻度，暂时没有找到文档，通过源码来看是返回一个数组，数组内是字符串，代表刻度
-      tickCount: 5,
-      type: 'linear',
-      // 在此处完成X轴数据的格式化
-      label: {
-        formatter: (data) => {
-          // console.log('data', data)
-          // 如果是小数，返回空
-          if (data % 1 !== 0) return ''
-          // 如果是100的倍数且大于1000，返回k
-          if (data % 100 === 0 && data >= 1000) return `${data / 1000}k`
-          return data
-        }
-      },
-
-      // x轴坐标轴样式
-      line: {
-        style: {
-          stroke: borderColor,
-          lineWidth: 2
-        }
-      },
-      // x轴刻度样式
-      tickLine: {
-        length: 4,
-        style: {
-          stroke: borderColor,
-          lineWidth: 2
-        }
-      }
-    },
-    yAxis: {
-      min: null,
-      label: {
-        // 在此处完成Y轴数据的格式化
-        formatter: (data) => {
-          return formatNumber2SN(data)
-        }
-      },
-      // y轴坐标轴样式
-      line: {
-        style: {
-          stroke: borderColor,
-          lineWidth: 2
-        }
-      },
-      // y轴刻度样式
-      tickLine: {
-        length: 4,
-        style: {
-          stroke: borderColor,
-          lineWidth: 2
-        }
-      },
-      // 网格线
-      grid: {
-        line: {
-          style: {
-            stroke: gridColor
-          }
-        }
-      }
-    },
-    // 悬浮提示相关
-    tooltip: {
-      // 在此处完成悬浮数据提示的格式化
-      // 如果需要自定义浮窗，可以用下面的customContent
-      // formatter: (data) => {
-      //   // console.log(data)
-      //   // 如果data.series是undefined，说明是单数据,直接显示source[0]即可
-      //   const name = data.series ? data.series : source[0]
-      //   return { name, value: formatNumber2SN(data.data) }
-      // },
-      follow: true,
-      enterable: false,
-      shared: true,
-      position: 'top',
-      showMarkers: true,
-      customContent: () => '',
-      domStyles: {
-        // 自己写tooltip的样式
-        'g2-tooltip': {
-          boxShadow: 'none',
-          borderWidth: 'none',
-          borderRadius: 'none'
-        }
-      },
-      showCrosshairs: true,
-      crosshairs: {
-        line: {
-          style: {
-            stroke: crosshairsColor,
-            lineWidth: 2
-          }
-        }
-      }
-    },
-    // 大小相关
-    height: originalHeight,
-    width: undefined,
-    autoFit: true,
-    // 开启一些交互
-    // interactions: [{ type: 'element-active' }],
-    // 平滑曲线
-    smooth: false,
-    animation: false,
-    ...config
-  })
-  c.render()
-  addTaskToBrowserMainThread(() => {
-    registerTooltipEvent(dom, zoom)
-  })
-  // 监听鼠标移入事件
-  // c.on('plot:mouseenter', (evt) => {
-  // })
-  // 监听鼠标移出事件
-  c.on('plot:mouseleave', () => {
-    restoreByTag(c, zoom, nowThickenTag, nowThickenColor)
-  })
-  // 监听鼠标移动事件
-  c.on('plot:mousemove', (evt) => {
-    // console.log('plot:mousemove', evt)
-    if (!nowData) return
-    const y = evt.y
-    // 遍历所有的nowData，寻找其中与y绝对值最小的点
-    let index = undefined
-    let min = Infinity
-    for (let i = 0; i < nowData.length; i++) {
-      const data = nowData[i]
-      const d = Math.abs(data.y - y)
-      if (d < min && !nowData[i].data.series.includes('&smooth')) {
-        min = d
-        index = i
-      }
-    }
-    // console.log('鼠标移动', props.chart.name)
-    if (index !== undefined) thickenByTag(c, zoom, nowData[index].data.series, nowData[index].color)
-  })
-
-  return c
-}
-
-// ---------------------------------- 数据格式化 ----------------------------------
-/**
- * 为了将数据格式化为图表可用的格式，需要将数据源中的数据进行格式化
- * 遍历data的所有key，合并其中的list为一个数组
- * @param { OpenChartData } data 待格式化的数据
- * @returns { Object } 格式化后的数据, { d: [{}, {}, ...], config: {} } config是图表的一些其他配置
- */
-const format = (data) => {
-  // 如果配置了平滑方法，需要动态计算平滑后的数据
-  if (smoothMethod) {
-    const smoothData = {}
-    for (const key in data) {
-      if (!data[key]) continue
-      // data中出现了字符串
-      console.log('data[key]', data[key])
-      if (data[key].list.some((item) => typeof item.data === 'string')) {
-        smoothData[key] = data[key]
-        continue
-      }
-      smoothData[key] = {
-        ...data[key],
-        list: makeSmooth(data[key].list, smoothMethod)
-      }
-    }
-    // 将原本的data的key添加&smooth后，但是注意不要修改原本的data，应该新建一个对象
-    for (const key in data) {
-      smoothData[`${key}&smooth`] = data[key]
-    }
-    data = smoothData
-  }
-  // console.log('data', data)
-  // 如果source的长度小于1，抛出错误
-  // console.log(data)
-  if (source.length < 1) throw new Error('source length must be greater than 1')
-  // 新的数据,遍历得到
-  const d = []
-  Object.keys(data).forEach((key) => {
-    // 如果key存在于props.chart.error中，说明这个数据有问题，直接返回
-    if (props.chart.error && props.chart.error[key]) return
-    // 如果不是单数据，需要将所有数据的list合并为一个数组
-    if (!data[key]) return
-    data[key].list.forEach((item) => {
-      // item新加series字段，用于标识数据来源
-      d.push({ ...item, series: key })
-    })
-  })
-  // console.log('d', d)
-  // 依据xField排序，从小到大
-  d.sort((a, b) => a[xField] - b[xField])
-  // console.log('d', d)
-  // console.log('data', data)
-  // 依据source生成legend，排序依据是source，数据结构：[{name, color}]
-  const items = []
-  for (const s of source) {
-    if (props.chart.error && props.chart.error[s]) continue
-    if (!data[s]) continue
-    items.push({ name: s, color: getColor(s, source.indexOf(s), props.chart.id), experiment_id: data[s].experiment_id })
-  }
-  legend.value = items
-  // console.log('legend', legend.value)
-  return { d, config: multi || smoothMethod ? { seriesField } : { color: defaultColor } }
-}
-
-const formatTime = (time) => {
-  let { year, month, day, hour, minute } = getTimes(time)
-  // year只保留后两位
-  year = year.toString().slice(-2)
-  // 格式化minute，如果是个位数，前面加0
-  minute = minute.toString().length === 1 ? `0${minute}` : minute
-  let pm = false
-  if (hour > 12) {
-    hour -= 12
-    pm = true
-  }
-  let result = `${year}/${month}/${day} ${hour}:${minute}`
-  if (pm) result += ' PM'
-  else result += ' AM'
-  return result
-}
-
-provide('formatTime', formatTime)
-provide('formatNumber2SN', formatNumber2SN)
-// ---------------------------------- 渲染、重渲染功能 ----------------------------------
-let chartObj = null
-// 渲染
-const render = (data) => {
-  // console.log('渲染折线图')
-  // console.log('data', data)
-  const { d, config } = format(data)
-  // console.log('data', data)
-  chartObj && chartObj.destroy()
-  chartObj = createChart(g2Ref.value, d, config)
-  // console.log('chartObj', chartObj)
-  // 可以使用update api来更新配置
-}
-// 重渲染
-const change = (data) => {
-  const { d } = format(data)
-  chartObj.changeData(d)
-  // // change函数等于render函数
-  // chartObj.destroy()
-  // chartObj = createChart(g2Ref.value, d, { animation: false, ...config })
-}
-
-// ---------------------------------- 放大功能 ----------------------------------
-// 是否放大
-const isZoom = ref(false)
-let zoomChartObj = null
-// 放大数据
-const zoom = (data) => {
-  isZoom.value = true
-  // 当前window的高度
-  const { d, config } = format(data)
-  const height = window.innerHeight * 0.6
-  addTaskToBrowserMainThread(() => {
-    zoomChartObj = createChart(
-      g2ZoomRef.value,
-      d,
-      {
-        interactions: [{ type: 'brush-x' }],
-        height,
-        ...config
-      },
-      true
-    )
-  })
-}
-
-// ---------------------------------- 多linechart之间的tooltip联动 ----------------------------------
-
-let manual = true
-// 调用此方法则必然是自动触发
-const lineShowTooltip = (point) => {
-  if (error.value) return
-  manual = false
-  // console.log('lineShowTooltip', props.index)
-  // console.log('title', title)
-  chartObj?.chart?.showTooltip(point)
-}
-const lineHideTooltip = () => {
-  if (error.value) return
-  manual = false
-  chartObj?.chart?.hideTooltip()
-}
-let point = null
-const registerTooltipEvent = (dom, zoom) => {
-  // 给 tooltip 添加点击事件
-  if (error.value) return
-  // 代理实现tooltip的显示和隐藏
-  const chart = zoom ? zoomChartObj : chartObj
-  const tRef = zoom ? tooltipZoomRef : tooltipRef
-
-  chart.on('tooltip:show', (evt) => {
-    // console.log('tooltip:show', evt)
-    // 获取当前tooltip的left
-    const items = evt.data.items
-    addTaskToBrowserMainThread(() => {
-      const left = dom.querySelector('.g2-tooltip').style.left
-      tRef.value.show(items, dom.clientWidth, left)
-    })
-    // 非js触发的tooltip，不执行下面的逻辑
-    if (!manual) {
-      // console.log('auto show tooltip')
-      return
-    }
-    nowData = evt.data.items
-    // console.log('nowData', nowData)
-    point = { x: evt.data.x, y: evt.data.y }
-    // 通知其他图表，当前图表的数据被hover到了
-    !zoom &&
-      chartRefListExceptSelf.value?.forEach((chart) => {
-        chart?.showTooltip(point)
-      })
-    manual = true
-  })
-  chart.on('tooltip:hide', (...args) => {
-    // console.log('tooltip:hide', chart)
-    // 非js触发的tooltip，不执行下面的逻辑
-    tRef.value.hide()
-    if (manual && !zoom)
-      // 通知其他图表，当前图表的数据被hover到了
-      chartRefListExceptSelf.value?.forEach((chart) => {
-        chart?.hideTooltip(...args)
-      })
-    nowData = null
-    manual = true
-    point = null
-  })
-  // brush相关事件
-  chart.on(G2.BRUSH_FILTER_EVENTS.BEFORE_FILTER, (...agrs) => {
-    console.log('BEFORE_FILTER', agrs)
-  })
-}
-
-// ---------------------------------- tooltip出现并且是非js触发时，可执行copy操作 ----------------------------------
-// 当前tooltip的数据,用于copy
-let nowData = null
-// 全局注册keydown事件，当mac端触发command+c，windows端触发ctrl+c时，且nowData不为null，执行copy操作
-const handleCopy = (e) => {
-  if (error.value) return
-  if (nowData === null) {
-    return
-    // return console.log('非js触发的tooltip，或者未悬浮，不执行copy操作')
-  }
-  if (e.key === 'c' && (isApple ? e.metaKey : e.ctrlKey)) {
-    e.preventDefault()
-    // console.log('copy:', nowData)
-    // nowData依据data降序
-    nowData.sort((a, b) => b.data.data - a.data.data)
-    // 生成copy的内容，zoom和非zoom样式不一样
-    let content = ''
-    if (!isZoom.value) {
-      // 复制数据和tag：tag data
-      for (const d of nowData) {
-        content += `${d.data.series} ${formatNumber2SN(d.data.data)}\n`
-      }
-    } else {
-      // 复制数据、时间和tag：tag data time
-      for (const d of nowData) {
-        content += `${d.data.series} ${formatNumber2SN(d.data.data)} ${formatTime(d.data.create_time)}\n`
-      }
-    }
-    copyTextToClipboard(content, () => message.success(t('chart.charts.line.copy.success')))
-  }
-}
-window.addEventListener('keydown', handleCopy)
-onUnmounted(() => {
-  window.removeEventListener('keydown', handleCopy)
-})
-
-// ---------------------------------- 控制线段加粗 ----------------------------------
-/**
- * 当前加粗的tag,需要注意的是当前加粗的tag可能不存在于当前图表的数据中
- * multi为true时，按照tag加粗，否则按照color加粗
- */
-let nowThickenTag = null
-/**
- * 当前加粗的颜色，需要注意的是当前加粗的颜色可能不存在于当前图表的数据中
- * mutlti为false时，按照color加粗，否则按照tag加粗
- */
-let nowThickenColor = null
-/**
- * 加粗指定tag的线段
- * @param { Object } plot plot对象，chartObj或者zoomChartObj
- * @param { bool } zoom 是否放大
- * @param { string } tag 实验标签
- */
-const thickenByTag = (plot, zoom, tag, color) => {
-  // console.log('触发:', props.chart.name)
-  if (!tag || !color) return
-  if (!plot) return
-  if (tag === nowThickenTag) return // 避免重复加粗，会造成无限回调
-  if (tag !== nowThickenTag && nowThickenTag) restoreByTag(plot, zoom, nowThickenTag, nowThickenColor)
-  const els = plot.chart.getElements()
-  for (const e of els) {
-    // 多数据模式下，依据tag加粗
-    if (multi) {
-      // 如果是array，取[0],如果是object，取series
-      const series = e.model.data[0]?.series || e.model.data.series
-      // console.log('series', series)
-      if (series === tag) {
-        // console.log('加粗', props.chart.name, e)
-        e.update({ ...e.model, style: { lineWidth: thickerLineWidth } })
-        break
-      }
-    }
-    // 单数据模式下，依据color加粗
-    else {
-      if (color === e.model.color) {
-        e.update({ ...e.model, style: { lineWidth: thickerLineWidth } })
-        break
-      }
-    }
-  }
-  nowThickenTag = tag
-  nowThickenColor = color
-  // 加粗其他图表的数据，保持联动
-  chartRefListExceptSelf.value.forEach((chart) => {
-    chart?.thicken(zoom, tag, color)
-  })
-}
-
-/**
- * 将指定颜色的线段恢复原状
- * @param { Object } plot plot对象，chartObj或者zoomChartObj
- * @param { bool } zoom 是否放大
- * @param { string } tag 实验标签
- */
-const restoreByTag = (plot, zoom, tag, color) => {
-  if (!tag || !color) return
-  if (!nowThickenTag) return
-  const els = plot.chart.getElements()
-  for (const e of els) {
-    // 多数据模式下，依据tag恢复
-    if (multi) {
-      const series = e.model.data[0]?.series || e.model.data.series
-      if (series === tag) {
-        e.update({ ...e.model, style: { lineWidth: lineWidth } })
-        break
-      }
-    }
-    // 单数据模式下，依据color恢复
-    else {
-      if (color === e.model.color) {
-        e.update({ ...e.model, style: { lineWidth: lineWidth } })
-        break
-      }
-    }
-  }
-  nowThickenTag = null
-  nowThickenColor = null
-  chartRefListExceptSelf.value.forEach((chart) => {
-    chart?.thin(zoom, tag, color)
-  })
-}
-
-/**
- * 用于交给其他图表联动调用来加粗线段
- * @param { Object } plot plot对象，chartObj或者zoomChartObj
- * @param { bool } zoom 是否放大
- * @param { string } color 颜色
- */
-const thickenByTagLinkage = (zoom, tag, color) => {
-  const plot = zoom ? zoomChartObj : chartObj
-  thickenByTag(plot, zoom, tag, color)
-}
-
-/**
- * 用于交给其他图表联动调用来恢复线段粗细
- */
-const restoreByTagLinkage = (zoom, tag, color) => {
-  const plot = zoom ? zoomChartObj : chartObj
-  restoreByTag(plot, zoom, tag, color)
-}
-
-// ---------------------------------- 图例渲染，在这保存数据，传给legend组件 ----------------------------------
-
-const legend = ref(null)
-
-const legendHoverin = (item, zoom) => {
-  // console.log('mouseenter', item)
-  const plot = zoom ? zoomChartObj : chartObj
-  thickenByTag(plot, zoom, item.name, item.color)
-}
-
-const legendHoverout = (item, zoom) => {
-  // console.log('mouseleave', item)
-  const plot = zoom ? zoomChartObj : chartObj
-  restoreByTag(plot, zoom, item.name, item.color)
-}
-
-// ---------------------------------- 图表平滑 ----------------------------------
-/**
- * @type {OpenChartData} chartData
- */
-const chartData = inject('data')
-const smooth = (method) => {
-  console.log('smooth by linechart:', chartData)
-  // console.log('smooth by linechart:', method)
-  if (!needSmooth(method)) {
-    smoothMethod = null
-  } else {
-    smoothMethod = method
-  }
-  render(chartData)
-}
-
-// ---------------------------------- 动态设置原始高度 ----------------------------------
-
-let originalHeight = 220
-/**
- * @type {setOriginalChartHeight} setLineChartHeight
- */
-const setLineChartHeight = (height, maxHeight = 800, minHeight = 200) => {
-  if (height > maxHeight) return
-  if (height < minHeight) return
-  const nowLineChartHeight = lineChartRef.value.clientHeight
-  const nowLineChartCanvas = lineChartRef.value.getElementsByTagName('canvas')[0]
-  if (!nowLineChartCanvas) return (originalHeight = height - nowLineChartCanvasHeight)
-  const nowLineChartCanvasHeight = nowLineChartCanvas.clientHeight
-  console.log('nowLineChartHeight', nowLineChartHeight)
-  console.log('nowLineChartCanvasHeight', nowLineChartCanvasHeight)
-  const staticHeight = nowLineChartHeight - nowLineChartCanvasHeight
-  // 设置canvas高度，需要减去静态高度
-  nowLineChartCanvas.style.height = `${height - staticHeight}px`
-}
-
-// ---------------------------------- 暴露api ----------------------------------
-defineExpose({
-  render,
-  change,
-  zoom,
-  smooth,
-  showTooltip: lineShowTooltip,
-  hideTooltip: lineHideTooltip,
-  thicken: thickenByTagLinkage,
-  thin: restoreByTagLinkage,
-  setHeight: setLineChartHeight
-})
-</script>
-
-<style lang="scss"></style>
diff --git a/vue/src/charts/package/TextChart.vue b/vue/src/charts/package/TextChart.vue
deleted file mode 100644
index 8975ad65..00000000
--- a/vue/src/charts/package/TextChart.vue
+++ /dev/null
@@ -1,116 +0,0 @@
-<template>
-  <!-- 图表标题 -->
-  <p class="text-center font-semibold">{{ title }}</p>
-  <!-- 如果图表数据错误 -->
-  <div class="flex flex-col justify-center grow text-dimmer gap-2" v-if="error">
-    <SLIcon class="mx-auto h-5 w-5" icon="error" />
-    <p class="text-center text-xs">
-      <!-- 在此处显示错误信息 -->
-      {{ error }}
-    </p>
-  </div>
-  <!-- 如果图表数据正确 -->
-  <template v-else>
-    <!-- 在此处完成图表主体定义 -->
-    <TextModule class="text-table" :data="data" v-if="originalData" :tag="title" />
-    <!-- 放大效果弹窗 -->
-    <SLModal class="pb-4 overflow-hidden" max-w="-1" v-model="isZoom" @onExit="exitByEsc">
-      <TextModule v-model="isDetailZoom" :data="data" :tag="title" modal />
-    </SLModal>
-  </template>
-</template>
-
-<script setup>
-/**
- * @description: 文字图标
- * @file: TextChart.vue
- * @since: 2024-02-20 20:04:09
- **/
-import { ref, computed } from 'vue'
-import SLModal from '@swanlab-vue/components/SLModal.vue'
-import SLIcon from '@swanlab-vue/components/SLIcon.vue'
-import TextModule from '../modules/TextModule.vue'
-
-// ---------------------------------- 配置 ----------------------------------
-
-const props = defineProps({
-  title: {
-    type: String,
-    required: true
-  },
-  chart: {
-    type: Object,
-    required: true
-  },
-  index: {
-    type: Number,
-    required: true
-  }
-})
-
-const originalData = ref()
-const source = ref(props.chart.source)
-const data = computed(() => {
-  if (!originalData.value) return []
-  return originalData.value[source.value[0]]?.list
-})
-
-// ---------------------------------- 错误处理，如果chart.error存在，则下面的api都将不应该被执行 ----------------------------------
-
-const error = computed(() => {
-  if (!props.chart.error) {
-    return props.chart.error
-  } else if (!originalData.value) {
-    return 'No Data'
-  }
-  return false
-})
-
-// ---------------------------------- 渲染、重渲染功能 ----------------------------------
-
-// 渲染
-const render = (data) => {
-  // 保存原始数据，其中主要数据结构：{tag: {list: [{data: 'xxx', more: {caption: 'xxx'}}]}}
-  originalData.value = data
-}
-
-// 重渲染
-const change = (data) => {
-  // 将发生更新的 tag 数据保存到原始数据中
-  for (let key in data) {
-    // originalData.value[key] = data[key] => 这行代码触发不了 props 的响应式，而下面这行可以
-    originalData.value[key] = { ...data[key] }
-  }
-}
-
-// ---------------------------------- 放大功能 ----------------------------------
-
-// 是否放大
-const isZoom = ref(false)
-// 是否展示数据详情弹窗
-const isDetailZoom = ref(false)
-// 放大数据
-const zoom = () => {
-  isZoom.value = true
-}
-
-// 通过按键退出放大弹窗
-const exitByEsc = () => {
-  // 如果放大弹窗中有数据详情弹窗，则不关闭放大弹窗，直接关闭数据详情弹窗（在 TextModel.vue 中关闭）
-  if (isDetailZoom.value) return
-  isZoom.value = false
-}
-
-// ---------------------------------- 暴露api ----------------------------------
-defineExpose({
-  render,
-  change,
-  zoom
-})
-</script>
-
-<style lang="scss" scoped>
-.text-table {
-  @apply pt-2 -mx-3 w-[calc(100%+1.5rem)];
-}
-</style>
diff --git a/vue/src/charts/package/UnknownChart.vue b/vue/src/charts/package/UnknownChart.vue
deleted file mode 100644
index e278e845..00000000
--- a/vue/src/charts/package/UnknownChart.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<template>
-  <div class="flex flex-col justify-center grow text-dimmer gap-2">
-    <SLIcon class="mx-auto h-5 w-5" icon="error" />
-    <p class="text-center text-xs">{{ $t('chart.unkonwn', { type: chart.type }) }}</p>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 未知图表，用于展示未知类型的图表
- * @file: UnknownChart.vue
- * @since: 2023-12-29 15:55:18
- **/
-
-defineProps({
-  title: {
-    type: String,
-    required: true
-  },
-  chart: {
-    type: Object,
-    required: true
-  }
-})
-
-const render = () => {}
-const change = () => {}
-const zoom = () => {}
-
-defineExpose({
-  render,
-  change,
-  zoom
-})
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/charts/package/smooth.js b/vue/src/charts/package/smooth.js
deleted file mode 100644
index ea214de9..00000000
--- a/vue/src/charts/package/smooth.js
+++ /dev/null
@@ -1,114 +0,0 @@
-/**
- * 数据平滑处理
- */
-
-// 时间加权均衡
-const timeEMA = (data, param) => {
-  console.log('tema', data, param)
-  // 基础smoothingWeight
-  const smoothingWeight = Math.min(Math.sqrt(param || 0), 0.999)
-  let debiasWeight = 0
-  // const rangeOfX = data[data.length - 1].index - data[0].index
-  // 上一个点的y值
-  let lastY = data.length > 0 ? 0 : NaN
-  return data.map((item, index) => {
-    const prevX = index > 0 ? index - 1 : 0
-    // VIEWPORT_SCALE scales the result to the chart's x-axis range
-    const changeInX = data[index].index - data[prevX].index
-    const smoothingWeightAdj = Math.pow(smoothingWeight, changeInX)
-    lastY = lastY * smoothingWeightAdj + data[index].data
-    debiasWeight = debiasWeight * smoothingWeightAdj + 1
-    return { ...item, data: lastY / debiasWeight }
-  })
-}
-
-// 流动平均
-const runningAverage = (data, param) => {
-  // 理论上的窗口大小
-  const w = Math.floor(param / 2)
-  let floorX, ceilX, nowIndex
-  let sum = 0
-  return data.map((item, index) => {
-    floorX = index - w
-    ceilX = index + w
-    console.log('runningAverage', floorX, ceilX)
-    sum = 0
-    for (let i = floorX; i <= ceilX; i++) {
-      nowIndex = Math.min(Math.max(i, 0), data.length - 1)
-      sum += data[nowIndex].data
-    }
-    return { ...item, data: sum / (ceilX - floorX + 1) }
-  })
-}
-
-// 创建高斯函数
-const createGaussian = () => {
-  // 根号2π
-  const sqrt2PI = Math.sqrt(2 * Math.PI)
-  /**
-   * 高斯函数
-   * @param { Number } x 自变量
-   * @param { Number } stdDev 标准差
-   */
-  return (x, stdDev) => {
-    return Math.exp(-0.5 * Math.pow(x / stdDev, 2)) / (stdDev * sqrt2PI)
-  }
-}
-
-// 高斯平均
-const gaussianAverage = (data, param) => {
-  const gaussian = createGaussian()
-  // 在代码中正负无穷似乎在大数据上影响算法速度，但是高斯函数在-3到3区间上进行积分似乎已经达到0.99以上，考虑使用此区间即可
-  const range = 6
-  // 对于每一个值，计算高斯函数的值，只采用-3到3的区间
-  let sum = 0
-  let weightSum = 0
-  return data.map((item, index) => {
-    sum = 0
-    weightSum = 0
-    for (let i = -range; i <= range; i++) {
-      if (index + i >= 0 && index + i < data.length) {
-        sum += data[index + i].data * gaussian(i, param)
-        weightSum += gaussian(i, param)
-      }
-    }
-    return { ...item, data: sum / weightSum }
-  })
-}
-
-// 平滑算法映射关系
-const smoothArithmetics = new Map([
-  [0, undefined],
-  [1, timeEMA],
-  [2, runningAverage],
-  [3, gaussianAverage]
-])
-
-/**
- * 数据平滑处理，传入数据和平滑方法
- * @param { Array } data 数据
- * @param { Object } method 平滑方法
- * @param { Number } method.id 平滑方法id
- * @param { Number} method.value 平滑方法参数
- * @returns { Array } 平滑后的数据
- */
-export default function smooth(data, method) {
-  // console.log('smooth', data, method)
-  return smoothArithmetics.get(method.id)(data, method.value)
-}
-
-/**
- * 判断是否需要平滑，传入平滑方法
- * @param { Object } method 平滑方法
- * @param { Number } method.id 平滑方法id
- * @param { Number} method.value 平滑方法参数
- * @returns { Boolean } 是否需要平滑，true为需要，false为不需要
- */
-export const needSmooth = (method) => {
-  if (!method.value) {
-    return false
-  }
-  if (method.id !== 0) {
-    return true
-  }
-}
diff --git a/vue/src/charts/package/utils.js b/vue/src/charts/package/utils.js
deleted file mode 100644
index 0882225e..00000000
--- a/vue/src/charts/package/utils.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * 在此处封装一些图表可共用的函数逻辑
- */
-
-/**
- * 将refrence字段映射为x轴字段和显示的x轴名称
- */
-export const refrence2XField = {
-  step: { xField: 'index', xTitle: 'Step' }
-}
-
-export const transparentColor = (color, opacity = 0.1) => {
-  // 将十六进制增加透明度通道
-  opacity = Math.floor(opacity * 255)
-  // opacity转为十六进制
-  const opacityHex = opacity.toString(16)
-  return color + opacityHex
-}
diff --git a/vue/src/components/README.md b/vue/src/components/README.md
deleted file mode 100644
index 00c386c1..00000000
--- a/vue/src/components/README.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# components
-
-这个目录包含了当前项目的全局通用组件，这些组件可以在任何地方使用。
-这个目录下的组件应该遵循以下原则：
-
-1. 不能包含业务逻辑，不能使用api等包含业务的js文件，也不能导入i18n
-2. 不能包含状态（store）
-3. 不能包含路由，里面也不能检测路由变化
-4. 可以使用tailwindcss中配置好的内容
-5. 可以使用全局的scss变量
-
-在组件中不允许使用i18n，应该在外部用i18n组件传递props
diff --git a/vue/src/components/SLButton.vue b/vue/src/components/SLButton.vue
deleted file mode 100644
index 7f30acc0..00000000
--- a/vue/src/components/SLButton.vue
+++ /dev/null
@@ -1,154 +0,0 @@
-<template>
-  <button :class="themeClass" :disabled="disabled" :title="disabled ? disabledTip : tip">
-    <slot></slot>
-    {{ text }}
-  </button>
-</template>
-
-<script setup>
-/**
- * SaButton - 封装一些常用的按钮样式，只对背景、字体颜色和边框颜色做了封装，其他样式需要自己写
- * @prop {string} theme - 按钮主题，可选值: primary、positive、negative、default等，默认值: default
- * @prop {string} text - 按钮文本，可不填，以插槽形式填入
- *
- *
- * @example
- * <SLButton text="你好" theme="primary" hollow />
- */
-import { computed } from 'vue'
-const props = defineProps({
-  theme: {
-    type: String,
-    default: 'default'
-  },
-  hollow: {
-    type: Boolean,
-    default: false
-  },
-  gradient: {
-    type: Boolean,
-    default: false
-  },
-  text: {
-    type: String,
-    default: ''
-  },
-  disabled: {
-    type: Boolean,
-    default: false
-  },
-  tip: {
-    type: String,
-    default: ''
-  },
-  disabledTip: {
-    type: String,
-    default: 'Not Allowed'
-  }
-})
-
-const themeClass = computed(() => {
-  const theme = `sa-button-${props.theme}${props.hollow ? '-hollow' : ''}`
-  return props.disabled ? '' : theme
-})
-</script>
-
-<style lang="scss" scoped>
-button {
-  @apply border select-none box-border;
-  &:disabled {
-    pointer-events: auto !important;
-    cursor: not-allowed !important;
-    opacity: 0.3;
-  }
-}
-
-// 默认样式
-.sa-button-default {
-  @apply bg-higher border-default text-default;
-
-  &:hover {
-    @apply bg-highest;
-  }
-
-  &:active {
-    @apply border-primary-default;
-  }
-}
-
-// primary样式
-.sa-button-primary {
-  @apply bg-primary-higher border-primary-higher text-white-default;
-
-  &:hover {
-    @apply bg-primary-default border-primary-default;
-  }
-
-  &:active {
-    border-color: var(--foreground-default);
-  }
-}
-
-// positive样式
-.sa-button-positive {
-  @apply text-white-default border-positive-higher bg-positive-higher;
-
-  &:hover {
-    @apply border-positive-default bg-positive-default;
-  }
-
-  &:active {
-    @apply border-primary-default;
-  }
-}
-
-// negative样式
-.sa-button-negative {
-  @apply text-white-default border-negative-higher bg-negative-higher;
-
-  &:hover {
-    @apply border-negative-default bg-negative-default;
-  }
-
-  &:active {
-    @apply border-primary-default;
-  }
-}
-
-// ----------------------- hollow 样式 -----------------------
-.sa-button-default-hollow {
-  @apply text-default border-default bg-default transition-all;
-
-  &:hover {
-    @apply bg-higher;
-  }
-
-  &:active {
-    @apply border-primary-default;
-  }
-}
-
-.sa-button-negative-hollow {
-  @apply text-negative-default border-negative-default bg-default transition-all;
-
-  &:hover {
-    @apply bg-higher;
-  }
-
-  &:active {
-    @apply border-primary-default;
-  }
-}
-
-.sa-button-primary-hollow {
-  @apply text-primary-default border-primary-default bg-default transition-all;
-
-  &:hover {
-    @apply bg-higher;
-  }
-
-  &:active {
-    @apply border-primary-default;
-  }
-}
-</style>
diff --git a/vue/src/components/SLCheck.vue b/vue/src/components/SLCheck.vue
deleted file mode 100644
index 8342c615..00000000
--- a/vue/src/components/SLCheck.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
-  <div
-    class="flex items-center gap-2"
-    :class="{ 'cursor-pointer': !disabled }"
-    @click="!disabled && $emit('update:checked', !checked)"
-  >
-    <input type="checkbox" :checked="checked && !disabled" :disabled="disabled" />
-    <span class="select-none truncate">{{ label }}</span>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 选项框
- * @file: SLCheck.vue
- * @since: 2024-01-16 18:13:34
- **/
-
-defineProps({
-  label: {
-    type: String,
-    default: ''
-  },
-  checked: {
-    type: Boolean,
-    default: false
-  },
-  disabled: {
-    type: Boolean,
-    default: false
-  }
-})
-
-defineEmits(['update:checked'])
-</script>
-
-<style lang="scss" scoped>
-input[type='checkbox'] {
-  @apply border cursor-pointer w-4 h-4;
-  &:disabled {
-    @apply cursor-not-allowed;
-  }
-}
-</style>
diff --git a/vue/src/components/SLIcon.vue b/vue/src/components/SLIcon.vue
deleted file mode 100644
index 22ca9a87..00000000
--- a/vue/src/components/SLIcon.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<template>
-  <svg aria-hidden="true">
-    <use :xlink:href="'#icon-' + icon"></use>
-  </svg>
-</template>
-
-<script setup>
-/**
- * @description: 对iconfont图标的使用进行封装
- * @file: SLIcon.vue
- * @since: 2023-12-04 16:07:51
- **/
-import '@swanlab-vue/assets/iconfont.js'
-defineProps({
-  icon: {
-    type: String,
-    required: true
-  }
-})
-</script>
diff --git a/vue/src/components/SLLoading.vue b/vue/src/components/SLLoading.vue
deleted file mode 100644
index 0f098b71..00000000
--- a/vue/src/components/SLLoading.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<template>
-  <div
-    class="relative flex items-center justify-center rounded-full overflow-hidden animate-spin"
-    :class="size ? `w-${size} h-${size}` : 'w-8 h-8'"
-  >
-    <div class="absolute w-full h-full rounded-full border-4 border-t-positive-default border-positive-dimmest"></div>
-    <div class="w-2 h-2 z-10 rounded-full bg-positive-default animate-ping"></div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 加载动画
- * @file: SLLoading.vue
- * @since: 2023-12-12 17:35:53
- **/
-
-defineProps({
-  size: {
-    type: String,
-    default: '8'
-  }
-})
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/components/SLModal.vue b/vue/src/components/SLModal.vue
deleted file mode 100644
index 4ab19485..00000000
--- a/vue/src/components/SLModal.vue
+++ /dev/null
@@ -1,118 +0,0 @@
-<template>
-  <Dialog as="div" class="sl-dialog-mask" data-sl-modal :open="modelValue" @click.prevent="close('mask')">
-    <!-- 模态框主体部分 -->
-    <div class="sl-dialog animate-bounce-in" :class="$props.class" :style="{ maxWidth }" data-sl-modal @click.stop="">
-      <!-- 关闭按钮 -->
-      <button
-        class="absolute z-full !border-none"
-        :class="[buttonPosition, buttonStyle]"
-        @click="close('button')"
-        v-if="!hiddenClose"
-      >
-        <SLIcon class="w-full h-full" icon="close" />
-      </button>
-      <!-- 插槽，模态框内容 -->
-      <slot></slot>
-    </div>
-  </Dialog>
-</template>
-
-<script setup>
-import { computed, watch } from 'vue'
-import { Dialog } from '@headlessui/vue'
-import SLIcon from './SLIcon.vue'
-
-const props = defineProps({
-  // 组件挂载时是否显示，默认不显示，值变为true时显示
-  modelValue: {
-    type: Boolean,
-    default: false
-  },
-  // 点击遮罩层是否关闭，默认不关闭
-  closeOnOverlayClick: {
-    type: Boolean,
-    default: false
-  },
-  // 关闭按钮的位置
-  buttonPosition: {
-    type: String,
-    default: 'right-4 top-4 '
-  },
-  // 除了位置以外的其他样式
-  buttonStyle: {
-    type: String,
-    default: 'w-5 h-5 p-1 rounded'
-  },
-  maxW: {
-    type: String,
-    default: '800'
-  },
-  class: {
-    type: String,
-    default: ''
-  },
-  hiddenClose: {
-    type: Boolean,
-    default: false
-  },
-  // 是否可通过键盘 esc 键关闭弹窗
-  escExit: {
-    type: Boolean,
-    default: false
-  }
-})
-// onBeforeClose事件特指人为关闭事件，如果是在外部用js修改modelValue的值，不会触发该事件
-const emits = defineEmits(['onBeforeClose', 'update:modelValue', 'onExit'])
-
-// 关闭弹窗方法
-const close = (type) => {
-  if (type === 'mask' && !props.closeOnOverlayClick) return
-  emits('update:modelValue', false)
-  emits('onBeforeClose')
-}
-
-const maxWidth = computed(() => {
-  return props.maxW + 'px'
-})
-
-// ---------------------------------- 键盘esc关闭弹窗 ----------------------------------
-
-/**
- * 通过按键关闭弹窗
- * @param {*} param0
- */
-const handleEsc = ({ key }) => {
-  if (key != 'Escape') return
-  emits('onExit')
-  if (props.escExit) {
-    emits('onBeforeClose')
-    close('esc')
-  }
-}
-
-// 订阅或销毁键盘事件
-watch(
-  () => props.modelValue,
-  (value) => {
-    if (value) return window.addEventListener('keyup', handleEsc)
-    window.removeEventListener('keyup', handleEsc)
-  }
-)
-</script>
-
-<style lang="scss">
-.sl-dialog-mask[data-sl-modal] {
-  @apply z-full w-full h-full fixed top-0 left-0;
-  @apply py-16 px-8 overflow-y-auto overflow-x-hidden;
-  background-color: var(--background-overlay);
-  // 高斯模糊
-  backdrop-filter: blur(10px);
-}
-
-.sl-dialog[data-sl-modal] {
-  @apply rounded-lg relative mx-auto;
-  /** 样式 */
-  background-color: var(--background-default);
-  border: 1px solid var(--outline-default);
-}
-</style>
diff --git a/vue/src/components/SLOperation.vue b/vue/src/components/SLOperation.vue
deleted file mode 100644
index f0d367ad..00000000
--- a/vue/src/components/SLOperation.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<template>
-  <div class="absolute">
-    <button class="outline-none" @click="show">
-      <slot></slot>
-    </button>
-    <div
-      class="absolute opacity-0 transition-all border p-2 rounded-lg"
-      :class="`${isShow ? 'opacity-100' : ''} ${position}`"
-    >
-      <slot name="main"></slot>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 操作集合
- * @file: SLOperation.vue
- * @since: 2024-01-08 13:32:03
- *
- * 当有多个按钮、内容需要集成，并且具有收起、展开样式，可以使用该组件
- **/
-
-import { ref } from 'vue'
-
-defineProps({
-  // 展开方向
-  direction: {
-    type: String,
-    default: 'down',
-    validator: (value) => {
-      return ['up', 'down', 'right', 'left'].includes(value)
-    }
-  },
-  // 相对位置
-  position: {
-    type: String,
-    default: 'right-0 top-8'
-  }
-})
-
-const isShow = ref(false)
-
-const show = () => {
-  isShow.value = !isShow.value
-}
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/components/SLSearch.vue b/vue/src/components/SLSearch.vue
deleted file mode 100644
index 3255048b..00000000
--- a/vue/src/components/SLSearch.vue
+++ /dev/null
@@ -1,78 +0,0 @@
-<template>
-  <div class="swan-search" :class="{ 'icon-reverse': reverse }">
-    <SLIcon class="swan-icon" icon="search" />
-    <input
-      type="text"
-      v-model="value"
-      class="focus:border-primary-default hover:border-primary-dimmer"
-      :placeholder="placeholder"
-      @input="input"
-      @keydown.enter="$emit('search', value)"
-    />
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 搜索
- * @file: SLSearch.vue
- * @since: 2024-01-08 16:36:14
- **/
-
-import { ref } from 'vue'
-import SLIcon from './SLIcon.vue'
-import { debounce } from '@swanlab-vue/utils/common'
-
-const emits = defineEmits(['input', 'search', 'update:modelValue'])
-
-const props = defineProps({
-  delay: {
-    type: String,
-    default: '0'
-  },
-  placeholder: {
-    type: String,
-    required: true
-  },
-  modelValue: {
-    type: String,
-    default: ''
-  },
-  reverse: {
-    type: Boolean,
-    default: false
-  }
-})
-
-const value = ref(props.modelValue || '')
-
-const input = debounce(() => {
-  emits('input', value.value)
-  emits('update:modelValue', value.value)
-}, props.delay)
-</script>
-
-<style lang="scss" scoped>
-.swan-search {
-  @apply relative w-full rounded-lg;
-  input {
-    @apply w-full outline-none text-sm text-dimmer border rounded-lg py-1.5 px-3 transition-all;
-    @apply pl-7;
-  }
-
-  .swan-icon {
-    @apply w-4 h-4 shrink-0;
-    @apply absolute top-1/2 left-2 transform -translate-y-1/2;
-  }
-}
-
-.icon-reverse {
-  input {
-    @apply pl-3 pr-7;
-  }
-
-  .swan-icon {
-    @apply right-3 left-auto;
-  }
-}
-</style>
diff --git a/vue/src/components/SLSlideBar.vue b/vue/src/components/SLSlideBar.vue
deleted file mode 100644
index a024391d..00000000
--- a/vue/src/components/SLSlideBar.vue
+++ /dev/null
@@ -1,249 +0,0 @@
-<template>
-  <div class="flex items-center w-full">
-    <div class="sliding-range" :class="{ '!cursor-default': disabled }" ref="slidingRange">
-      <!-- 滑动条容器 -->
-      <div class="w-full relative h-1 flex">
-        <!-- 滑动条进度 -->
-        <div class="sliding-bar" :style="{ backgroundColor: barColor }" ref="slidingBar"></div>
-        <!-- 滑动条背景容器 -->
-        <div class="sliding-container"></div>
-        <!-- 滑动按钮 -->
-        <div class="sliding-button" ref="slidingButton"></div>
-      </div>
-    </div>
-    <!-- 输入框,只有change的时候才会更改置 -->
-    <div class="flex items-center relative ml-3" :class="{ 'input-disabled': disabled }" v-if="showInput">
-      <input
-        type="number"
-        :disabled="disabled"
-        :value="props.modelValue"
-        ref="inputRef"
-        @keydown.enter="handleChange"
-        @blur="handleChange"
-      />
-      <div class="w-3 flex-shrink-0 flex-col flex absolute right-1">
-        <SLIcon icon="down" class="w-full h-3 -rotate-180 -mb-1" @click="handleClickUp" />
-        <SLIcon icon="down" class="w-full aspect-square" @click="handleClickDown" />
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 滑块组件
- * @file: SLSlideBar.vue
- * @since: 2024-01-30 16:06:28
- **/
-import { computed, onMounted, ref, watch } from 'vue'
-import SLIcon from './SLIcon.vue'
-/**
- * 滑块封装组件
- */
-const props = defineProps({
-  // 滑块的最小值
-  min: {
-    type: Number,
-    default: 0
-  },
-  // 滑块的最大值
-  max: {
-    type: Number,
-    default: 100
-  },
-  // 滑块的当前值，通过v-model双向绑定
-  modelValue: {
-    default: 0
-  },
-  // 是否为整数
-  isInt: {
-    type: Boolean,
-    default: false
-  },
-  // 已经滑动的进度条颜色
-  barColor: {
-    type: String,
-    default: 'var(--positive-default)'
-  },
-  // 是否显示输入框
-  showInput: {
-    type: Boolean,
-    default: false
-  },
-  // 是否禁用，禁用时不可拖动
-  disabled: {
-    type: Boolean,
-    default: false
-  }
-})
-
-const inputRef = ref(null)
-
-watch(
-  () => props.modelValue,
-  () => {
-    // 访问一次slidingValue，触发get
-    slidingValue.value
-    if (!inputRef.value) return
-    inputRef.value.value = props.modelValue
-  }
-)
-
-const emits = defineEmits(['on-change', 'update:modelValue', 'turn', 'change'])
-const slidingBar = ref(null)
-const slidingButton = ref(null)
-const slidingRange = ref(null)
-const slidingValue = computed({
-  get() {
-    const v = formatValue(props.modelValue)
-    setButtonAndBarPosition(v)
-    return v
-  },
-  set(val) {
-    if (val === slidingValue.value) return
-    if (!val) val = 0
-    // console.log(val)
-    // 设置滑动按钮的位置
-    // 触发事件
-    emits('update:modelValue', val)
-  }
-})
-
-onMounted(() => {
-  setButtonAndBarPosition(slidingValue.value)
-  // 监听滑动条的点击、拖动事件
-  slidingRange.value.addEventListener('mousedown', (e) => {
-    if (props.disabled) return
-    const startX = e.clientX
-    const rect = slidingRange.value.getBoundingClientRect()
-    const startLeft = rect.left
-    // 相减得到偏移量
-    const offsetLeft = startX - startLeft
-    // 总长度
-    const slidingRangeWidth = slidingRange.value.offsetWidth
-    // console.log(startX, startLeft)
-    // 设置滑动按钮的位置
-    // console.log(offsetLeft / slidingRangeWidth)
-    // 双向绑定
-    const newValue = formatValue((offsetLeft / slidingRangeWidth) * props.max)
-    slidingValue.value = newValue
-    // 触发事件
-    emits('on-change', newValue)
-
-    // 滑动事件
-    const mousemove = (e) => {
-      const moveX = e.clientX
-      let left = moveX - startX + offsetLeft
-      const process = left / slidingRangeWidth
-      const newValue = formatValue(process * props.max)
-      slidingValue.value = newValue
-      // 触发事件
-      emits('on-change', newValue)
-    }
-    const mouseup = () => {
-      emits('change', slidingValue.value)
-      // console.log('mouseup')
-      document.removeEventListener('mousemove', mousemove)
-      document.removeEventListener('mouseup', mouseup)
-    }
-    document.addEventListener('mousemove', mousemove)
-    document.addEventListener('mouseup', mouseup)
-  })
-})
-
-// 格式化值
-function formatValue(value) {
-  if (props.isInt) value = Math.floor(value)
-  // 值在最小值和最大值之间
-  value = Math.min(Math.max(value, props.min), props.max)
-  // 保留两位小数，且如果是整数，不显示小数点‘
-  value = Number(value.toFixed(2).replace('.00', ''))
-  return value
-}
-
-// 设置按钮位置和进度条位置，传入的是value
-function setButtonAndBarPosition(value) {
-  const process = value / props.max
-  // 按钮位置
-  const percent = Math.min(process * 100, 100) + '%'
-  slidingButton.value.style.left = percent
-  // 进度条长度
-  slidingBar.value.style.width = percent
-}
-
-// ---------------------------------- 右侧输入框相关 ----------------------------------
-
-/**
- * 向后渐少
- */
-const handleClickDown = () => {
-  if (props.disabled) return
-  if (props.modelValue > props.min) {
-    emits('turn', 'backward', props.modelValue)
-  }
-}
-
-/**
- * 向前增加
- */
-const handleClickUp = () => {
-  if (props.disabled) return
-  if (props.modelValue < props.max) {
-    emits('turn', 'forward', props.modelValue)
-  }
-}
-
-const handleChange = (e) => {
-  e.preventDefault()
-  // 保留两位小数
-  e.target.value = Number(e.target.value).toFixed(2)
-  if (props.disabled) return console.log('disabled')
-  if (e.target.value == props.modelValue) return console.log('same')
-  if (e.target.value < props.min) e.target.value = props.min
-  if (e.target.value > props.max) e.target.value = props.max
-  emits('change', Number(e.target.value))
-}
-</script>
-
-<style lang="scss" scoped>
-.sliding-range {
-  @apply hover:cursor-grab active:cursor-grabbing mx-0.5 py-3 grow;
-  // 滑动条进度
-  .sliding-bar {
-    @apply h-full rounded-l-full;
-  }
-  // 滑动条背景容器
-  .sliding-container {
-    @apply h-full rounded-r-full grow;
-    background-color: var(--outline-default);
-  }
-  // 滑动按钮
-  .sliding-button {
-    @apply h-3 w-3 border-2 border-dimmer rounded-full bg-default top-0 -translate-y-[30%] -translate-x-[50%] absolute;
-  }
-
-  // 当滑动条被点击时，滑动按钮的样式
-  &:active {
-    .sliding-button {
-      @apply border-dimmer;
-    }
-  }
-}
-
-input[type='number']::-webkit-inner-spin-button,
-input[type='number']::-webkit-outer-spin-button {
-  -webkit-appearance: none;
-  margin: 0;
-}
-input[type='number'] {
-  -moz-appearance: textfield;
-  appearance: textfield;
-}
-
-input {
-  @apply w-16 h-6 pl-1 pr-5 rounded border outline-none bg-transparent text-xs focus:border-primary-default;
-}
-.input-disabled {
-  @apply opacity-50;
-}
-</style>
diff --git a/vue/src/components/SLStatusLabel.vue b/vue/src/components/SLStatusLabel.vue
deleted file mode 100644
index cd5df2f2..00000000
--- a/vue/src/components/SLStatusLabel.vue
+++ /dev/null
@@ -1,110 +0,0 @@
-<template>
-  <component :is="label" class="sl-status-label" :class="config.class" :href="url" @click.prevent="handleClick">
-    <SLIcon :icon="config.icon" />
-    <slot></slot>
-  </component>
-</template>
-
-<script setup>
-/**
- * @description: 实验状态展示，status为0代表正在运行，为1代表运行成功，-1代表运行失败
- * @param { Number } status 实验状态
- * @param { Number } id 实验id，如果不传入id，点击状态标签不会跳转
- * @file: StatusLabel.vue
- * @since: 2023-12-08 20:24:49
- **/
-import { computed } from 'vue'
-import { useRouter } from 'vue-router'
-const router = useRouter()
-const props = defineProps({
-  status: {
-    type: Number,
-    required: true
-  },
-  url: {
-    type: String,
-    default: ''
-  }
-})
-
-// ---------------------------------- 将status转译为对应的状态提示和class ----------------------------------
-
-const config = computed(() => {
-  let className, icon
-  switch (props.status) {
-    case 0:
-      className = 'running'
-      icon = 'circle-fill'
-      break
-    case 1:
-      className = 'finished'
-      icon = 'success'
-      break
-    case -1:
-      className = 'crashed'
-      icon = 'error'
-      break
-  }
-  if (!className) throw new Error('Invalid status:' + props.status + 'in StatusLabel.vue')
-
-  return {
-    class: className,
-    icon
-  }
-})
-
-// ---------------------------------- 动态选择标签类型 ----------------------------------
-const label = computed(() => {
-  if (props.url) return 'a'
-  return 'div'
-})
-
-// ---------------------------------- 代理a标签点击事件 ----------------------------------
-const handleClick = () => {
-  if (!props.url) return
-  router.push(props.url)
-}
-</script>
-
-<style lang="scss" scoped>
-@mixin icon {
-  svg {
-    @apply w-4 h-4;
-  }
-}
-.sl-status-label {
-  @apply px-3 py-1 rounded-full text-sm text-center inline-flex items-center gap-1.5;
-}
-
-.crashed {
-  @apply bg-negative-dimmest text-negative-default;
-  @include icon;
-}
-
-.finished {
-  @apply bg-positive-dimmest text-positive-higher;
-  @include icon;
-}
-.running {
-  @apply bg-primary-dimmest text-primary-higher;
-  @include icon;
-  // 设置svg动画，呼吸效果
-  @keyframes breathe {
-    0% {
-      transform: scale(0.8);
-      opacity: 0.5;
-    }
-    50% {
-      transform: scale(1);
-      opacity: 1;
-    }
-    100% {
-      transform: scale(0.8);
-      opacity: 0.5;
-    }
-  }
-  svg {
-    animation: breathe 1.5s ease-in-out infinite;
-  }
-}
-</style>
diff --git a/vue/src/components/confirm/SLConfirm.vue b/vue/src/components/confirm/SLConfirm.vue
deleted file mode 100644
index 05ad23f6..00000000
--- a/vue/src/components/confirm/SLConfirm.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-<template>
-  <SLModal @on-before-close="cancel" v-model="visiable" class="p-6 h-48 flex flex-col" button-position="top-7 right-6">
-    <h1 class="text-xl font-semibold">{{ text.title }}</h1>
-    <p class="mt-4 text-dimmer text-base">{{ text.content }}</p>
-    <div class="mt-auto text-sm flex gap-3 justify-end">
-      <SLButton :text="text.cancel" @click="cancel" />
-      <SLButton :text="text.confirm" theme="negative" @click="confirm" />
-    </div>
-  </SLModal>
-</template>
-
-<script setup>
-/**
- * @description: 全局确认弹窗，在app创建的、同时实例化此组件
- * @file: SLConfirm.vue
- * @since: 2024-01-03 19:19:46
- **/
-import { ref } from 'vue'
-import SLModal from '../SLModal.vue'
-import { reactive } from 'vue'
-import SLButton from '../SLButton.vue'
-
-const visiable = ref(false)
-const defaultConfig = {
-  title: 'Are you sure?',
-  content: 'Are you sure you want to do this?',
-  confirm: 'Yes, I confirm',
-  cancel: 'Cancel'
-}
-
-const text = reactive({
-  title: defaultConfig.title,
-  content: defaultConfig.content,
-  confirm: 'Yes, I confirm',
-  cancel: 'Cancel'
-})
-
-const callback = {
-  resolve: () => {},
-  reject: () => {}
-}
-
-// ---------------------------------- 控制展示和隐藏 ----------------------------------
-// 通过调用此方法，显示弹窗，传入标题、内容、配置，配置包含：确认按钮文字、取消按钮文字、确认回调、取消回调
-const show = (title, content, config, resolve, reject) => {
-  visiable.value = true
-  text.title = title || defaultConfig.title
-  text.content = content || defaultConfig.content
-  text.confirm = config?.buttonText?.confirm || defaultConfig.confirm
-  text.cancel = config?.buttonText?.cancel || defaultConfig.cancel
-  callback.resolve = resolve
-  callback.reject = reject
-}
-
-const confirm = () => {
-  visiable.value = false
-  callback.resolve && callback.resolve()
-}
-
-const cancel = () => {
-  visiable.value = false
-  callback.reject && callback.reject()
-}
-
-// ---------------------------------- 暴露api ----------------------------------
-defineExpose({
-  show,
-  confirm,
-  cancel
-})
-</script>
-
-<style lang="scss" scoped>
-button {
-  @apply p-2 rounded;
-}
-</style>
diff --git a/vue/src/components/confirm/index.js b/vue/src/components/confirm/index.js
deleted file mode 100644
index 6085f46a..00000000
--- a/vue/src/components/confirm/index.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import SLConfirmVue from './SLConfirm.vue'
-
-const confirmObj = {
-  // 用于存储组件实例
-  _r: null,
-  get _ref() {
-    if (!confirmObj._r) console.error('Message组件未注册')
-    return confirmObj._r.value
-  }
-}
-
-/**
- * 弹出确认框
- *
- * @param {string} title - 确认框的标题，如果不传默认为"Are you sure?"。
- * @param {string} content - 确认框的内容, 如果不传默认为"Are you sure you want to do this?"。
- * @param {Object} [config] - 模态对话框的配置选项
- * @param {Object} [config.buttonText] - 按钮的文本, 分为confirm和cancel两个按钮, 如果不传默认为"Yes, I confirm"和"Cancel"。
- * @param {string} [config.buttonText.confirm] - 确认按钮的文本, 如果不传默认为"Yes, I confirm"。
- * @param {string} [config.buttonText.cancel] - 取消按钮的文本, 如果不传默认为"Cancel"。
- *
- * @returns {Promise} - 返回一个Promise对象，当用户点击确认按钮时，Promise会resolve，否则会reject。
- */
-export const confirm = (title, content, config = {}) => {
-  return new Promise((resolve, reject) => {
-    confirmObj._ref.show(title, content, config, resolve, reject)
-  })
-}
-
-export const installConfirm = (ref) => {
-  confirmObj._r = ref
-}
-
-export const SLConfirm = SLConfirmVue
diff --git a/vue/src/components/menu/SLMenu.vue b/vue/src/components/menu/SLMenu.vue
deleted file mode 100644
index 22b4b263..00000000
--- a/vue/src/components/menu/SLMenu.vue
+++ /dev/null
@@ -1,119 +0,0 @@
-<template>
-  <Menu as="div" class="relative select-none" :class="props.menuClass" v-slot="{ open, close }">
-    <!-- 按钮区域 -->
-    <div class="relative w-full items-center flex select-none" ref="button">
-      <MenuButton class="w-full" :class="[{ 'sa-popover-button-active': open }]" data-sa-menu>
-        <slot :open="open">按钮</slot>
-        <SLIcon
-          icon="down"
-          class="h-4 w-4 absolute transition-transform duration-300"
-          :class="[$props.downPosition, { 'rotate-180': open }]"
-          v-if="down"
-        />
-      </MenuButton>
-    </div>
-    <!-- 弹窗区域 -->
-    <div class="absolute z-full" :class="[$props.class]" :style="{ minWidth: menuMinWidth + 'px' }" ref="container">
-      <MenuItems
-        tabindex=""
-        as="div"
-        class="sa-menu-pop py-2"
-        :class="[$props.itemsClass]"
-        :style="{ maxHeight: props.menuMaxHeight + 'px' }"
-        data-sa-menu
-      >
-        <slot name="pop" :open="open" :close="close">弹窗区域</slot>
-      </MenuItems>
-    </div>
-    <!-- 尖尖角 -->
-    <div class="sa-menu-corner" :class="[props.cornerClass]" v-if="hasCorner && open" data-sa-menu ref="corner"></div>
-  </Menu>
-</template>
-
-<script setup>
-/**
- * @description: 菜单组件, 用于包裹菜单项
- * @file: SLMenu.vue
- * @since: 2024-03-04 21:30:31
- **/
-import { Menu, MenuButton, MenuItems } from '@headlessui/vue'
-import SLIcon from '../SLIcon.vue'
-
-const props = defineProps({
-  // 是否需要down图标
-  down: {
-    type: Boolean,
-    default: false
-  },
-  // 右侧down的位置
-  downPosition: {
-    type: String,
-    default: 'right-3 top-3'
-  },
-  // 菜单样式，影响最顶层的样式
-  menuClass: {
-    type: String
-  },
-  // 弹窗的样式，直观感受上，对外层组件使用class的时候应该修改弹出框的样式
-  class: {
-    type: String,
-    default: 'w-full'
-  },
-  // 弹窗最小宽度
-  menuMinWidth: {
-    type: String,
-    default: '120'
-  },
-  // 弹窗的最大高度（超出此高度后，需要与itemsClass配合出现滑动条）
-  menuMaxHeight: {
-    type: String,
-    default: '240'
-  },
-  // 默认颜色，一般用于覆盖
-  itemsClass: {
-    type: String,
-    default: 'bg-default'
-  },
-  // 尖尖角样式，只要输入值就代表有尖尖角
-  cornerClass: {
-    type: String,
-    default: ''
-  },
-  // 自动翻转，默认开始
-  notFlip: {
-    type: Boolean,
-    default: false
-  }
-})
-
-const hasCorner = computed(() => {
-  return !!props.cornerClass
-})
-</script>
-
-<style lang="scss" scoped>
-.sa-popover-button-active[data-sa-menu] {
-  border-color: var(--primary-default) !important;
-}
-
-@mixin corner {
-  position: absolute;
-  display: block;
-  width: 12px;
-  height: 12px;
-  pointer-events: none;
-  content: ' ';
-  clip-path: polygon(20% 0, 100% 0, 100% 80%, 80% 100%, 0 20%);
-}
-
-.sa-menu-corner[data-sa-menu] {
-  @apply z-full border-t border-r -rotate-45;
-  @apply bg-default;
-  @include corner;
-}
-
-.sa-menu-pop[data-sa-menu] {
-  @apply border rounded w-full text-sm my-1;
-  box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.1);
-}
-</style>
diff --git a/vue/src/components/menu/SLMenuItem.vue b/vue/src/components/menu/SLMenuItem.vue
deleted file mode 100644
index e8520423..00000000
--- a/vue/src/components/menu/SLMenuItem.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-<template>
-  <MenuItem :as="as" class="p-3 hover:bg-higher cursor-pointer">
-    <slot></slot>
-  </MenuItem>
-</template>
-
-<script setup>
-/**
- * SLMenuItem - 菜单项组件，封装headlessui的MenuItem组件
- */
-import { MenuItem } from '@headlessui/vue'
-
-defineProps({
-  as: {
-    type: String,
-    default: 'div'
-  }
-})
-</script>
diff --git a/vue/src/components/menu/SLMenuItems.vue b/vue/src/components/menu/SLMenuItems.vue
deleted file mode 100644
index 1ee9f419..00000000
--- a/vue/src/components/menu/SLMenuItems.vue
+++ /dev/null
@@ -1,16 +0,0 @@
-<template>
-  <div class="sa-menu-items">
-    <slot></slot>
-  </div>
-</template>
-
-<script setup></script>
-
-<style lang="scss" scoped>
-.sa-menu-items {
-  @apply w-full;
-  &:not(:last-child) {
-    @apply border-b pb-2;
-  }
-}
-</style>
diff --git a/vue/src/components/message/SLMessage.vue b/vue/src/components/message/SLMessage.vue
deleted file mode 100644
index 6e710eaf..00000000
--- a/vue/src/components/message/SLMessage.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-<template>
-  <div class="flex justify-center">
-    <div class="message-container" :class="[message.type, theme === 'dark' ? 'dark-theme' : '']">
-      <!-- 成功图标 -->
-      <SLIcon v-if="message.type === 'success'" icon="success" class="text-white-default" />
-      <!-- 错误图标 -->
-      <SLIcon v-if="message.type === 'error'" icon="error" class="text-white-default" />
-      <!-- 警告图标 -->
-      <SLIcon v-if="message.type === 'warning'" icon="info" class="text-white-default" />
-      <p class="text-nowrap">{{ message.text }}</p>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 消息提示组件，用于显示单个消息提示，由SLMessages组件调用，不应该单独使用
- * @file: SLMessage.vue
- * @property { string } text 消息提示的信息
- * @property { string } type 消息提示的类型，可选值为error、success、processing
- * @since: 2024-01-03 15:34:01
- **/
-import { onMounted, onUnmounted } from 'vue'
-
-const props = defineProps({
-  message: {
-    type: Object,
-    required: true
-  },
-  theme: {
-    type: String,
-    default: 'default'
-  }
-})
-
-let timer = null
-onMounted(() => {
-  timer = setTimeout(() => {
-    props.message.close()
-  }, props.message.delay)
-})
-
-onUnmounted(() => {
-  clearTimeout(timer)
-})
-</script>
-
-<style lang="scss" scoped>
-.dark-theme {
-  background-color: #27282e !important;
-  color: white !important;
-  border-color: #616568 !important;
-}
-.message-container {
-  @apply px-3 h-10 border rounded inline-flex items-center gap-2 shadow-md bg-default text-sm;
-  svg {
-    @apply w-5 h-5;
-  }
-}
-.success {
-  svg {
-    @apply text-positive-default;
-  }
-}
-
-.error {
-  svg {
-    @apply text-negative-default;
-  }
-}
-
-.warning {
-  svg {
-    @apply text-warning-dimmer;
-  }
-}
-</style>
diff --git a/vue/src/components/message/SLMessages.vue b/vue/src/components/message/SLMessages.vue
deleted file mode 100644
index b87809df..00000000
--- a/vue/src/components/message/SLMessages.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<template>
-  <TransitionGroup name="list" tag="ul" class="messages-container">
-    <SLMessage :message="m" :theme="m.theme" v-for="m in messages" :key="m.id" />
-  </TransitionGroup>
-</template>
-
-<script setup>
-/**
- * @description: 消息提示组件容器，全局挂载，通过App.vue中的message对象调用
- * 此组件暴露一些方法，用于控制消息提示的显示，虽然这样破坏了vue的单向数据流，但是这样做可以减少很多重复代码
- * 并且全局理论上只会有一个SLMessages组件，所以这样做也不会有什么问题
- * @file: SLMessages.vue
- * @since: 2024-01-03 15:34:01
- **/
-import { ref } from 'vue'
-import { debounce } from '@swanlab-vue/utils/common'
-import { uuid } from '@swanlab-vue/utils/browser'
-import SLMessage from './SLMessage.vue'
-
-const messages = ref([])
-// 消息提示的最大数量，超过这个数量，添加新消息时会删除最早的消息
-const maxLength = 10
-
-/**
- * 添加一个消息提示
- * @param { String } message 消息信息
- * @param { Number } delay 消息显示的时间，单位为毫秒
- * @param { String } type 消息类型，可选值为error、success、processing
- * @param { Function } callback 消息被关闭时的回调函数，可选
- */
-const add = (message, type = 'error', delay = 2000, callback, theme = 'default') => {
-  const id = uuid()
-  // 生成一个函数callback，用于关闭该消息提示，并且将它传入BsMessage组件
-  const close = (id, callback) => () => {
-    remove(id, callback)
-  }
-  messages.value.push({
-    id,
-    text: message,
-    type,
-    delay,
-    close: close(id, callback),
-    theme
-  })
-  limitRemoveDebounce()
-}
-/**
- * 清空所有消息提示
- */
-const clear = () => {
-  messages.value = []
-}
-
-/**
- * 关闭一个消息提示
- * @param { String } id 消息提示的id
- */
-const remove = (id, callback) => {
-  // 找到该消息提示的索引
-  const index = messages.value.findIndex((item) => item.id === id)
-  // 如果找到了，就将该消息提示从messages中删除，没找到就不管了
-  if (index !== -1) {
-    messages.value.splice(index, 1)
-    // 如果有回调函数，就调用回调函数
-    callback && callback()
-  }
-}
-const limitRemoveDebounce = debounce(() => {
-  if (messages.value.length > maxLength) {
-    const m = messages.value[0]
-    m.close()
-  }
-}, 500)
-
-// ---------------------------------- 暴露给外部的方法 ----------------------------------
-
-defineExpose({
-  add,
-  clear
-})
-</script>
-
-<style lang="scss" scoped>
-.messages-container {
-  @apply fixed top-10 left-1/2 -translate-x-1/2 z-message;
-  @apply inline-block;
-  @apply flex flex-col gap-2;
-}
-
-// vue动画
-.list-enter-active,
-.list-leave-active {
-  transition: all 0.3s ease;
-}
-.list-enter-from,
-.list-leave-to {
-  opacity: 0;
-  height: 0;
-  transform: translateY(-30px);
-}
-</style>
diff --git a/vue/src/components/message/index.js b/vue/src/components/message/index.js
deleted file mode 100644
index e718ee98..00000000
--- a/vue/src/components/message/index.js
+++ /dev/null
@@ -1,61 +0,0 @@
-// ---------------------------------- Message Component,在此处写入组件的使用、注册、导出等信息 ----------------------------------
-
-import SLMessagesVue from './SLMessages.vue'
-export const message = {
-  // 用于存储组件实例
-  _r: null,
-  get _ref() {
-    if (!message._r) console.error('Message组件未注册')
-    return message._r.value
-  },
-  dark: {
-    error: (text, delay, onClose) => {
-      message._ref.add(text, 'error', delay, onClose, 'dark')
-    },
-    success: (text, delay, onClose) => {
-      message._ref.add(text, 'success', delay, onClose, 'dark')
-    },
-    warning: (text, delay, onClose) => {
-      message._ref.add(text, 'warning', delay, onClose, 'dark')
-    }
-  },
-  /**
-   * @description 显示错误消息
-   * @param {String} text 消息文本
-   * @param {Number} delay 延迟关闭时间
-   * @param {Function} onClose 关闭时的回调函数
-   */
-  error: (text, delay, onClose) => {
-    message._ref.add(text, 'error', delay, onClose)
-  },
-  /**
-   * @description 显示成功消息
-   * @param {String} text 消息文本
-   * @param {Number} delay 延迟关闭时间
-   * @param {Function} onClose 关闭时的回调函数
-   */
-  success: (text, delay, onClose) => {
-    message._ref.add(text, 'success', delay, onClose)
-  },
-  /**
-   * @description 显示警告消息
-   * @param {String} text 消息文本
-   * @param {Number} delay 延迟关闭时间
-   * @param {Function} onClose 关闭时的回调函数
-   */
-  warning: (text, delay, onClose) => {
-    message._ref.add(text, 'warning', delay, onClose)
-  },
-  /**
-   * 清空所有消息
-   */
-  clear: () => {
-    message._ref.clear()
-  }
-}
-
-export const installMessage = (ref) => {
-  message._r = ref
-}
-
-export const SLMessages = SLMessagesVue
diff --git a/vue/src/docs/chart.js b/vue/src/docs/chart.js
deleted file mode 100644
index c69e0e1e..00000000
--- a/vue/src/docs/chart.js
+++ /dev/null
@@ -1,64 +0,0 @@
-// 前端图表部分以数组排序，每个元素为一个group即section
-// section数组下标代表排序，下标越小排序越靠前
-// section内有charts字段，为数组，内包含一个个charts对象
-// 开源版对象类型以Open为前缀
-
-/**
- * @typedef {Object} OpenSection 一个节容器，存放图表，和一些默认情况
- * @property {number | string} id 平常不会使用，用于区别pin的section（-1）和hidden的section（-2），不影响排序，只影响名称显示；另一方面，将使用这个作为v-for时的key
- * @property {string} name section的名称，特别的，如果为default或者id为-1、-2，则按照前端定义的名称显示
- * @property {boolean | number} opened section是否展开，默认为true或者1
- * @property {OpenChart[]} charts 这个section内包含的图表
- */
-
-/**
- * @typedef {Object} OpenChart 一个图表配置，用于标注这个图表是什么类型以及配置是什么，使用了什么数据源
- * @property {number | string} id 图表唯一id，用于v-for时的key
- * @property {boolean} multi 标注是否为多实验图表显示，这涉及到图表组件内部不同的数据处理方式
- * @property {string} name 图表名称
- * @property {string} reference 参考系，目前默认为step
- * @property {string[]} source 数据源列表，这里是显示在图表上的数据名称
- * @property {Object} source_map 数据源映射，用于前端显示数据源的名称映射到数据源对应的实验id
- * @property {Object} error 当前图表是否错误，如果不为null说明图表错误，此信息用于显示错误
- * @property {string} error.data_class sdk中上传时的数据类型，用于前端提示
- */
-
-/**
- * @typedef {Object} OpenMetricData 一个指标包含的数据
- * @property {string | number} experiment_id 实验id
- * @property {OpenMetricDetail[]} list 数据列表
- */
-
-/**
- * @typedef {Object} OpenMetricDetail 一个指标包含的数据
- * @property {number} index 指标步数
- * @property {number | string | string[]} data 这一步的数据
- * @property {boolean} _last 是否为最后一条数据，最后一个数据设置为true即可，其他不需要设置
- */
-
-/**
- * @typedef {Object<string, OpenMetricData>} OpenChartData 传递给图表的数据
- */
-
-/**
- * @callback OpenChartSubscribe 图表本身向父组件传递数据的订阅函数
- * @param {string[]} sources 数据源名称，代表这个图表使用了哪些数据源
- * @param {string} chartId 图表id
- * @param {OpenChartSubscribeCallback} callback 订阅成功后的回调函数
- */
-
-/**
- * @callback OpenChartSubscribeCallback 图表本身向父组件传递数据的 订阅成功/数据更改 的回调函数，父组件将在订阅成功后调用这个函数
- * @param {string} key 数据源名称
- * @param {string} data 数据源的数据
- * @param {Object} error 请求失败的错误信息——如果有的话，没有就是null
- */
-
-// ---------------------------------- 图表api ----------------------------------
-
-/**
- * @callback setOriginalChartHeight 设置图表的原始高度，如果在执行前图表还没有被渲染，那么将在图表渲染后立即执行
- * @param {number} height 图表的高度, 单位px
- * @param {number} [maxHeight=800] 图表的最大高度, 单位px, 默认为800
- * @param {number} [minHeight=200] 图表的最小高度, 单位px, 默认为200
- */
diff --git a/vue/src/docs/index.js b/vue/src/docs/index.js
deleted file mode 100644
index b1127ce4..00000000
--- a/vue/src/docs/index.js
+++ /dev/null
@@ -1,1 +0,0 @@
-export * from './chart.js'
diff --git a/vue/src/i18n/en-US/chart.json5 b/vue/src/i18n/en-US/chart.json5
deleted file mode 100644
index 94d5290d..00000000
--- a/vue/src/i18n/en-US/chart.json5
+++ /dev/null
@@ -1,64 +0,0 @@
-{
-  charts: {
-    audio: {
-      error: 'The Chart cannot be displayed because the value of "{tag}" is type {type}, and Audio Chart do not support displaying type {type}.'
-    },
-    image: {
-      error: 'The Chart cannot be displayed because the value of "{tag}" is type {type}, and Image Chart do not support displaying type {type}.'
-    },
-    line: {
-      error: 'The Chart cannot be displayed because the value of "{tag}" is type {type}, and Line Chart do not support displaying type {type}.',
-      copy: {
-        windows: 'Press CTRL+C to copy this data.',
-        apple: 'Press CMD+C to copy this data.',
-        success: 'Copied!'
-      }
-    },
-    text: {
-      titles: {
-        tag: 'tag',
-        step: 'step',
-        caption: 'caption',
-        count: 'number of words',
-        text: 'Text'
-      }
-    },
-    share: {
-      step: 'Step',
-      tag: 'Tag',
-      time: 'Time',
-      value: 'Value',
-      smooth: {
-        smoothing: 'Smoothing',
-        method: 'Method'
-      },
-      download: {
-        button: 'Download',
-        png: {
-          download: 'Download image',
-          height: 'Height',
-          name: 'Name',
-          title: 'Export PNG',
-          width: 'Width'
-        }
-      }
-    }
-  },
-  empty: 'Empty Experiment with No Charts',
-  error: 'This chart is not displayed correctly.',
-  search: {
-    placeholder: 'Search Chart...'
-  },
-  label: {
-    default: 'Charts',
-    pinned: 'Pinned',
-    hidden: 'Hidden'
-  },
-  unkonwn: 'Unknown chart type: {type}',
-  zoom: 'Zoom In',
-  pin: 'Pin',
-  unpin: 'Unpin',
-  more: 'More actions',
-  hide: 'Hide',
-  unhide: 'Unhide'
-}
diff --git a/vue/src/i18n/en-US/common.json5 b/vue/src/i18n/en-US/common.json5
deleted file mode 100644
index 67cb004a..00000000
--- a/vue/src/i18n/en-US/common.json5
+++ /dev/null
@@ -1,55 +0,0 @@
-{
-  'config-editor': {
-    button: 'Edit',
-    'not-allowed': 'Not Allowed Since Is Running',
-    save: {
-      default: 'save changes',
-      savimg: 'saving',
-      success: 'Save Successfully'
-    },
-    'sub-title': {
-      experiment: {
-        desc: 'Description',
-        name: 'Experiment name'
-      },
-      project: {
-        desc: 'Description',
-        name: 'Project name'
-      }
-    },
-    title: {
-      experiment: 'Experiment Information',
-      project: 'Project Information'
-    }
-  },
-  copy: 'Copy successful!',
-  delete: {
-    button: 'Delete',
-    content: {
-      experiment: 'Once you delete a experiment, there is no going back. Please be certain.',
-      project: 'Once you delete a project, there is no going back. Please be certain.'
-    },
-    'not-allowed': 'Not Allowed Since Is Running',
-    title: {
-      experiment: 'Delete this experiment',
-      project: 'Delete this project'
-    }
-  },
-  search: {
-    placeholder: 'Search Lab...'
-  },
-  sidebar: {
-    experiments: {
-      running: 'running',
-      title: 'Experiments'
-    },
-    project: {
-      charts: 'Charts',
-      runs: 'Tables',
-      title: 'Project'
-    }
-  },
-  table: {
-    empty: 'Empty Table'
-  }
-}
diff --git a/vue/src/i18n/en-US/error.json5 b/vue/src/i18n/en-US/error.json5
deleted file mode 100644
index 772baaf6..00000000
--- a/vue/src/i18n/en-US/error.json5
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-  '404': {
-    message: 'Page not found',
-    return: 'Return to homepage'
-  },
-  'experiment-error': {
-    reasons: ['1. Corresponding log file not found', '2. Corresponding log file is damaged or cannot be opened'],
-    title: 'This experiment cannot be viewed'
-  },
-  'init-error': {
-    reasons: [
-      "1. You haven't written any data to the log file yet",
-      "2. SwanLab couldn't find your log file",
-      '3. Maybe this is an internal error of SwanLab'
-    ],
-    title: 'No available experiment dashboard in the current directory'
-  },
-  wrap: {
-    'error-code': 'Error code:',
-    message: [
-      'If you are a beginner with SwanLab and want to learn how to view training logs using SwanLab, you can refer to our ',
-      'official documentation'
-    ],
-    'reason-title': 'Possible reasons:',
-    time: 'Dashboard loading time: {time}'
-  }
-}
diff --git a/vue/src/i18n/en-US/experiment.json5 b/vue/src/i18n/en-US/experiment.json5
deleted file mode 100644
index 0a79491e..00000000
--- a/vue/src/i18n/en-US/experiment.json5
+++ /dev/null
@@ -1,112 +0,0 @@
-{
-  navs: {
-    index: 'Experiment Card',
-    chart: 'Chart',
-    log: 'Logs',
-    env: 'Environment'
-  },
-  status: {
-    '-1': 'Crashed',
-    '0': 'Running',
-    '1': 'Finished'
-  },
-  index: {
-    header: {
-      'table-bar': {
-        placeholder: 'Search experiments',
-        summary: 'Only summary',
-        export: 'Export'
-      },
-      experiment_infos: {
-        status: 'State',
-        start_time: 'Start Time',
-        last_time: 'Duration',
-        version: 'SwanLab Version',
-        git: 'Git Repository',
-        tags: 'Tags',
-        python: 'Python Version',
-        time: {
-          day: 'd ',
-          hour: 'h ',
-          minute: 'min ',
-          second: 's '
-        }
-      },
-      experiment_device: {
-        hostname: 'Hostname',
-        os: 'OS',
-        python: 'Python Version',
-        executable: 'Python executable',
-        hardware: 'System Hardware',
-        cpu: 'CPU Count: {value}',
-        gpu: 'GPU Count: {value}',
-        type: 'GPU Type: {value}'
-      },
-      stop: {
-        button: 'Confirm',
-        modal: {
-          title: 'Has this experiment been stopped?',
-          text: "Once confirmed, the state will be set to 'Crashed', and stop receiving data."
-        }
-      }
-    },
-    config: {
-      title: 'Configuration Information',
-      detail: 'Config',
-      summarize: 'Summary',
-      help: {
-        config: "The Configuration parameter describes the input options of your model. Configure it in the 'config' parameter of swanlab.init. Click the icon to learn more.",
-        summary: 'The Summary parameter describes the final results of the metrics you record. Click the icon to learn more.'
-      },
-      table: {
-        empty: 'No Data',
-        search: 'Search...'
-      }
-    }
-  },
-  env: {
-    navs: {
-      index: 'Overview',
-      hardware: 'System Hardware',
-      requirements: 'Requirements'
-    },
-    title: {
-      exp_env_index: 'Overview',
-      exp_env_hardware: 'System Hardware',
-      exp_env_dependencies: 'Requirements'
-    },
-    keys: {
-      name: 'Experiment name',
-      description: 'Description',
-      start_time: 'Start Time',
-      duration: 'Duration',
-      python: 'Python Version',
-      executable: 'Python executable',
-      workspace: 'Run path',
-      command: 'Command',
-      hostname: 'Hostname',
-      OS: 'OS',
-      git_remote: 'Git Repository',
-      git_branch: 'Git Branch',
-      git_commit: 'Git Commit',
-      cpu: 'CPU count',
-      memory: 'Memory',
-      gpu_cores: 'GPU count',
-      gpu_type: 'GPU type',
-      logdir: 'Logdir',
-      swanlab: 'SwanLab Version'
-    },
-    empty: {
-      requirements: 'No requirements found'
-    }
-  },
-  'func-bar': {
-    copy: 'Copy',
-    download: 'Download',
-    'copy-success': 'Copied',
-    placeholder: {
-      requirements: 'Search requirement...',
-      log: 'Search log...'
-    }
-  }
-}
diff --git a/vue/src/i18n/en-US/home.json5 b/vue/src/i18n/en-US/home.json5
deleted file mode 100644
index 6368fe8a..00000000
--- a/vue/src/i18n/en-US/home.json5
+++ /dev/null
@@ -1,22 +0,0 @@
-{
-  overview: {
-    create: 'Created',
-    latest: 'Latest Run',
-    total: 'Total Experiments',
-    logdir: 'Logdir'
-  },
-  list: {
-    title: 'Experiments',
-    table: {
-      header: {
-        name: 'Name',
-        status: 'State',
-        create: 'Created',
-        duration: 'Runtime'
-      }
-    }
-  },
-  empty: {
-    table: 'There are no experiments, you can create one by importing SwanLog in your code.'
-  }
-}
diff --git a/vue/src/i18n/en-US/index.js b/vue/src/i18n/en-US/index.js
deleted file mode 100644
index 67dd3643..00000000
--- a/vue/src/i18n/en-US/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import sider from './sider.json5'
-import home from './home.json5'
-import experiment from './experiment.json5'
-import moment from './moment.json5'
-import common from './common.json5'
-import error from './error.json5'
-import chart from './chart.json5'
-
-export default {
-  chart,
-  sider,
-  home,
-  experiment,
-  moment,
-  common,
-  error
-}
diff --git a/vue/src/i18n/en-US/moment.json5 b/vue/src/i18n/en-US/moment.json5
deleted file mode 100644
index 89c3f14c..00000000
--- a/vue/src/i18n/en-US/moment.json5
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-  relativeTime: {
-    future: '%s from now',
-    past: '%s ago',
-    s: 'a few seconds',
-    ss: '%d seconds',
-    m: '1 min',
-    mm: '%d min',
-    h: '1 hour',
-    hh: '%d hours',
-    d: '1 day',
-    dd: '%d days',
-    w: '1 week',
-    ww: '%d weeks',
-    M: '1 month',
-    MM: '%d months',
-    y: '1 year',
-    yy: '%d years'
-  }
-}
diff --git a/vue/src/i18n/en-US/sider.json5 b/vue/src/i18n/en-US/sider.json5
deleted file mode 100644
index c7b37ba7..00000000
--- a/vue/src/i18n/en-US/sider.json5
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  version: {
-    version: 'Current Version: v{version}, View',
-    updates: 'Release Notes'
-  },
-  nav: {
-    home: 'Overview',
-    search: 'Search Experiments...',
-    experiment: 'Experiment',
-    help: 'Help'
-  }
-}
diff --git a/vue/src/i18n/index.js b/vue/src/i18n/index.js
deleted file mode 100644
index 9d8dac14..00000000
--- a/vue/src/i18n/index.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import zhCN from './zh-CN'
-import en from './en-US'
-import { createI18n } from 'vue-i18n'
-
-export const i18n = createI18n({
-  locale: 'en', // 设置地区
-  legacy: false, // 如果要支持compositionAPI，此项必须设置为false
-  globalInjection: true, // 全局注册$t方法
-  messages: {
-    'zh-CN': zhCN,
-    en
-  }
-})
-
-export const { t, te, tm } = i18n.global
diff --git a/vue/src/i18n/zh-CN/chart.json5 b/vue/src/i18n/zh-CN/chart.json5
deleted file mode 100644
index 3d25ecb1..00000000
--- a/vue/src/i18n/zh-CN/chart.json5
+++ /dev/null
@@ -1,64 +0,0 @@
-{
-  charts: {
-    audio: {
-      error: '图表无法被显示，因为"{tag}"是 {type} 类型, 但是音频图并不支持此类型.'
-    },
-    image: {
-      error: '图表无法被显示，因为"{tag}"是 {type} 类型, 但是图片图并不支持此类型.'
-    },
-    line: {
-      copy: {
-        apple: '按下CMD+C拷贝此数据',
-        success: '已复制！',
-        windows: '按下CTRL+C拷贝此数据'
-      },
-      error: '图表无法被显示，因为"{tag}"是 {type} 类型, 但是折线图并不支持此类型.'
-    },
-    share: {
-      smooth: {
-        method: '算法',
-        smoothing: '平滑'
-      },
-      step: '步数',
-      tag: '标签',
-      time: '时间',
-      value: '值',
-      download: {
-        button: '下载',
-        png: {
-          title: '导出为PNG',
-          name: '名称',
-          width: '宽度',
-          height: '高度',
-          download: '下载图像'
-        }
-      }
-    },
-    text: {
-      titles: {
-        caption: '说明',
-        count: '字数',
-        step: '步数',
-        tag: '标签',
-        text: '文本'
-      }
-    }
-  },
-  empty: '缺少实验数据',
-  error: '此图表无法正确显示',
-  search: {
-    placeholder: '搜索图表...'
-  },
-  label: {
-    default: '图表',
-    pinned: '置顶图表',
-    hidden: '隐藏图表'
-  },
-  unkonwn: '未知的图表类型: {type}',
-  zoom: '放大展示',
-  pin: '置顶',
-  unpin: '取消置顶',
-  more: '更多操作',
-  hide: '隐藏',
-  unhide: '取消隐藏'
-}
diff --git a/vue/src/i18n/zh-CN/common.json5 b/vue/src/i18n/zh-CN/common.json5
deleted file mode 100644
index 57a3f7ef..00000000
--- a/vue/src/i18n/zh-CN/common.json5
+++ /dev/null
@@ -1,55 +0,0 @@
-{
-  'config-editor': {
-    button: 'Edit',
-    'not-allowed': 'Not Allowed Since Is Running',
-    save: {
-      default: 'save changes',
-      savimg: 'saving',
-      success: 'Save Successfully'
-    },
-    'sub-title': {
-      experiment: {
-        desc: 'Description',
-        name: 'Experiment name'
-      },
-      project: {
-        desc: 'Description',
-        name: 'Project name'
-      }
-    },
-    title: {
-      experiment: 'Modify Experiment Information',
-      project: 'Modify Project Information'
-    }
-  },
-  copy: '复制成功！',
-  delete: {
-    button: 'Delete',
-    content: {
-      experiment: '一旦你删除了一个实验，就没有回头路了。请确认一下。',
-      project: '一旦你删除了一个项目，就没有回头路了。一定要确定。'
-    },
-    'not-allowed': 'Not Allowed Since Is Running',
-    title: {
-      experiment: '删除这个实验',
-      project: '删除这个项目'
-    }
-  },
-  search: {
-    placeholder: 'Search Lab...'
-  },
-  sidebar: {
-    experiments: {
-      running: '运行中',
-      title: '实验'
-    },
-    project: {
-      charts: '图表',
-      runs: '工作区',
-      title: '项目'
-    }
-  },
-  table: {
-    empty: '暂无数据'
-  }
-}
diff --git a/vue/src/i18n/zh-CN/error.json5 b/vue/src/i18n/zh-CN/error.json5
deleted file mode 100644
index 3acd713f..00000000
--- a/vue/src/i18n/zh-CN/error.json5
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-  '404': {
-    message: '没有找到页面',
-    return: '回到首页'
-  },
-  'experiment-error': {
-    reasons: ['1. 未找到对应的日志文件', '2. 对应的日志文件损坏或无法被打开'],
-    title: '此实验无法被查看'
-  },
-  'init-error': {
-    reasons: ['1. 你还没有在日志文件中写入任何数据', '2. SwanLab没有找到你的日志文件', '3.这可能是SwanLab的内部错误'],
-    title: '当前目录下没有可用的实验看板'
-  },
-  wrap: {
-    'error-code': '错误码：',
-    message: ['如果你是SwanLab的初学者，并且想了解如何使用SwanLab查看训练日志，可以查看我们的', '官方文档'],
-    'reason-title': '可能的原因：',
-    time: '加载看板时间：{time}'
-  }
-}
diff --git a/vue/src/i18n/zh-CN/experiment.json5 b/vue/src/i18n/zh-CN/experiment.json5
deleted file mode 100644
index 01e32f6a..00000000
--- a/vue/src/i18n/zh-CN/experiment.json5
+++ /dev/null
@@ -1,112 +0,0 @@
-{
-  env: {
-    empty: {
-      requirements: 'No requirements found'
-    },
-    keys: {
-      OS: 'OS',
-      command: 'Command',
-      cpu: 'CPU count',
-      description: 'Description',
-      duration: 'Duration',
-      executable: 'Python executable',
-      git_branch: 'Git Branch',
-      git_commit: 'Git Commit',
-      git_remote: 'Git Repository',
-      gpu_cores: 'GPU count',
-      gpu_type: 'GPU type',
-      hostname: 'Hostname',
-      logdir: 'Logdir',
-      memory: 'Memory',
-      name: 'Experiment name',
-      python: 'Python Version',
-      start_time: 'Start Time',
-      swanlab: 'SwanLab Version',
-      workspace: 'Run path'
-    },
-    navs: {
-      hardware: 'System Hardware',
-      index: 'Overview',
-      requirements: 'Requirements'
-    },
-    title: {
-      exp_env_dependencies: 'Requirements',
-      exp_env_hardware: 'System Hardware',
-      exp_env_index: 'Overview'
-    }
-  },
-  'func-bar': {
-    copy: 'Copy',
-    'copy-success': 'Copied',
-    download: 'Download',
-    placeholder: {
-      log: 'Search log...',
-      requirements: 'Search requirement...'
-    }
-  },
-  index: {
-    config: {
-      detail: '配置',
-      help: {
-        config: '配置（Config）参数描述你的模型的输入项，在swanlab.init的config参数中配置，点击图标了解更多。',
-        summary: '摘要（Summary）参数描述你记录的指标的最终结果，点击图标了解更多。'
-      },
-      summarize: '摘要',
-      table: {
-        empty: 'No Data',
-        search: 'Search...'
-      },
-      title: '配置信息'
-    },
-    header: {
-      experiment_device: {
-        cpu: 'CPU 核心数: {value}',
-        executable: 'Python可执行文件',
-        gpu: 'GPU 核心数: {value}',
-        hardware: '系统硬件',
-        hostname: 'Hostname',
-        os: '操作系统',
-        python: 'Python版本',
-        type: 'GPU类型：{Value}'
-      },
-      experiment_infos: {
-        git: 'Git存储库',
-        last_time: '持续时间',
-        python: 'Python版本',
-        start_time: '开始时间',
-        status: '状态',
-        tags: 'Tag',
-        time: {
-          day: '天',
-          hour: '小时',
-          minute: '分钟',
-          second: '秒'
-        },
-        version: 'SwanLab 版本号'
-      },
-      stop: {
-        button: 'Confirm',
-        modal: {
-          text: "Once confirmed, the state will be set to 'Crashed', and stop receiving data.",
-          title: 'Has this experiment been stopped?'
-        }
-      },
-      'table-bar': {
-        export: 'Export to CSV',
-        placeholder: 'Search experiments',
-        summary: 'Only summary'
-      }
-    }
-  },
-  navs: {
-    chart: '实验图表',
-    env: '实验环境',
-    index: '概览',
-    log: '日志Log'
-  },
-  status: {
-    '-1': '中断',
-    '0': '正在进行',
-    '1': '完成'
-  }
-}
diff --git a/vue/src/i18n/zh-CN/home.json5 b/vue/src/i18n/zh-CN/home.json5
deleted file mode 100644
index a29631dd..00000000
--- a/vue/src/i18n/zh-CN/home.json5
+++ /dev/null
@@ -1,22 +0,0 @@
-{
-  overview: {
-    create: '创建时间',
-    latest: '最近运行时间',
-    total: '总实验数',
-    logdir: '存储路径'
-  },
-  list: {
-    title: '实验列表',
-    table: {
-      header: {
-        name: '实验名',
-        status: '状态',
-        create: '创建时间',
-        duration: 'Runtime'
-      }
-    }
-  },
-  empty: {
-    table: 'There are no experiments, you can create one by importing SwanLog in your code'
-  }
-}
diff --git a/vue/src/i18n/zh-CN/index.js b/vue/src/i18n/zh-CN/index.js
deleted file mode 100644
index 67dd3643..00000000
--- a/vue/src/i18n/zh-CN/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import sider from './sider.json5'
-import home from './home.json5'
-import experiment from './experiment.json5'
-import moment from './moment.json5'
-import common from './common.json5'
-import error from './error.json5'
-import chart from './chart.json5'
-
-export default {
-  chart,
-  sider,
-  home,
-  experiment,
-  moment,
-  common,
-  error
-}
diff --git a/vue/src/i18n/zh-CN/moment.json5 b/vue/src/i18n/zh-CN/moment.json5
deleted file mode 100644
index fadb8bef..00000000
--- a/vue/src/i18n/zh-CN/moment.json5
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-  relativeTime: {
-    future: '%s之前',
-    past: '%s前',
-    s: '几秒钟',
-    ss: '%d秒',
-    m: '1分钟',
-    mm: '%d分钟',
-    h: '1小时',
-    hh: '%d小时',
-    d: '1天',
-    dd: '%d天',
-    w: '1周',
-    ww: '%d周',
-    M: '1个月',
-    MM: '%d个月',
-    y: '1年',
-    yy: '%d年'
-  }
-}
diff --git a/vue/src/i18n/zh-CN/sider.json5 b/vue/src/i18n/zh-CN/sider.json5
deleted file mode 100644
index eb860440..00000000
--- a/vue/src/i18n/zh-CN/sider.json5
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  version: {
-    version: '当前版本：v{version}，查看',
-    updates: '更新日志'
-  },
-  nav: {
-    home: '概览',
-    search: '搜索实验...',
-    experiment: '实验',
-    help: '帮助'
-  }
-}
diff --git a/vue/src/layouts/ErrorLayout.vue b/vue/src/layouts/ErrorLayout.vue
deleted file mode 100644
index bd0c14ec..00000000
--- a/vue/src/layouts/ErrorLayout.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-<template>
-  <div class="w-full flex justify-center pt-8 px-6">
-    <div class="max-w-[640px] px-6 py-7 w-full border rounded-lg bg-default">
-      <slot></slot>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 错误界面
- * @file: ErrorLayout.vue
- * @since: 2023-12-14 14:40:41
- **/
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/layouts/ExperimentLayout.vue b/vue/src/layouts/ExperimentLayout.vue
deleted file mode 100644
index fc83745e..00000000
--- a/vue/src/layouts/ExperimentLayout.vue
+++ /dev/null
@@ -1,159 +0,0 @@
-<template>
-  <div class="w-full h-full py-5">
-    <!-- 导航栏 -->
-    <div class="px-6 border-b">
-      <!-- 第一行内容，项目标题、实验标题、编辑按钮、删除按钮 -->
-      <div class="experiment-title transition-marging duration-300">
-        <div class="flex items-center gap-3">
-          <!-- 项目标题/实验标题 -->
-          <h1 class="text-2xl items-center gap-1 truncate max-w-sm sm:max-w-lg 2xl:max-w-5xl">
-            <span class="font-semibold">{{ experimentStore.name }}</span>
-          </h1>
-          <!-- 编辑按钮 -->
-          <ConfigEditor type="experiment" @modify="modifyExperiment" :disabled="experimentStore.isRunning" />
-          <!-- 实验状态 -->
-          <SLStatusLabel :name="experiment.name" :status="experiment.status">
-            {{ $t('experiment.status.' + experiment.status) }}
-          </SLStatusLabel>
-          <slot name="stop-button" v-if="experimentStore.isRunning"></slot>
-        </div>
-        <!-- 删除按钮 -->
-        <div class="flex justify-end grow transition-padding duration-300 ml-1">
-          <DeleteButton type="experiment" @confirm="deleteExperiment" />
-        </div>
-      </div>
-      <!-- 第二行内容，实验描述 -->
-      <p class="experiment-description" v-if="experimentStore.description">
-        {{ experimentStore.description }}
-      </p>
-      <!-- 第三行内容，导航标签 -->
-      <nav class="experiment-navs">
-        <RouterLink
-          class="nav-item"
-          active-class="nav-active"
-          :data-text="nav.label"
-          v-for="nav in navs"
-          :key="nav.to"
-          :to="nav.to"
-        >
-          <SLIcon :icon="nav.icon" class="w-5 h-5" />
-          {{ nav.label }}
-        </RouterLink>
-      </nav>
-    </div>
-    <slot></slot>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 实验页布局（不含左侧侧边栏）
- * @file: ExperimentLayout.vue
- * @since: 2023-12-09 20:22:32
- **/
-import ConfigEditor from '@swanlab-vue/business/config-editor/ConfigEditor.vue'
-import DeleteButton from '@swanlab-vue/business/config-editor/DeleteButton.vue'
-import http from '@swanlab-vue/api/http'
-import { useProjectStore, useExperimentStore } from '@swanlab-vue/store'
-import { useRouter } from 'vue-router'
-import { message } from '@swanlab-vue/components/message'
-import { t } from '@swanlab-vue/i18n'
-import { ref } from 'vue'
-
-const router = useRouter()
-const projectStore = useProjectStore()
-const experimentStore = useExperimentStore()
-const experiment = ref(experimentStore.experiment)
-
-// ---------------------------------- 删除实验 ----------------------------------
-const deleteExperiment = () => {
-  http
-    .delete(`/experiment/${experimentStore.id}`)
-    .then(({ data }) => {
-      projectStore.deleteExperiment(data.experiment_id)
-      router.replace('/').then(() => {
-        message.success('Delete Successfully')
-      })
-    })
-    .catch(({ data }) => {
-      message.error(data.message)
-    })
-}
-
-// ---------------------------------- 修改实验信息 ----------------------------------
-
-const modifyExperiment = async (newV, hideModal) => {
-  const id = experimentStore.id
-  const { data } = await http.patch(`/experiment/${id}`, newV)
-  data.name = data.name.trim()
-  experimentStore.setExperimentPartial(data)
-  projectStore.setExperimentInfo(id, newV)
-  hideModal()
-}
-
-// ---------------------------------- 导航标签配置 ----------------------------------
-const navs = [
-  {
-    label: t('experiment.navs.index'),
-    to: `/experiment/${experimentStore.id}/index`,
-    icon: 'experiment'
-  },
-  {
-    label: t('experiment.navs.chart'),
-    to: `/experiment/${experimentStore.id}/chart`,
-    icon: 'chart'
-  },
-  {
-    label: t('experiment.navs.log'),
-    to: `/experiment/${experimentStore.id}/log`,
-    icon: 'logs'
-  },
-  {
-    label: t('experiment.navs.env'),
-    to: `/experiment/${experimentStore.id}/env`,
-    icon: 'info'
-  }
-]
-</script>
-
-<style scoped lang="scss">
-.experiment-title {
-  @apply flex items-center w-full overflow-x-auto overflow-y-visible;
-  // 隐藏滚动条
-  &::-webkit-scrollbar {
-    display: none;
-  }
-}
-.experiment-description {
-  @apply mt-3 w-full break-words text-sm text-dimmer;
-  display: -webkit-box;
-  -webkit-box-orient: vertical;
-  overflow: hidden;
-  -webkit-line-clamp: 2; /* 设置为希望显示的最大行数 */
-}
-
-.experiment-navs {
-  @apply flex items-center gap-8 mt-3 w-full overflow-x-auto overflow-y-visible;
-  // 隐藏滚动条
-  &::-webkit-scrollbar {
-    display: none;
-  }
-  .nav-item {
-    @apply px-1 pt-2 pb-1.5 relative text-dimmer whitespace-nowrap ring-0 outline-none;
-    @apply mb-1 rounded flex items-center gap-1.5;
-    &:hover {
-      @apply bg-higher;
-    }
-  }
-  .nav-active {
-    @apply text-default font-semibold;
-    &:hover {
-      background-color: transparent !important;
-    }
-    &:after {
-      @apply w-full h-0.5 bg-positive-higher absolute -bottom-1 left-1/2 -translate-x-1/2 z-10;
-      content: '';
-    }
-  }
-}
-</style>
diff --git a/vue/src/layouts/HomeLayout.vue b/vue/src/layouts/HomeLayout.vue
deleted file mode 100644
index 7560d062..00000000
--- a/vue/src/layouts/HomeLayout.vue
+++ /dev/null
@@ -1,102 +0,0 @@
-<template>
-  <div class="w-full h-full pt-5 flex flex-col">
-    <!-- 项目标题部分 -->
-    <div class="px-6 border-b flex-shrink-0">
-      <!-- 第一行内容，项目标题、实验标题、编辑按钮、删除按钮 -->
-      <div class="project-title transition-marging duration-300">
-        <div class="flex items-center gap-3">
-          <!-- 项目标题/实验标题 -->
-          <h1 class="text-2xl items-center gap-1 font-semibold max-w-md truncate">
-            {{ projectStore.name }}
-          </h1>
-          <!-- 编辑按钮 -->
-          <ConfigEditor type="project" @modify="modifyProject" />
-        </div>
-        <!-- 删除按钮 -->
-        <div class="flex justify-end grow transition-padding duration-300 ml-1">
-          <DeleteButton type="project" @confirm="deleteProject" :disabled="hasRunning" />
-        </div>
-      </div>
-      <!-- 第二行内容，项目描述 -->
-      <p class="project-description" v-if="projectStore.description">
-        {{ projectStore.description }}
-      </p>
-      <!-- 其他内容，插槽 -->
-      <slot name="top"></slot>
-    </div>
-    <!-- 下面的内容 -->
-    <slot></slot>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 首页布局，主要针对首页的title和内容进行布局
- * @file: HomeLayout.vue
- * @since: 2024-01-09 16:33:56
- **/
-import { computed } from 'vue'
-import { useProjectStore } from '@swanlab-vue/store'
-import http from '@swanlab-vue/api/http'
-import { message } from '@swanlab-vue/components/message'
-import ConfigEditor from '@swanlab-vue/business/config-editor/ConfigEditor.vue'
-import DeleteButton from '@swanlab-vue/business/config-editor/DeleteButton.vue'
-const projectStore = useProjectStore()
-// ---------------------------------- 修改项目信息 ----------------------------------
-
-const modifyProject = async (newV, hideModal) => {
-  const { data } = await http.patch('/project/update', newV)
-  projectStore.updateInfo(data.updates)
-  hideModal()
-}
-
-// ---------------------------------- 删除项目 ----------------------------------
-
-/**
- * 是否可以删除项目
- * 如果有实验正在进行中，直接不显示删除按钮
- */
-const hasRunning = computed(() => {
-  for (const experiment of projectStore.experiments) {
-    if (experiment.status === 0) return true
-  }
-  return false
-})
-
-/**
- * 删除项目
- *
- * success：显示成功，清空状态，并显示空项目错误页
- *
- * fail：在有实验正在进行的时候删除项目
- */
-const deleteProject = () => {
-  http
-    .delete('/project')
-    .then(() => {
-      message.success('Delete Successfully')
-      location.reload()
-    })
-    .catch(() => {
-      message.error('Error deleting project')
-    })
-}
-</script>
-
-<style lang="scss" scoped>
-.project-title {
-  @apply flex items-center w-full overflow-x-auto;
-  // 隐藏滚动条
-  &::-webkit-scrollbar {
-    display: none;
-  }
-}
-
-.project-description {
-  @apply mt-3.5 w-full break-words text-sm text-dimmer;
-  display: -webkit-box;
-  -webkit-box-orient: vertical;
-  overflow: hidden;
-  -webkit-line-clamp: 2; /* 设置为希望显示的最大行数 */
-}
-</style>
diff --git a/vue/src/layouts/main/MainLayout.vue b/vue/src/layouts/main/MainLayout.vue
deleted file mode 100644
index f89e384c..00000000
--- a/vue/src/layouts/main/MainLayout.vue
+++ /dev/null
@@ -1,254 +0,0 @@
-<template>
-  <section class="w-screen h-screen overflow-x-clip">
-    <!-- 侧边栏关闭/开启按钮 -->
-    <button class="close-button" ref="cbRef" @click="handleClose" v-if="showSideBar">
-      <SLIcon icon="sidebar" class="w-full h-full" />
-    </button>
-    <!-- 顶部header -->
-    <header class="h-14 w-full">
-      <HeaderBar :version="version" />
-    </header>
-    <!-- 下半部分主要内容，增加一个relative-container的原因是解决按钮在滑动时的隐藏问题 -->
-    <main class="main-container">
-      <!-- 遮罩，淡入淡出 -->
-      <transition name="fade" v-if="showSideBar">
-        <div class="md:hidden sidebar-overlay" v-if="isSideBarShow" @click="handleClose"></div>
-      </transition>
-      <!-- 侧边栏 -->
-      <div class="sidebar-container bg-default" ref="sidebarRef" v-if="showSideBar">
-        <!-- 侧边栏规定宽度 -->
-        <div class="sidebar-content">
-          <SideBar />
-        </div>
-      </div>
-      <!-- 右侧主要内容 -->
-      <div class="main-content border-l" ref="containerRef">
-        <slot></slot>
-      </div>
-    </main>
-  </section>
-</template>
-<script setup>
-/**
- * MainContentLayout - 页面布局，包含sidebar和右侧部分，用于展示页面内容
- * 右侧部分可以通过传入的props定义是否显示
- */
-import { ref, watch, onMounted, provide, computed } from 'vue'
-import HeaderBar from './components/HeaderBar.vue'
-import SideBar from './components/SideBar.vue'
-import { useRoute } from 'vue-router'
-const props = defineProps({
-  version: {
-    type: String,
-    default: 'unknown'
-  },
-  showSideBar: {
-    type: Boolean,
-    default: true
-  }
-})
-
-// ---------------------------------- 开启/关闭sidebar ----------------------------------
-
-const cbRef = ref(null)
-/**
- * 是否是小屏幕，这意味着isSideBarShow在初始化的时候是否为false，如果为false，那么侧边栏将不会显示
- * 后续考虑改为响应式
- */
-const threshold = 768
-const isSmallScreen = ref(window.innerWidth < threshold)
-window.addEventListener('resize', () => {
-  isSmallScreen.value = window.innerWidth < threshold
-})
-
-// 是否显示主布局的sideBar，可以监听，主要用于实现MainContent和MainHeader之间的交互动画
-const isSideBarShow = ref(!isSmallScreen.value)
-// 注入isSideBarShow，内部插槽可以通过inject('isSideBarShow')获取到isSideBarShow的值，但是不可以修改
-provide(
-  'isSideBarShow',
-  computed(() => isSideBarShow.value)
-)
-
-// 如果需要修改，通过下面注入的openSideBar和closeSideBar方法完成，这两个方法如果被内部组件调用，自动检测是否是小屏幕，如果不是小屏幕，不执行任何操作
-provide('openSideBar', () => {
-  if (isSmallScreen.value) {
-    isSideBarShow.value = true
-  }
-})
-provide('closeSideBar', () => {
-  if (isSmallScreen.value) {
-    isSideBarShow.value = false
-  }
-})
-
-// 关闭按钮点击事件
-const handleClose = () => {
-  isSideBarShow.value = !isSideBarShow.value
-}
-
-// ---------------------------------- 其他对象 ----------------------------------
-
-const sidebarRef = ref(null)
-const containerRef = ref(null)
-
-// ---------------------------------- 注册挂载钩子 ----------------------------------
-
-onMounted(() => {
-  watch(
-    isSideBarShow,
-    (val) => {
-      if (!props.showSideBar) return
-      // 显示
-      if (val) {
-        sidebarRef.value.style = 'width: 288px;'
-        cbRef.value.classList.remove('close-button-sidebar-close')
-        cbRef.value.classList.add('close-button-sidebar-open')
-      } else {
-        sidebarRef.value.style = 'width: 0;'
-        cbRef.value.classList.remove('close-button-sidebar-open')
-        cbRef.value.classList.add('close-button-sidebar-close')
-      }
-    },
-    {
-      immediate: true
-    }
-  )
-})
-
-// ---------------------------------- 路由更改时，判断是否是小屏幕，如果是，隐藏sidebar ----------------------------------
-
-const route = useRoute()
-watch(
-  computed(() => route.fullPath),
-  () => {
-    if (isSmallScreen.value) {
-      isSideBarShow.value = false
-    }
-  }
-)
-// ---------------------------------- 暴露对象 ----------------------------------
-
-defineExpose({
-  containerRef
-})
-</script>
-
-<style lang="scss" scoped>
-$open-top: 72px;
-$close-top: 17px;
-
-// 动画持续时间
-$duration: 400ms;
-
-// 关闭按钮
-$close-button-size: 24px;
-
-// 侧边栏宽度，需要与上面js中设置的宽度一致
-$sidebar-width: 288px;
-
-// 除去header的高度
-$main-content-height: calc(100vh - 56px);
-
-.relative-container {
-  @apply relative overflow-y-hidden;
-  height: $main-content-height;
-}
-.main-container {
-  @apply w-screen flex  overflow-auto;
-  height: $main-content-height;
-
-  .main-content {
-    @apply h-full w-full overflow-y-auto overflow-x-hidden;
-    // 宽度变化
-    transition: width $duration ease-in-out;
-  }
-}
-
-.close-button {
-  @apply absolute z-full outline-none border-none rounded p-1 bg-transparent;
-  transition-duration: $duration;
-  transition-timing-function: ease-in-out;
-  transition-property: top, color, transform, background-color;
-  width: $close-button-size;
-  height: $close-button-size;
-  left: calc($sidebar-width - $close-button-size - 16px);
-}
-
-// sidebar被关闭
-.close-button-sidebar-close {
-  top: $close-top;
-  color: var(--accent-white-highest);
-  transform: translateX(-108px) rotateY(180deg);
-  &:hover {
-    @apply text-white-default;
-  }
-}
-
-// sidebar开启时
-.close-button-sidebar-open {
-  top: $open-top;
-  &:hover {
-    @apply bg-highest;
-  }
-}
-
-// 定义一个动画，$duration秒之内不透明度从1到0再到1
-@keyframes closeOpen {
-  0% {
-    opacity: 1;
-  }
-  50% {
-    opacity: 0;
-  }
-  100% {
-    opacity: 1;
-  }
-}
-
-.close-button-enter-active,
-.close-button-leave-active {
-  animation: closeOpen $duration ease-in-out;
-}
-
-// 遮罩
-.sidebar-overlay {
-  @apply absolute w-full  overflow-x-hidden z-40;
-  height: $main-content-height;
-  background-color: var(--background-overlay);
-  animation: fadeIn $duration ease;
-}
-// sidebar容器
-.sidebar-container {
-  @apply z-50 box-content  md:h-full overflow-y-auto overflow-x-clip flex-shrink-0 md:static absolute;
-  height: $main-content-height;
-  // 宽度变化
-  transition: width $duration ease-in-out;
-  // 将滚动条隐藏
-  &::-webkit-scrollbar {
-    display: none; /* 隐藏WebKit浏览器的滚动条 */
-  }
-
-  .sidebar-content {
-    @apply h-full;
-    width: $sidebar-width;
-  }
-}
-
-// 淡出
-.fade-leave-active {
-  transition: opacity 0.5s;
-}
-.fade-leave-to {
-  opacity: 0;
-}
-
-// 淡入
-@keyframes fadeIn {
-  from {
-    opacity: 0; /* 从完全透明开始 */
-  }
-  to {
-    opacity: 1; /* 渐变到完全不透明 */
-  }
-}
-</style>
diff --git a/vue/src/layouts/main/components/HeaderBar.vue b/vue/src/layouts/main/components/HeaderBar.vue
deleted file mode 100644
index d063ec9f..00000000
--- a/vue/src/layouts/main/components/HeaderBar.vue
+++ /dev/null
@@ -1,92 +0,0 @@
-<template>
-  <div class="w-full h-full bg-dimmest text-dimmest flex justify-between items-center px-6">
-    <!-- logo and version -->
-    <div class="flex items-center gap-1.5 hover:cursor-pointer" @click="$router.push('/')">
-      <!-- icon -->
-      <HeaderIcon />
-      <!-- version -->
-      <div class="flex items-end">
-        <span class="font-semibold mr-0.5">SwanLab</span>
-        <!-- 版本号被注释了 -->
-        <!-- <span class="whitespace-nowrap text-xs pl-2 text-dimmer"> {{ formatVersion(version) }}</span> -->
-      </div>
-    </div>
-    <!-- links -->
-    <div class="w-full grow px-8 items-center font-semibold justify-end gap-8 md:flex hidden">
-      <a :href="item.link" target="_blank" class="hover:text-white-higher" v-for="item in links" :key="item.link">
-        {{ item.title }}
-      </a>
-    </div>
-    <!-- fixeds -->
-    <div class="flex items-center font-semibold">
-      <a
-        :href="item.link"
-        target="_blank"
-        class="flex gap-1.5 items-center h-full text-dimmest hover:text-white-higher"
-        v-for="item in fixeds"
-        :key="item.icon"
-      >
-        <SLIcon :icon="item.icon" class="h-8 w-8" />
-        <!-- {{ item.title }} -->
-      </a>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 顶部页头
- * @file: PageHeader.vue
- * @since: 2024-01-09 11:13:20
- **/
-
-import { ref } from 'vue'
-import HeaderIcon from './HeaderIcon.vue'
-import SLIcon from '@swanlab-vue/components/SLIcon.vue'
-
-defineProps({
-  version: {
-    type: String,
-    default: 'unknown'
-  }
-})
-
-// ---------------------------------- 格式化版本号 ----------------------------------
-// const formatVersion = (version) => {
-//   if (version === 'unknown') return version
-//   return 'v' + version
-// }
-
-// ---------------------------------- 链接配置 ----------------------------------
-
-const links = ref([
-  {
-    title: 'Docs',
-    link: 'https://geektechstudio.feishu.cn/wiki/space/7310593325374013444?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home'
-  },
-  {
-    title: 'Examples',
-    link: 'https://github.com/SwanHubX/SwanLab-examples'
-  },
-  {
-    title: 'Feedback',
-    link: 'https://github.com/SwanHubX/SwanLab/issues'
-  }
-])
-
-const fixeds = ref([
-  {
-    title: 'GitHub',
-    icon: 'github',
-    link: 'https://github.com/SwanHubX/SwanLab'
-  }
-])
-</script>
-
-<style lang="scss" scoped>
-.a-hover {
-  &:hover {
-    @apply text-white-higher;
-  }
-}
-</style>
diff --git a/vue/src/layouts/main/components/HeaderIcon.vue b/vue/src/layouts/main/components/HeaderIcon.vue
deleted file mode 100644
index acd36350..00000000
--- a/vue/src/layouts/main/components/HeaderIcon.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-<template>
-  <div class="swan-icon">
-    <svg xmlns="http://www.w3.org/2000/svg" width="18" height="19" viewBox="0 0 18 19">
-      <path
-        d="M4.30738 8.91662C4.22697 8.95157 4.15007 8.9941 4.07772 9.04362L4.05444 9.06267C4.04533 9.06852 4.03682 9.07525 4.02904 9.08278C2.49975 10.2723 1.6806 12.19 2.11663 14.112C2.36285 15.2141 2.98149 16.1976 3.86828 16.8969C4.75507 17.5961 5.8558 17.9682 6.98495 17.9505H7.15746C7.89778 17.9189 8.62135 17.7202 9.27406 17.3695C9.92678 17.0187 10.4917 16.5249 10.9267 15.9249C11.3616 15.325 11.6552 14.6345 11.7856 13.9051C11.916 13.1757 11.8798 12.4262 11.6797 11.7127C11.4839 11.0111 11.0765 10.3644 10.7198 9.73365C10.0933 8.62593 9.46533 7.51856 8.83597 6.41155C8.64971 6.08453 8.46132 5.75962 8.27717 5.4326C8.03481 5.0008 7.65487 4.5309 7.63476 4.01867C7.61466 3.50643 8.03164 3.01219 8.54175 2.91377C9.50272 2.73068 9.95357 3.77208 9.95569 3.77102C10.0723 4.04984 10.2904 4.274 10.566 4.39818C10.8415 4.52237 11.1539 4.53731 11.4401 4.44001C11.7262 4.3427 11.9647 4.1404 12.1074 3.87398C12.2502 3.60756 12.2864 3.2969 12.2089 3.00479C11.9736 2.23639 11.482 1.57187 10.8161 1.12202C10.2305 0.703176 9.52507 0.485225 8.80528 0.500778C8.30219 0.503547 7.8052 0.611133 7.346 0.816677C6.88681 1.02222 6.47549 1.32121 6.13829 1.69457C5.75508 2.12281 5.48051 2.63694 5.33776 3.19359C5.19501 3.75023 5.18826 4.33305 5.31808 4.89285C5.36995 5.10309 5.4408 5.30819 5.52975 5.50562C5.78269 6.07924 6.15416 6.60205 6.46743 7.14498C6.85195 7.81172 7.23648 8.47882 7.62101 9.14628C8.00554 9.81373 8.39359 10.484 8.78517 11.1571C9.10267 11.7138 9.44874 12.2006 9.48367 12.8695C9.49839 13.2263 9.4391 13.5822 9.30954 13.915C9.17998 14.2477 8.98295 14.5501 8.73085 14.803C8.47875 15.0559 8.17704 15.2538 7.84469 15.3845C7.51235 15.5151 7.15657 15.5755 6.79975 15.5619C5.5615 15.5058 4.62699 14.5882 4.43861 13.3881C4.2026 11.8725 5.38264 11.0407 5.38264 11.0407C5.6313 10.8944 5.81851 10.6629 5.90958 10.3892C6.00064 10.1154 5.9894 9.81791 5.87792 9.55181C5.76644 9.28571 5.56229 9.06903 5.30328 8.94193C5.04428 8.81482 4.74799 8.7859 4.4693 8.86053C4.41598 8.87553 4.36366 8.89391 4.31267 8.91556"
-      />
-      <path
-        d="M17.0001 13.0458C17.0004 12.6181 16.965 12.1912 16.8942 11.7694C16.7841 11.1042 16.5777 10.4585 16.2815 9.85281C16.1037 9.49192 15.8793 9.13103 15.5279 8.93418C15.352 8.83501 15.1533 8.78332 14.9513 8.78419C14.7494 8.78506 14.5511 8.83845 14.376 8.93912C14.2009 9.03979 14.0551 9.18428 13.9527 9.35839C13.8504 9.5325 13.7951 9.73024 13.7923 9.93218C13.7827 10.3777 14.024 10.7788 14.204 11.1895C14.503 11.8739 14.6384 12.6185 14.5995 13.3644C14.5605 14.1103 14.3483 14.8368 13.9796 15.4863C13.957 15.5248 13.9368 15.5648 13.9193 15.6059C13.8443 15.7805 13.8051 15.9683 13.8039 16.1583C13.8118 16.4308 13.9131 16.6923 14.091 16.8989C14.2689 17.1055 14.5124 17.2446 14.7807 17.2929C15.438 17.4103 15.9597 16.9172 16.2359 16.3668C16.7437 15.3337 17.0052 14.197 17.0001 13.0458Z"
-      />
-      <path
-        d="M11.2098 5.22189C11.1881 5.22605 11.1665 5.23135 11.1453 5.23777C11.102 5.24986 11.063 5.27396 11.0328 5.30728C11.0026 5.3406 10.9825 5.38179 10.9748 5.42607C10.967 5.47036 10.972 5.51593 10.989 5.55752C11.0061 5.59911 11.0346 5.63502 11.0712 5.6611C11.6095 6.06818 12.0635 6.57608 12.4079 7.15652C12.5014 7.31595 12.5828 7.48223 12.6513 7.65394C12.7084 7.8021 12.7846 7.99578 12.9773 7.98519C13.1275 7.97779 13.1889 7.84973 13.2302 7.72167C13.4265 7.05578 13.4877 6.35734 13.4101 5.66745C13.3548 5.29159 13.2684 4.92096 13.1519 4.55938C13.1342 4.47182 13.0933 4.39065 13.0333 4.32443C12.9952 4.28979 12.9468 4.26849 12.8954 4.26372C12.8441 4.25895 12.7926 4.27097 12.7487 4.29797C12.707 4.33172 12.6713 4.37213 12.6428 4.41756C12.4664 4.65488 12.2399 4.85038 11.9793 4.99012C11.8478 5.06001 11.7095 5.11605 11.5665 5.15734C11.4469 5.19226 11.3252 5.19755 11.2098 5.22189Z"
-      />
-    </svg>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 顶部图标
- * @file: HeaderIcon.vue
- * @since: 2024-01-24 13:32:24
- **/
-</script>
-
-<style lang="scss" scoped>
-.swan-icon {
-  @apply w-8 h-8 flex justify-center items-center rounded text-white-default;
-  background-color: var(--accent-green-default);
-}
-</style>
diff --git a/vue/src/layouts/main/components/SideBar.vue b/vue/src/layouts/main/components/SideBar.vue
deleted file mode 100644
index af96bcdb..00000000
--- a/vue/src/layouts/main/components/SideBar.vue
+++ /dev/null
@@ -1,155 +0,0 @@
-<template>
-  <!-- 侧边栏区域 -->
-  <div class="flex flex-col grow h-full bg-higher">
-    <!-- 概览区域 -->
-    <div class="p-4 flex flex-col border-b gap-2 font-semibold">
-      <!-- 项目信息 -->
-      <h1 class="font-semibold mb-1">{{ $t('common.sidebar.project.title') }}</h1>
-      <RouterLink to="/" active-class="active-link">
-        <SLIcon icon="runs" class="w-4 h-4 mr-2" />
-        <span>{{ $t('common.sidebar.project.runs') }}</span>
-      </RouterLink>
-      <RouterLink to="/charts" active-class="active-link">
-        <SLIcon icon="charts" class="w-4 h-4 mr-2" />
-        <span>{{ $t('common.sidebar.project.charts') }}</span>
-      </RouterLink>
-    </div>
-    <!-- 实验路由 -->
-    <div class="experiments-container" ref="expContainerRef">
-      <div class="flex flex-col gap-4 sticky top-0 bg-higher py-4 -mb-4">
-        <div class="flex items-center gap-1.5">
-          <h1 class="font-semibold">{{ $t('common.sidebar.experiments.title') }}</h1>
-          <span class="font-semibold px-3 text-sm bg-highest rounded-full">{{ totalExperiments }}</span>
-        </div>
-        <SLSearch @input="search" reverse :placeholder="$t('common.search.placeholder')" />
-      </div>
-      <!-- 实验列表 -->
-      <RouterLink
-        v-for="experiment in experiments"
-        :key="experiment.id"
-        :to="getExperimentRouter(experiment)"
-        :title="experiment.name"
-        class="experiment-link"
-        active-class="active-link"
-      >
-        <circle
-          class="w-4 h-4 rounded-full mr-3 flex-shrink-0"
-          :style="{ backgroundColor: getExperimentColor(experiment) }"
-        />
-        <span class="truncate">{{ experiment.name }}</span>
-        <!-- 更多信息，进入此容器后不触发父容器所有效果(包括hover、active的css效果) -->
-        <div class="more-info" @click.prevent @mouseenter="removeHover" @mouseleave="resetHover">
-          <!-- 如果实验正在运行，显示running -->
-          <span v-if="experiment.status === 0"> ({{ $t('common.sidebar.experiments.running') }}) </span>
-          <!-- 如果在charts页面，显示眼睛 -->
-          <button class="show-button" v-if="$route.path == '/charts'" @click="changeExperimentShow(experiment.id)">
-            <SLIcon icon="eye" class="w-full h-full" v-if="experiment.show" />
-            <SLIcon icon="eye-close" class="w-full h-full text-dimmest" v-else />
-          </button>
-        </div>
-      </RouterLink>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 侧边栏导航
- * @file: HomeSiderBar.vue
- * @since: 2023-12-04 18:20:02
- **/
-import SLIcon from '@swanlab-vue/components/SLIcon.vue'
-import SLSearch from '@swanlab-vue/components/SLSearch.vue'
-import { ref, computed } from 'vue'
-import { RouterLink } from 'vue-router'
-import { useProjectStore } from '@swanlab-vue/store'
-import http from '@swanlab-vue/api/http'
-import { debounces } from '@swanlab-vue/utils/common'
-
-const projectStore = useProjectStore()
-// ---------------------------------- 实验id转路由 ----------------------------------
-const getExperimentRouter = (experiment) => {
-  return `/experiment/${experiment.id}`
-}
-
-// ---------------------------------- 搜索实验 ----------------------------------
-
-// 需要展示的实验信息——默认展示全部，但在搜索过后，更新为搜索结果
-const experiments = computed(() => {
-  if (!searchValue.value) return projectStore.experiments
-  return projectStore.experiments.filter((expr) => expr.name.toLowerCase().includes(searchValue.value))
-})
-
-const searchValue = ref('')
-
-const search = (value) => {
-  searchValue.value = value.toLowerCase()
-}
-
-// ---------------------------------- 获取实验颜色 ----------------------------------
-const getExperimentColor = (experiment) => {
-  return experiment.light
-}
-
-// ---------------------------------- 计算实验数量，也包括可视实验数量 ----------------------------------
-
-const totalExperiments = computed(() => {
-  return projectStore.experiments.length
-})
-
-// ---------------------------------- 项目图表界面下，点击眼睛后的效果 ----------------------------------
-const path = debounces((id, show) => {
-  http.patch('/experiment/' + id + '/show', {
-    show
-  })
-}, 500)
-const changeExperimentShow = (id) => {
-  const show = projectStore.changeExperimentShow(id)
-  // 请求后端更新，直接更新store后请求，不需要等待
-  path(id, show)
-}
-
-// ---------------------------------- 进入more-info部分，将父元素hover效果移除 ----------------------------------
-const removeHover = (e) => {
-  // console.log('进入', e.target.parentNode)
-  const parent = e.target.parentNode
-  parent.classList.add('!bg-transparent')
-}
-const resetHover = (e) => {
-  // console.log('离开', e.target.parentNode)
-  const parent = e.target.parentNode
-  parent.classList.remove('!bg-transparent')
-}
-</script>
-
-<style lang="scss" scoped>
-// RouterLink共享样式
-a {
-  @apply flex items-center px-4 h-9 text-default hover:bg-highest rounded-lg;
-}
-
-.active-link {
-  @apply bg-highest text-default cursor-default;
-}
-
-.experiments-container {
-  @apply flex flex-col p-4 pt-0 grow gap-4 overflow-auto;
-  &::-webkit-scrollbar-track {
-    background: transparent;
-  }
-
-  // 实验链接样式
-  .experiment-link {
-    @apply flex-shrink-0 text-base pr-0;
-    .more-info {
-      @apply flex justify-end grow text-primary-default pr-4 gap-2 cursor-default;
-      .show-button {
-        @apply w-7 h-6 p-1 rounded;
-        &:hover {
-          @apply text-primary-highest bg-highest;
-        }
-      }
-    }
-  }
-}
-</style>
diff --git a/vue/src/main.js b/vue/src/main.js
deleted file mode 100644
index 951ae452..00000000
--- a/vue/src/main.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import { createApp } from 'vue'
-import App from './App.vue'
-import { createPinia } from 'pinia'
-import { i18n } from './i18n'
-import router from './router'
-import './style.scss'
-// 設置 pinia
-const pinia = createPinia()
-const app = createApp(App)
-
-app.use(pinia)
-app.use(i18n)
-app.use(router)
-
-// ---------------------------------- 设置指令 ----------------------------------
-// v-tippy
-import { directive } from 'vue-tippy'
-import 'tippy.js/dist/tippy.css'
-
-app.directive('tippy', directive)
-
-// ---------------------------------- 挂载 ----------------------------------
-
-app.mount('#app')
-
-/********************************************
- * 判断是否是苹果设备，如果不是，默认为windows，设置一些样式
- *******************************************/
-import { isApple } from '@swanlab-vue/utils/browser'
-if (!isApple) {
-  const sidevbarTheme = `
-    /* 滚动条整体样式 */
-    * ::-webkit-scrollbar {
-      width: 8px; /* 设置滚动条宽度 */
-    }
-    /* 滚动条轨道样式 */
-    * ::-webkit-scrollbar-track {
-      background-color: var(--background-default);
-    }
-
-    /* 滚动条滑块样式 */
-    * ::-webkit-scrollbar-thumb {
-    background-clip: padding-box;
-      background-color: rgba(50, 50, 50, 0.3);
-      border-radius: 6px;
-      border: 2px solid rgba(0,0,0,0);
-    }
-
-    *::-webkit-scrollbar-button {
-      display: none;
-      height: 0;
-      width: 12px
-    }
-
-    /*
-    * ::-webkit-scrollbar-thumb:hover {
-      background-color: var(--outline-stronger);
-    }
-
-    * ::-webkit-scrollbar-thumb:active {
-      background-color: var(--outline-strongest);
-    }
-    */
-  `
-  const style = document.createElement('style')
-  style.innerHTML = sidevbarTheme
-  document.getElementsByTagName('head')[0].appendChild(style)
-}
diff --git a/vue/src/router/index.js b/vue/src/router/index.js
deleted file mode 100644
index 82ac4ac7..00000000
--- a/vue/src/router/index.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import { createRouter, createWebHistory } from 'vue-router'
-
-const routes = [
-  {
-    path: '/',
-    name: 'overview',
-    component: () => import('@swanlab-vue/views/home/HomeView.vue')
-  },
-  {
-    path: '/charts',
-    name: 'charts',
-    component: () => import('@swanlab-vue/views/charts/ChartsView.vue')
-  },
-  {
-    path: '/experiment/:experimentId',
-    name: 'experiment',
-    component: () => import('@swanlab-vue/views/experiment/ExperimentView.vue'),
-    redirect: { name: 'experiment_chart' },
-    children: [
-      {
-        path: 'index',
-        name: 'experiment_index',
-        component: () => import('@swanlab-vue/views/experiment/pages/index/IndexPage.vue')
-      },
-      {
-        path: 'chart',
-        name: 'experiment_chart',
-        component: () => import('@swanlab-vue/views/experiment/pages/chart/ChartPage.vue')
-      },
-      {
-        path: 'log',
-        name: 'experiment_log',
-        component: () => import('@swanlab-vue/views/experiment/pages/log/LogPage.vue')
-      },
-      {
-        path: 'env',
-        name: 'experiment_env',
-        component: () => import('@swanlab-vue/views/experiment/pages/environment/EnvironmentPage.vue'),
-        redirect: { name: 'exp_env_index' },
-        children: [
-          {
-            path: 'index',
-            name: 'exp_env_index',
-            component: () => import('@swanlab-vue/views/experiment/pages/environment/pages/EnvIndex.vue')
-          },
-          {
-            path: 'hardware',
-            name: 'exp_env_hardware',
-            component: () => import('@swanlab-vue/views/experiment/pages/environment/pages/EnvHardware.vue')
-          },
-          {
-            path: 'requirements',
-            name: 'exp_env_dependencies',
-            component: () => import('@swanlab-vue/views/experiment/pages/environment/pages/EnvRequirements.vue')
-          }
-        ]
-      }
-    ]
-  },
-  {
-    path: '/help',
-    name: 'help',
-    component: () => import('@swanlab-vue/views/help/HelpView.vue')
-  },
-  {
-    path: '/404',
-    name: 'not-found',
-    component: () => import('@swanlab-vue/views/error/pages/NotFound.vue')
-  },
-  { path: '/:pathMatch(.*)*', redirect: '/404' }
-]
-
-const router = createRouter({
-  history: createWebHistory(),
-  base: '/',
-  routes
-})
-
-export default router
diff --git a/vue/src/store/index.js b/vue/src/store/index.js
deleted file mode 100644
index ad28a725..00000000
--- a/vue/src/store/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import { useExperimentStore } from './modules/experiment'
-import { useProjectStore } from './modules/project'
-
-export { useExperimentStore, useProjectStore }
diff --git a/vue/src/store/modules/experiment.js b/vue/src/store/modules/experiment.js
deleted file mode 100644
index 2e7c2895..00000000
--- a/vue/src/store/modules/experiment.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import { defineStore } from 'pinia'
-import { computed, ref } from 'vue'
-import { getDuration } from '@swanlab-vue/utils/time'
-
-export const useExperimentStore = defineStore('experiment', () => {
-  /** state */
-  // 当前实验
-  const experiment = ref()
-  // 当前实验的charts
-  const charts = ref()
-  /** getter */
-  // 当前实验id
-  const id = computed(() => experiment.value?.id)
-  // 实验名称
-  const name = computed(() => experiment.value?.name)
-  // 实验描述
-  const description = computed(() => experiment.value?.description)
-  // 当前实验状态
-  const status = computed(() => experiment.value?.status)
-  // 当前实验颜色
-  const color = computed(() => experiment.value?.light)
-  // 是否running
-  const isRunning = computed(() => status.value === 0)
-  // 持续时间
-  const duration = computed(() => {
-    return getDuration(experiment.value) || ''
-  })
-
-  /** action */
-  // 设置当前实验状态
-  const setStatus = (status) => {
-    experiment.value.status = status
-  }
-  // 修改完成时间
-  const setFinishTime = (time) => {
-    experiment.value.finish_time = time
-  }
-  // 修改实验信息 => 全部信息
-  const setExperiment = (x) => {
-    experiment.value = x
-  }
-  // 修改实验信息 => 部分信息
-  const setExperimentPartial = (data) => {
-    for (const key in data) {
-      // 如果experiment中没有这个key，说明key不合法
-      if (experiment.value[key] == undefined) continue
-      // key 合法，更新该字段
-      experiment.value[key] = data[key]
-    }
-  }
-
-  return {
-    // state
-    experiment,
-    charts,
-    // getter
-    id,
-    name,
-    description,
-    status,
-    color,
-    isRunning,
-    duration,
-    // action
-    setStatus,
-    setFinishTime,
-    setExperiment,
-    setExperimentPartial
-  }
-})
diff --git a/vue/src/store/modules/project.js b/vue/src/store/modules/project.js
deleted file mode 100644
index 1b97ee9e..00000000
--- a/vue/src/store/modules/project.js
+++ /dev/null
@@ -1,151 +0,0 @@
-import { defineStore } from 'pinia'
-import { ref, computed } from 'vue'
-
-// 这是一个全局的 store，用于存储当前项目的信息，以及一些前端需要的信息
-export const useProjectStore = defineStore('project', () => {
-  /** state */
-  // 当前项目信息
-  const project = ref()
-  // 当前主题暗还是亮
-  const dark = ref(false)
-
-  /** getter */
-  const name = computed(() => (project.value?.name ? project.value.name : 'my-machine-learning-project'))
-  const description = computed(() => (project.value?.description ? project.value.description : ''))
-  const experiments = computed(() => project.value?.experiments)
-  const sum = computed(() => project.value?.experiments.length)
-  const createTime = computed(() => project.value?.create_time)
-  const updateTime = computed(() => project.value?.update_time)
-  const logdir = computed(() => project.value?.logdir)
-  // 色盘
-  const colors = computed(() => {
-    const key = dark.value ? 'dark' : 'light'
-    return project.value?.colors[key]
-  })
-  // 当前主题下的所有实验颜色映射关系
-  const colorMap = computed(() => {
-    const key = dark.value ? 'dark' : 'light'
-    const colors = {}
-    project.value?.experiments.forEach((e) => {
-      colors[e.name] = e[key]
-    })
-    return colors
-  })
-  // 当前所有实验可见映射关系
-  const showMap = computed(() => {
-    const showMap = {}
-    project.value?.experiments.forEach((e) => {
-      showMap[e.name] = e.show
-    })
-    return showMap
-  })
-
-  /** action */
-  const setProject = (p) => {
-    p.experiments.reverse()
-    project.value = p
-  }
-  /**
-   * 通过实验名获取实验 run_id
-   */
-  const getExpRunIdByName = (name) => {
-    const experiment = experiments.value.find((e) => e.name === name)
-    return experiment?.run_id
-  }
-  /**
-   * 删除对应的实验
-   * @param {int} id 实验id
-   */
-  const deleteExperiment = (id) => {
-    project.value.experiments = project.value.experiments.filter((e) => e.experiment_id !== id)
-  }
-  /**
-   * 修改项目信息
-   * @param {string} name 项目名称
-   * @param {string} description 项目描述
-   */
-  const updateInfo = ({ name, description }) => {
-    project.value.name = name
-    project.value.description = description
-  }
-  /**
-   * 清空 project
-   */
-  const clearProject = () => {
-    project.value = null
-  }
-  /**
-   * 设置指定实验的状态
-   * @param { number } id 实验id
-   * @param { string } status 实验状态
-   * @param { number } finish_time 实验更新时间
-   */
-  const setExperimentStatus = (id, status, finish_time) => {
-    // 不需要绝对等于
-    const experiment = experiments.value.find((e) => e.experiment_id == id)
-    experiment.status = status
-    if (finish_time) experiment.finish_time = finish_time
-  }
-  /**
-   * 重置实验列表中某一个实验的信息
-   * @param {number} id 实验唯一id
-   * @param {object} newInfo 涵盖字段为experiment中字段的子集，不需要修改的不必传
-   * @param {boolean} overwirte 设置true后，newInfo直接覆盖experiment，请保证newInfo的完整性
-   * @returns
-   */
-  const setExperimentInfo = (id, newInfo, overwirte) => {
-    let experiment = experiments.value.find((e) => e.experiment_id == id)
-    if (overwirte) {
-      experiment = newInfo
-      return
-    }
-    // 遍历 newInfo 中的 key
-    for (const key in newInfo) {
-      // 如果experiment中没有这个key，说明key不合法
-      if (!experiment[key]) continue
-      // key 合法，更新该字段
-      experiment[key] = newInfo[key]
-    }
-  }
-
-  /**
-   * 修改实验的显示状态，是否显示，二元状态，如果show为1，则改为0，反之亦然
-   */
-  let changeShowCallback = null
-  const changeExperimentShow = (id) => {
-    const experiment = experiments.value.find((e) => e.experiment_id == id)
-    experiment.show = experiment.show ? 0 : 1
-    changeShowCallback && changeShowCallback(id, experiment.show)
-    return experiment.show
-  }
-
-  const registerChangeShowCallback = (callback) => {
-    changeShowCallback = callback
-  }
-  const destoryChangeShowCallback = () => {
-    changeShowCallback = null
-  }
-
-  return {
-    sum,
-    name,
-    description,
-    experiments,
-    createTime,
-    updateTime,
-    logdir,
-    colors,
-    colorMap,
-    showMap,
-    setProject,
-    getExpRunIdByName,
-    deleteExperiment,
-    updateInfo,
-    clearProject,
-    setExperimentStatus,
-    setExperimentInfo,
-    changeExperimentShow,
-    registerChangeShowCallback,
-    destoryChangeShowCallback
-  }
-})
diff --git a/vue/src/style.scss b/vue/src/style.scss
deleted file mode 100644
index e961cb48..00000000
--- a/vue/src/style.scss
+++ /dev/null
@@ -1,76 +0,0 @@
-// 导入项目定义的颜色
-@import url('./theme/color.scss');
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-body {
-  font-family: Source Sans Pro, PingFang SC, DFPKingGothicGB-Medium, sans-serif;
-  // 设置任何元素的默认边框颜色、文字颜色
-  @apply text-default;
-  * {
-    @apply border-default;
-  }
-  // svg的默认颜色为当前颜色
-  svg {
-    fill: currentColor;
-  }
-  // 添加一种字体
-  @font-face {
-    font-family: 'JetBrains Mono';
-    src: url('@swanlab-vue/assets/font/JetBrainsMono-Regular.ttf') format('truetype');
-    font-weight: normal;
-    font-style: normal;
-  }
-
-  @font-face {
-    font-family: 'Source Sans Pro';
-    src: url('@swanlab-vue/assets/font/SourceSansPro-Regular.ttf') format('truetype');
-    font-weight: normal;
-    font-style: normal;
-  }
-}
-
-// 移除safari的蓝边
-a,
-input,
-p,
-div {
-  outline: none;
-  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-}
-
-// 弹跳进入动画
-.animate-bounce-in {
-  animation: bounceIn 200ms;
-  @keyframes bounceIn {
-    0%,
-    50%,
-    100% {
-      -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
-      animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
-    }
-
-    0% {
-      opacity: 0.9;
-      -webkit-transform: scale3d(0.98, 0.98, 0.98);
-      transform: scale3d(0.98, 0.98, 0.98);
-    }
-
-    70% {
-      opacity: 1;
-      -webkit-transform: scale3d(1.03, 1.03, 1.03);
-      transform: scale3d(1.03, 1.03, 1.03);
-    }
-
-    100% {
-      -webkit-transform: scale3d(1, 1, 1);
-      transform: scale3d(1, 1, 1);
-    }
-  }
-}
-
-// 解决 html2canvas 截图时文字下移的问题
-img {
-  @apply inline-block;
-}
diff --git a/vue/src/theme/color.scss b/vue/src/theme/color.scss
deleted file mode 100644
index badb84e3..00000000
--- a/vue/src/theme/color.scss
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * 这部分是颜色变量的定义
- * 一般情况下颜色命名方式为：[颜色类型]-[颜色深度]
- * high、strong相关为颜色更亮
- * dimmed相关为颜色更暗
- */
-
-// @mixin dark {
-//   /********************************************
-//    * 背景色
-//    *******************************************/
-//   --background-default: #0f1524;
-//   --background-dimmer: #29334f;
-//   /********************************************
-//    * 边框色
-//    *******************************************/
-//   --outline-default: #2d3242;
-//   /********************************************
-//    * 前景色（一般指页面文字）
-//    *******************************************/
-//   --foreground-default: #f5f9fc;
-//   --foreground-dimmer: #c2c8cc;
-//   --foreground-dimmest: #9da2a6;
-//   /********************************************
-//    * 引导色
-//    *******************************************/
-//   // 成功颜色
-//   --positive-highest: #d5e4d9;
-//   --positive-higher: #cff1d8;
-//   --positive-default: #4e9553;
-//   --positive-dimmer: #37733c;
-//   // 标准颜色
-//   --primary-default: #1c74dd;
-//   --primary-dimmest: #d6e8ff;
-//   // 危险颜色
-//   --negative-default: #bb4c51;
-//   --negative-dimmest: #eddcdc;
-// }
-
-@mixin light {
-  /********************************************
-   * 背景色
-   *******************************************/
-  --background-overlay: rgba(15, 21, 36, 0.7);
-  --background-highest: #dadde1;
-  --background-higher: #f8f9fa;
-  --background-default: #ffffff;
-  --background-dimmest: #27282e;
-  /********************************************
-   * 边框色
-   *******************************************/
-  --outline-default: #d1d7dd;
-  --outline-dimmer: #e5e8eb;
-  --outline-dimmest: #f2f2f2;
-  /********************************************
-   * 前景色（一般指页面文字）
-   *******************************************/
-  --foreground-default: #000000;
-  --foreground-dimmer: #555555;
-  --foreground-dimmest: #b5b5b5;
-  /********************************************
-   * 复杂文字颜色——文字高亮
-   *******************************************/
-  // 白色系
-  --accent-white-highest: #b5b5b5;
-  --accent-white-higher: #dadde1;
-  --accent-white-default: #ffffff;
-
-  // 绿色系
-  --accent-green-default: #99a559;
-
-  /********************************************
-   * 引导色
-   *******************************************/
-  // 成功颜色
-  --positive-highest: #004d0d;
-  --positive-higher: #37733c;
-  --positive-default: #4e9553;
-  --positive-dimmer: #cff1d8;
-  --positive-dimmest: #d5e4d9;
-  // 标准颜色
-  --primary-highest: #004182;
-  --primary-higher: #005cb8;
-  --primary-default: #1c74dd;
-  --primary-dimmer: #74b9ff;
-  --primary-dimmest: #d6e8ff;
-  // 警告颜色
-  --warning-dimmer: #ff9933;
-  --warning-dimmest: #ffad5a;
-  // 危险颜色
-  --negative-highest: #8a0000;
-  --negative-higher: #c20a0a;
-  --negative-default: #bb4c51;
-  --negative-dimmer: #ffc7c7;
-  --negative-dimmest: #eddcdc;
-}
-
-:root {
-  // // 外面使用的应该是通过var()获取的，媒体监听过后的变量，也就是说在此处处理深色、浅色模式
-  // @media (prefers-color-scheme: dark) {
-  //   @include dark;
-  // }
-  // // 浅色模式
-  // @media (prefers-color-scheme: light) {
-  //   @include light;
-  // }
-
-  @include light;
-
-  // // 通过body的data-theme属性来切换深色、浅色模式，用于用户主动切换
-  // body[data-theme='dark'] {
-  //   @include dark;
-  //   color-scheme: dark;
-  // }
-  // body[data-theme='light'] {
-  //   @include light;
-  //   color-scheme: light;
-  // }
-}
diff --git a/vue/src/utils/browser.js b/vue/src/utils/browser.js
deleted file mode 100644
index 9f98d7b0..00000000
--- a/vue/src/utils/browser.js
+++ /dev/null
@@ -1,90 +0,0 @@
-/** 保存一些和浏览器相关的东西 */
-// 1. 判断是否是苹果公司的设备
-export const isApple = /iphone|ipad|ipod|mac/i.test(navigator.userAgent)
-// 2. 当前设备是否在移动端
-export const isMobile = /Mobile/.test(navigator.userAgent)
-
-/**
- * 设置浏览器不可点击，用于异步请求时
- * @param { Function } asyncFunction 异步函数，传入一个请求函数，当请求函数执行完毕后，会自动恢复浏览器的点击事件
- * @returns
- */
-export const setBrowserUnClickable = async (asyncFunction) => {
-  document.body.style.pointerEvents = 'none'
-  return await asyncFunction().finally(() => {
-    document.body.style.pointerEvents = 'auto'
-  })
-}
-
-/**
- * 复制文本到剪贴板，异步函数，使用现代浏览器的API
- * @param { String } text 要复制的文本
- * @param { Function } callback 回调函数 如果执行，回传复制成功与否的状态
- */
-export const copyTextToClipboard = async (text, callback) => {
-  /**
-   * 使用旧的API复制文本到剪贴板，兼容性更好
-   * @param { string } text 要复制的文本
-   * @param { Function } callback 回调函数 如果执行，回传复制成功与否的状态
-   */
-  function fallbackCopyToClipboard(text, callback) {
-    try {
-      const textArea = document.createElement('textarea')
-      textArea.value = text
-      document.body.appendChild(textArea)
-      textArea.select()
-      document.execCommand('copy')
-      document.body.removeChild(textArea)
-      callback(true)
-    } catch (e) {
-      callback(false)
-    }
-  }
-  let status = true
-  try {
-    await navigator.clipboard.writeText(text)
-  } catch (e) {
-    status = false
-  }
-
-  if (!status) {
-    fallbackCopyToClipboard(text, (success) => {
-      status = success
-    })
-  }
-  callback && callback(status)
-}
-
-/**
- * 在浏览器主线程添加一个任务
- */
-export const addTaskToBrowserMainThread = (task) => {
-  setTimeout(task, 0)
-}
-
-/**
- * 生成一个uuid，用于标识一个唯一的id
- * @param { Number } now 传入一个时间戳，用于生成uuid，如果不传入，默认为当前时间戳
- * @returns { String } 生成的uuid
- */
-export const uuid = (now = Math.round(new Date() / 1000)) => {
-  return now.toString(36) + Math.random().toString(36).slice(-4)
-}
-
-/**
- * 将目标字符串下载成文件
- * @param {string} content 下载的内容
- * @param {string} fileName 文件名
- */
-export const downloadStringAsFile = (content, fileName) => {
-  // 创建一个包含字符串内容的 Blob 对象
-  const blob = new Blob([content], { type: 'text/plain' })
-
-  // 创建一个下载链接
-  const downloadLink = document.createElement('a')
-
-  // 设置链接的属性，包括下载文件的名称
-  downloadLink.href = window.URL.createObjectURL(blob)
-  downloadLink.download = fileName
-  downloadLink.click()
-}
diff --git a/vue/src/utils/common.js b/vue/src/utils/common.js
deleted file mode 100644
index d9b11303..00000000
--- a/vue/src/utils/common.js
+++ /dev/null
@@ -1,149 +0,0 @@
-/**
- * 数据、事件处理等公用函数
- */
-import { getTimes } from './time'
-
-/**
- * 基础防抖函数，用于减少函数的调用频率，接收两个参数，第一个是要防抖的函数，第二个是延迟的时间
- * @param { Function } func 函数
- * @param { string } delay 延迟时间，单位毫秒
- * @returns { Function } 返回一个新的函数
- */
-export function debounce(func, delay) {
-  let timer = null
-  return function (...args) {
-    if (timer) clearTimeout(timer)
-    timer = setTimeout(() => {
-      func?.apply(this, args)
-    }, delay)
-  }
-}
-
-/**
- * 进阶防抖函数，除了传入函数和延迟时间外，约定生成的函数的第一个参数为id，用于标识防抖唯一性
- * @param { Function } func 函数
- * @param { string } delay 延迟时间，单位毫秒
- * @returns { Function } 返回一个新的函数, 该函数的第一个参数为id
- */
-export function debounces(func, delay) {
-  const timers = []
-  return function (id, ...args) {
-    if (timers[id]) clearTimeout(timers[id])
-    timers[id] = setTimeout(() => {
-      func?.apply(this, [id, ...args])
-    }, delay)
-  }
-}
-
-/**
- * 生成一个uuid，用于标识一个唯一的id
- * @param { Number } now 传入一个时间戳，用于生成uuid，如果不传入，默认为当前时间戳
- * @returns { String } 生成的uuid
- */
-export const uuid = (now = Math.round(new Date() / 1000)) => {
-  return now.toString(36) + Math.random().toString(36).slice(-4)
-}
-
-/**
- * 科学记数法格式化数据，将普通数字转换为科学计数法，但是由于语言表示的限制，转换后的数据是字符串
- * 格式化规则如下：
- * 0. 定义区间：[min, max)，这两个值通过参数传入，默认为 [1e-7, 1e13)
- * 1. 区间内或者value为0，输入的值以非科学记数法表示
- * 2. 超出区间，统一使用科学计数法表示（不包括0）
- * 3. 科学计数法底数最多保留 digits 位小数
- * 4. 非科学计数法格式的数据，小数点后，保留到第一个有效数字及其后三位
- * 5. 在上述规则下，删除不必要的0，即删除小数点后，最后一个非零数后面的0
- *
- * @param {number, string} value 需要格式化的数字，可以是任何可以转换为数字的类型的值
- * @param {number} max 区间最大值，绝对值大于等于这个区间的值，统一使用科学计数法
- * @param {number} min 区间最小值，绝对值小于这个区间的值，统一使用科学计数法（不包括0）
- * @returns {string} 格式化后的数据
- */
-export const formatNumber2SN = (value, max = 1e5, min = 1e-4, digits = 4) => {
-  if (value === 'NaN' || value === 'INF') {
-    return value
-  }
-  // 如果一个传入的数字的小数点后ignore_digits位全为0，那么把这个数当作整数来处理
-  const ignore_digits = digits + 6
-  // 将传入的数据转换为Number
-  value = Number(value)
-  // 检测符号
-  const sign = value >= 0 ? '' : '-'
-  // 取绝对值
-  value = Math.abs(value)
-
-  // 如果在区间内，使用普通计数法，只需要保留一定精度即可
-  if ((value >= min && value < max) || value === 0) {
-    /**
-     * 这里使用 toFixed 的目的是，js 自动转化科学计数法的最小阈值是 e-6，而我们需要 e-7 才转化
-     * 为了将 e-7 ~ e-6 之间的数从科学计数法转为十进制，需要转化一下
-     * 要求保留小数点后第一个有效数字和其后三位，那么最多需要保留10位精度，例：0.000000123424
-     */
-    // 如果js自动转化为科学计数法
-    if (value.toString().includes('e')) {
-      // 获取最小的精度位
-      const min_str = min.toString()
-      const min_digits = Number(min_str.substring(min_str.indexOf('-') + 1))
-      value = value.toFixed(min_digits + digits - 1)
-    }
-
-    // 到这里，可以保证 value 是一个十进制表示的，位于区间内的值，且为 string
-    value = value.toString()
-
-    // 找到小数点后第一位有效数字的索引
-    const match = value.match(/\.\d*?([1-9])/)
-    if (match) {
-      // 如果找到匹配，获取 value 有效部分的长度
-      // 小数点后第一个非零数在小数点后的索引（match[0]中包含了小数点 => 长度-1）
-      const index = match[0].length - 1
-      /**
-       * 计算符合展示规则的长度
-       * 如果非零值在小数点 ignore_digits 之后，小数部分当 0 算
-       * 整数部分长度：value.substring(0, value.indexOf('.')).length
-       * 小数部分长度：index + digits
-       */
-      const point_index = value.indexOf('.')
-      if (index > ignore_digits) {
-        value = value.substring(0, point_index)
-      } else {
-        const length = value.substring(0, point_index).length + index + digits
-        value = value.substring(0, length)
-      }
-    }
-    // 没有匹配上说明没有小数部分，直接略过
-
-    /**
-     * 默认删除小数点后，无意义的 0
-     * 例如：1.01300 => 1.013
-     */
-    if (value.indexOf('.') !== -1) {
-      value = value.replace(/(\.\d*?[1-9])?0+$/, '$1')
-    }
-  }
-  // 如果超出范围，统一使用科学计数法
-  else {
-    value = value.toExponential(digits) // 注意 toExponential 的返回值是 string
-    // 判断小数部分是否全为 0
-    const point_index = value.indexOf('.')
-    const e_index = value.indexOf('e')
-    const fraction_part = value.substring(point_index + 1, e_index)
-    if (/^0+$/.test(fraction_part)) {
-      value = value.substring(0, point_index) + value.substring(e_index)
-    } else {
-      value = value.replace(/(\.\d*?[1-9])0+e/, '$1e')
-    }
-  }
-  // 最后处理和返回的时候需要保证是字符串格式，不然，如果符合js自动转化的条件，会自动变成科学计数法
-  return sign + value
-}
-
-/**
- * 生成文件名，包含时间戳
- * @param {string} prefix 文件前缀
- * @param {string} suffix 文件后缀
- * @returns {string} 文件名
- */
-export const generateFileNameWithTime = (prefix, suffix) => {
-  const { year, month, day, hour, minute, second } = getTimes(new Date().getTime())
-  return `${prefix}_${year}-${month}-${day}_${hour}-${minute}-${second}.${suffix}`
-}
diff --git a/vue/src/utils/time.js b/vue/src/utils/time.js
deleted file mode 100644
index 8d9aef9c..00000000
--- a/vue/src/utils/time.js
+++ /dev/null
@@ -1,167 +0,0 @@
-import moment from 'moment'
-import { t } from '@swanlab-vue/i18n'
-/**
- * 时间格式转换，转换为相对时间，转换结果由i18n提供
- * 相对时间只管显示，不管时区，如果需要时区转换，请使用convertUtcToLocal
- * @param { number } timestamp - 时间戳,单位为秒
- * @returns { string } "5 months ago"
- */
-export function transTime(time) {
-  moment.updateLocale('local', {
-    relativeTime: {
-      future: t('moment.relativeTime.future'), // 未来的算在之前
-      past: t('moment.relativeTime.past'),
-      s: t('moment.relativeTime.s'),
-      ss: t('moment.relativeTime.ss'),
-      m: t('moment.relativeTime.m'),
-      mm: t('moment.relativeTime.mm'),
-      h: t('moment.relativeTime.h'),
-      hh: t('moment.relativeTime.hh'),
-      d: t('moment.relativeTime.d'),
-      dd: t('moment.relativeTime.dd'),
-      w: t('moment.relativeTime.w'),
-      ww: t('moment.relativeTime.ww'),
-      M: t('moment.relativeTime.M'),
-      MM: t('moment.relativeTime.MM'),
-      y: t('moment.relativeTime.y'),
-      yy: t('moment.relativeTime.yy')
-    }
-  })
-  // 根据传入的时间和时区，转换为当前浏览器下的时间
-
-  return moment(time).fromNow()
-}
-
-/**
- * 将 UTC 时间转换为当前浏览器时区的时间。
- *
- * @param {string} utcTime - UTC 时间字符串，使用 ISO 8601 格式。
- * @returns {Date} 格式化的本地时间
- * @throws {Error} 如果输入的时间字符串格式不正确。
- *
- * @example
- * // 使用示例
- * const utcTime = "2023-01-01T12:00:00Z";
- * const localTime = convertUtcToLocal(utcTime);
- * console.log("本地时间:", localTime);
- */
-export function convertUtcToLocal(utcTime) {
-  // 创建 UTC 时间的 Date 对象
-  var utcDate = new Date(utcTime)
-
-  // 获取本地时区的时间偏移（分钟）
-  var localTimeZoneOffset = new Date().getTimezoneOffset()
-
-  // 计算 UTC 时间与本地时间的时间差（毫秒）
-  var timeZoneDifference = localTimeZoneOffset * 60 * 1000
-
-  // 计算本地时间
-  var localTime = new Date(utcDate.getTime() - timeZoneDifference)
-
-  // 检查结果是否有效
-  if (isNaN(localTime.getTime())) {
-    throw new Error('Invalid UTC time format')
-  }
-
-  return localTime
-}
-
-/**
- * 格式化时间，接收一个时间戳，转化当前浏览器当前时区下的时间，输入时间为UTC时间
- * 例如：上海时区下，2023-12-04T04:44:33.026550Z转换为"2023/12/04 12:44:33"
- * @param {string} time - 时间字符串
- */
-
-export const formatTime = (time) => {
-  // 判断是否携带时区信息
-  const zone = typeof time === 'string' && time.endsWith('Z')
-  let { year, month, day, hour, minute, second } = getTimes(time, zone)
-  // 如果minute是个位数，转换成两位数
-  if (month < 10) month = '0' + month
-
-  if (day < 10) day = '0' + day
-
-  if (hour < 10) hour = '0' + hour
-
-  if (minute < 10) minute = '0' + minute
-
-  if (second < 10) second = '0' + second
-
-  return `${year}/${month}/${day} ${hour}:${minute}:${second}`
-}
-
-/**
- * 获取年月日时分秒
- * @param {string} time - 时间字符串
- * @param {boolean} zone - 是否需要时区转换，为true意味着携带时区信息，不需要时区转换
- */
-export const getTimes = (time, zone = true) => {
-  let localDate
-  if (!zone) {
-    const date = new Date(time)
-    const timezoneOffset = new Date().getTimezoneOffset()
-    const localTime = date.getTime() - timezoneOffset * 60 * 1000
-    localDate = new Date(localTime)
-  } else {
-    localDate = new Date(time)
-  }
-  const year = localDate.getFullYear()
-  let month = localDate.getMonth() + 1
-  let day = localDate.getDate()
-  let hour = localDate.getHours()
-  let minute = localDate.getMinutes()
-  let second = localDate.getSeconds()
-
-  return {
-    year,
-    month,
-    day,
-    hour,
-    minute,
-    second
-  }
-}
-
-/**
- * 获取实验的持续时间
- * @param {experiment} experiment 实验对象
- * @returns {string} 实验持续时间
- */
-export const getDuration = (experiment) => {
-  if (!experiment) return null
-  const time1 = new Date(experiment.create_time)
-  const currentTime = new Date()
-  const time2 = experiment.status === 0 ? new Date(currentTime.getTime()) : new Date(experiment.finish_time)
-
-  if (isNaN(time1.getTime()) || isNaN(time2.getTime())) {
-    // 处理无效日期的情况
-    return 'Invalid date'
-  }
-
-  const timeDifference = Math.abs(time2 - time1)
-
-  const seconds = Math.floor(timeDifference / 1000) % 60
-  const minutes = Math.floor(timeDifference / (1000 * 60)) % 60
-  const hours = Math.floor(timeDifference / (1000 * 60 * 60)) % 24
-  const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24))
-
-  const formattedTime = []
-
-  if (days > 0) {
-    formattedTime.push(`${days}${t('experiment.index.header.experiment_infos.time.day')}`)
-  }
-
-  if (hours > 0) {
-    formattedTime.push(`${hours}${t('experiment.index.header.experiment_infos.time.hour')}`)
-  }
-
-  if (minutes > 0) {
-    formattedTime.push(`${minutes}${t('experiment.index.header.experiment_infos.time.minute')}`)
-  }
-
-  if (seconds > 0) {
-    formattedTime.push(`${seconds}${t('experiment.index.header.experiment_infos.time.second')}`)
-  }
-
-  return formattedTime.join('') || 'less than 1s'
-}
diff --git a/vue/src/views/charts/ChartsView.vue b/vue/src/views/charts/ChartsView.vue
deleted file mode 100644
index cb45b006..00000000
--- a/vue/src/views/charts/ChartsView.vue
+++ /dev/null
@@ -1,96 +0,0 @@
-<template>
-  <div class="flex flex-col min-h-full bg-higher">
-    <ChartsPage
-      :groups="groups"
-      :charts="charts"
-      :default-color="defaultColor"
-      :get-color="getColor"
-      :key="chartsPageKey"
-      v-if="groups.length"
-    />
-    <!-- 图表不存在 -->
-    <p class="font-semibold pt-5 text-center" v-else-if="ready">Empty Charts</p>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 项目对比图表，本组件完成项目对比图表的数据的请求和展示，大致流程是：
- * 1. 通过 http.get('/project/charts') 请求项目对比图表的数据，渲染到页面上
- * 2. 根据每个图表的数据源
- * @file: ChartsView.vue
- * @since: 2024-01-27 13:05:27
- **/
-import http from '@swanlab-vue/api/http'
-import { useProjectStore } from '@swanlab-vue/store'
-import { ref } from 'vue'
-import ChartsPage from './components/ChartsPage.vue'
-import { onUnmounted } from 'vue'
-const projectStore = useProjectStore()
-http.get('/project/charts').then(({ data }) => {
-  // 将namespaces转换为groups
-  charts.value = data.charts
-  namespaces.value = data.namespaces
-  groups.value = generateGroups()
-  ready.value = true
-})
-const ready = ref(false)
-// ---------------------------------- 数据驱动 ----------------------------------
-// 项目对比图表数据，[{name, charts: [charts]}]
-const groups = ref([])
-const charts = ref([])
-const namespaces = ref([])
-const chartsPageKey = ref(0)
-
-const generateGroups = () => {
-  // 生成groups
-  const groups = []
-  namespaces.value.forEach((namespace) => {
-    const group = {
-      ...namespace,
-      charts: []
-    }
-    namespace.charts.forEach((chart_id) => {
-      const chart = charts.value.find((chart) => {
-        return chart.id === chart_id
-      })
-      // 如果chart的所有source都为不可见，不push
-      if (chart.source.every((source) => !projectStore.showMap[source])) return
-      // 首先找到所有source中不在error的keys中的source
-      const sources = chart.source.filter((source) => !chart.error[source])
-      if (sources.every((source) => !projectStore.showMap[source])) return
-      // 如果在source中不在error的keys中的都不可见，不push
-      group.charts.push(chart)
-    })
-    // 如果group的所有chart都为不可见，不push
-    if (group.charts.length) groups.push(group)
-  })
-  return groups
-}
-
-// ---------------------------------- 向projectStore注册回调，当点击眼睛时执行此回调 ----------------------------------
-const handleShowChange = () => {
-  // 重新渲染页面
-  chartsPageKey.value++
-  // 重新生成groups
-  groups.value = generateGroups()
-}
-// 注册点击眼睛的回调
-projectStore.registerChangeShowCallback(handleShowChange)
-
-onUnmounted(() => {
-  projectStore.destoryChangeShowCallback()
-})
-
-// ---------------------------------- 色盘注入 ----------------------------------
-const getColor = (() => {
-  // 遍历所有实验，实验名称为key，实验颜色为value
-  const colors = projectStore.colorMap
-  return (exp_name) => {
-    return colors[exp_name]
-  }
-})()
-const defaultColor = projectStore.colors[0]
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/charts/components/ChartsPage.vue b/vue/src/views/charts/components/ChartsPage.vue
deleted file mode 100644
index ddbdd0de..00000000
--- a/vue/src/views/charts/components/ChartsPage.vue
+++ /dev/null
@@ -1,102 +0,0 @@
-<template>
-  <ChartsDashboard
-    :groups="groups"
-    :update-chart-status="updateChartStatus"
-    :update-namespace-status="updateNamespaceStatus"
-    :media="media"
-    :default-color="defaultColor"
-    :get-color="getColor"
-    :subscribe="on"
-  />
-</template>
-
-<script setup>
-/**
- * @description: 项目对比图表，本组件完成项目对比图表的数据的请求和展示
- * 这个组件是为了实现自动刷新的无奈之举（在websocket没有上之前），整个组件刷新的时候，会重新请求数据
- * @file: ChartsPage.vue
- * @since: 2024-02-06 17:17:55
- **/
-import { useProjectStore } from '@swanlab-vue/store'
-import { onUnmounted } from 'vue'
-import http from '@swanlab-vue/api/http'
-import ChartsDashboard from '@swanlab-vue/charts/ChartsDashboard.vue'
-import { updateChartStatus, updateNamespaceStatus, media } from '@swanlab-vue/api/chart'
-const props = defineProps({
-  // 图表组
-  groups: {
-    type: Array,
-    required: true
-  },
-  // 图表列表
-  charts: {
-    type: Array,
-    required: true
-  },
-  // 默认颜色
-  defaultColor: {
-    type: String,
-    required: true
-  },
-  // 获取颜色
-  getColor: {
-    type: Function,
-    required: true
-  }
-})
-
-const projectStore = useProjectStore()
-
-// ---------------------------------- 轮询器 ----------------------------------
-const intervalMap = new Map()
-const createInterval = (exp_name, cid, callback) => {
-  const interval = setInterval(() => {
-    getTagDataByExpName(exp_name, cid, callback)
-  }, 5000)
-  intervalMap.set(exp_name + '-' + cid, interval)
-}
-
-onUnmounted(() => {
-  intervalMap.forEach((interval) => {
-    clearInterval(interval)
-  })
-})
-
-// ---------------------------------- 数据驱动 ----------------------------------
-
-const on = (sources, cid, callback) => {
-  // 获取数据
-  sources.map((exp_name) => {
-    // 如果exp_name对应的实验的show为0，则跳过
-    if (projectStore.experiments.find((exp) => exp.name === exp_name).show === 0) return callback(exp_name, null, null)
-    getTagDataByExpName(exp_name, cid, callback)
-    // 判断当前实验状态
-    if (projectStore.experiments.find((exp) => exp.name === exp_name).status === 0) {
-      // 开启轮询
-      createInterval(exp_name, cid, callback)
-    }
-  })
-}
-
-// ---------------------------------- 请求数据 ----------------------------------
-
-/**
- * @description: 根据实验名称获取标签数据
- * @param {string} exp_name 实验名称
- * @param {number} cid 图表id
- */
-const getTagDataByExpName = (exp_name, cid, callback) => {
-  const exp_id = projectStore.experiments.find((exp) => exp.name === exp_name).id
-  const tag_name = props.charts.find((chart) => chart.id === cid).name
-  http
-    .get(`/experiment/${exp_id}/tag/${tag_name}`)
-    .then((res) => {
-      callback(exp_name, res.data, null)
-    })
-    .catch(() => {
-      callback(exp_name, null, null)
-    })
-}
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/error/ErrorView.vue b/vue/src/views/error/ErrorView.vue
deleted file mode 100644
index b2f21779..00000000
--- a/vue/src/views/error/ErrorView.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<template>
-  <ErrorLayout>
-    <component :is="Error"></component>
-  </ErrorLayout>
-</template>
-
-<script setup>
-/**
- * @description: 错误页
- * @file: ErrorView.vue
- * @since: 2023-12-14 14:49:58
- **/
-import ErrorLayout from '@swanlab-vue/layouts/ErrorLayout.vue'
-import InitError from './pages/InitError.vue'
-import ExperimentError from './pages/ExperimentError.vue'
-import CustomError from './pages/CustomError.vue'
-import { computed } from 'vue'
-import { provide } from 'vue'
-
-const props = defineProps({
-  code: {
-    type: Number,
-    default: 404
-  },
-  message: {
-    type: String,
-    default: ''
-  }
-})
-
-provide('error_code', props.code)
-provide('error_message', props.message)
-
-const Error = computed(() => {
-  console.log(props.code)
-  switch (props.code) {
-    case 3500:
-      return InitError
-    case 3404:
-      return ExperimentError
-    case 3000:
-      return InitError
-    default:
-      return CustomError
-  }
-})
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/error/components/ErrorWrap.vue b/vue/src/views/error/components/ErrorWrap.vue
deleted file mode 100644
index 5da3f45a..00000000
--- a/vue/src/views/error/components/ErrorWrap.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<template>
-  <section class="w-full h-full flex flex-col justify-between">
-    <h1 class="text-2xl font-semibold pb-6">
-      <slot name="title"></slot>
-    </h1>
-    <div class="pb-6">
-      <p>{{ $t('error.wrap.reason-title') }}</p>
-      <slot></slot>
-    </div>
-    <p class="pb-6">
-      <span>{{ $t('error.wrap.message[0]') }}</span>
-      <a :href="document" target="_blank" class="text-primary-default underline">{{ $t('error.wrap.message[1]') }}</a>
-      。
-    </p>
-    <p class="pb-6">{{ $t('error.wrap.error-code') + error_code }}</p>
-    <p>{{ $t('error.wrap.time', { time }) }}</p>
-  </section>
-</template>
-
-<script setup>
-/**
- * @description: 错误信息
- * @file: ErrorWrap.vue
- * @since: 2023-12-15 15:19:55
- **/
-import { formatTime } from '@swanlab-vue/utils/time'
-import { inject } from 'vue'
-
-const time = formatTime(new Date().getTime() - 8 * 60 * 60 * 1000)
-const document = 'https://geektechstudio.feishu.cn/wiki/UInBw9eaziv17IkwfrOcHCZ1nbc'
-
-const error_code = inject('error_code')
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/error/pages/CustomError.vue b/vue/src/views/error/pages/CustomError.vue
deleted file mode 100644
index bb98e9af..00000000
--- a/vue/src/views/error/pages/CustomError.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<template>
-  <ErrorWrap>
-    <template #title> Something Went Wrong </template>
-    <p>{{ message }}</p>
-  </ErrorWrap>
-</template>
-
-<script setup>
-/**
- * @description: 自定义错误，用于一些不是很严重的错误
- * @file: CustomError.vue
- * @since: 2024-01-04 17:52:02
- **/
-import { inject } from 'vue'
-import ErrorWrap from '../components/ErrorWrap.vue'
-
-const message = inject('error_message')
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/error/pages/ExperimentError.vue b/vue/src/views/error/pages/ExperimentError.vue
deleted file mode 100644
index 34163865..00000000
--- a/vue/src/views/error/pages/ExperimentError.vue
+++ /dev/null
@@ -1,23 +0,0 @@
-<template>
-  <ErrorWrap>
-    <template #title>
-      {{ $t(`error.experiment-error.title`) }}
-    </template>
-    <ul>
-      <li v-for="index in [0, 1]" :key="index">
-        {{ $t(`error.experiment-error.reasons[${index}]`) }}
-      </li>
-    </ul>
-  </ErrorWrap>
-</template>
-
-<script setup>
-/**
- * @description: 实验加载失败
- * @file: ExperimentError.vue
- * @since: 2023-12-15 15:18:59
- **/
-import ErrorWrap from '../components/ErrorWrap.vue'
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/error/pages/InitError.vue b/vue/src/views/error/pages/InitError.vue
deleted file mode 100644
index c5f10067..00000000
--- a/vue/src/views/error/pages/InitError.vue
+++ /dev/null
@@ -1,23 +0,0 @@
-<template>
-  <ErrorWrap>
-    <template #title>
-      {{ $t(`error.init-error.title`) }}
-    </template>
-    <ul>
-      <li v-for="index in [0, 1, 2]" :key="index">
-        {{ $t(`error.init-error.reasons[${index}]`) }}
-      </li>
-    </ul>
-  </ErrorWrap>
-</template>
-
-<script setup>
-/**
- * @description:
- * @file: InitError.vue
- * @since: 2023-12-14 14:49:52
- **/
-import ErrorWrap from '../components/ErrorWrap.vue'
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/error/pages/NotFound.vue b/vue/src/views/error/pages/NotFound.vue
deleted file mode 100644
index 05e8a359..00000000
--- a/vue/src/views/error/pages/NotFound.vue
+++ /dev/null
@@ -1,21 +0,0 @@
-<template>
-  <div class="w-full flex flex-col items-center pt-10">
-    <h1 class="text-5xl font-semibold">404</h1>
-    <p class="py-4 text-dimmer font-semibold text-lg">{{ $t('error.404.message') }}</p>
-    <SLButton class="px-4 py-1.5 border rounded-lg" @click="$router.replace('/')" theme="positive">{{
-      $t('error.404.return')
-    }}</SLButton>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 路由不存在，对应路由 404
- * @file: 404.vue
- * @since: 2023-12-15 15:47:55
- **/
-
-import SLButton from '@swanlab-vue/components/SLButton.vue'
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/experiment/ExperimentView.vue b/vue/src/views/experiment/ExperimentView.vue
deleted file mode 100644
index 4a0f272a..00000000
--- a/vue/src/views/experiment/ExperimentView.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-<template>
-  <ExperimentLayout :key="experimentStore.id" v-if="ready">
-    <template #stop-button>
-      <StopButton></StopButton>
-    </template>
-    <router-view />
-  </ExperimentLayout>
-  <ErrorView :code="errorCode" v-if="!ready && errorCode" />
-</template>
-
-<script setup>
-/**
- * @description: 实验视图，展示实验列表
- * @file: ExperimentView.vue
- * @since: 2023-12-04 19:07:53
- **/
-
-import ExperimentLayout from '@swanlab-vue/layouts/ExperimentLayout.vue'
-import ErrorView from '../error/ErrorView.vue'
-import { useExperimentStore, useProjectStore } from '@swanlab-vue/store'
-import { onBeforeRouteUpdate, useRoute } from 'vue-router'
-import http from '@swanlab-vue/api/http'
-import { computed } from 'vue'
-import { ref } from 'vue'
-import StopButton from './components/StopButton.vue'
-
-const route = useRoute()
-const experimentStore = useExperimentStore()
-const projectStore = useProjectStore()
-
-// ---------------------------------- 请求实验信息 ----------------------------------
-const ready = computed(() => {
-  return experimentStore.id !== undefined
-})
-const errorCode = ref(0) // 错误码
-
-const init = async (id = route.params.experimentId) => {
-  experimentStore.experiment = undefined
-  // 清空charts
-  experimentStore.charts = undefined
-  http
-    .get(`/experiment/${id}`)
-    .then(({ data }) => {
-      experimentStore.experiment = data
-      // 设置实验状态
-      projectStore.setExperimentStatus(data.id, data.status, data.finish_time)
-      // 如果实验状态还在running，就轮询
-      if (experimentStore.isRunning) polling()
-    })
-    .catch((response) => {
-      console.error(response)
-      errorCode.value = response.data?.code || 3000 // 3000 时，后端启动失败
-    })
-}
-
-init()
-
-// ---------------------------------- 控制页面切换，组件刷新 ----------------------------------
-
-onBeforeRouteUpdate((to, from) => {
-  // console.log('leave')
-  if (to.params.experimentId !== from.params.experimentId) {
-    init(to.params.experimentId)
-    clearInterval(timer)
-  }
-})
-
-// ---------------------------------- 轮询函数，获取实验状态 ----------------------------------
-let timer = null
-const polling = () => {
-  timer = setInterval(() => {
-    http
-      .get(`/experiment/${experimentStore.id}/status`)
-      .then(({ data }) => {
-        if (Number(experimentStore.id) !== Number(route.params.experimentId)) {
-          clearInterval(timer)
-          return console.log('stop, experiment id is not equal to route id')
-        }
-        // 设置实验状态
-        experimentStore.setStatus(data.status)
-        experimentStore.setFinishTime(data.finish_time)
-        projectStore.setExperimentStatus(experimentStore.id, data.status, data.finish_time)
-        experimentStore.charts = data.charts
-
-        if (!experimentStore.isRunning) {
-          clearInterval(timer)
-          return console.log('stop, experiment status is not 0')
-        }
-      })
-      .catch((response) => {
-        console.error('polling error', response)
-        clearInterval(timer)
-      })
-  }, 1000)
-}
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/experiment/components/FuncBar.vue b/vue/src/views/experiment/components/FuncBar.vue
deleted file mode 100644
index 8a06b700..00000000
--- a/vue/src/views/experiment/components/FuncBar.vue
+++ /dev/null
@@ -1,109 +0,0 @@
-<template>
-  <div class="w-full flex justify-between gap-5">
-    <div class="flex items-center w-full">
-      <SLSearch @input="input" :placeholder="placeholder" class="max-w-[400px]" />
-      <div class="flex items-center px-4" v-show="targets?.length">
-        <span>{{ _modelValue }} / {{ targets?.length }}</span>
-        <div class="w-3 flex-shrink-0 flex-col flex ml-2">
-          <SLIcon icon="down" class="w-full h-3 -rotate-180 -mb-1" @click="_modelValue++" />
-          <SLIcon icon="down" class="w-full aspect-square" @click="_modelValue--" />
-        </div>
-      </div>
-    </div>
-    <div class="flex gap-3">
-      <SLButton hollow @click="copy">
-        <SLIcon icon="copy" class="icon"></SLIcon>
-        {{ $t('experiment.func-bar.copy') }}
-      </SLButton>
-      <SLButton hollow @click="download">
-        <SLIcon icon="download" class="icon"></SLIcon>
-        {{ $t('experiment.func-bar.download') }}
-      </SLButton>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 功能条：搜索、复制、下载
- * @file: FuncBar.vue
- * @since: 2024-01-10 12:54:19
- **/
-
-import { computed } from 'vue'
-import SLSearch from '@swanlab-vue/components/SLSearch.vue'
-import { message } from '@swanlab-vue/components/message'
-import { t } from '@swanlab-vue/i18n'
-import { copyTextToClipboard, downloadStringAsFile } from '@swanlab-vue/utils/browser'
-
-const props = defineProps({
-  // 复制、下载的内容
-  content: {
-    type: String,
-    default: ''
-  },
-  // 下载时使用的文件名
-  filename: {
-    type: String,
-    default: 'swanlab.file'
-  },
-  // 搜索栏的占位
-  placeholder: {
-    type: String,
-    default: 'Search...'
-  },
-  // 目标行
-  targets: {
-    type: Array,
-    default: () => []
-  },
-  // 当前选中的搜索目标
-  modelValue: {
-    type: Number,
-    default: 1
-  }
-})
-
-const emits = defineEmits(['input', 'download', 'update:modelValue'])
-
-const _modelValue = computed({
-  get() {
-    return props.modelValue
-  },
-  set(v) {
-    if (v < 1) {
-      emits('update:modelValue', 1)
-    } else if (v > props.targets.length) {
-      emits('update:modelValue', props.targets.length)
-    } else {
-      emits('update:modelValue', v)
-    }
-  }
-})
-
-// 输入触发
-const input = (value) => {
-  emits('input', value)
-}
-
-// 复制
-const copy = () => {
-  copyTextToClipboard(props.content)
-  message.success(t('experiment.func-bar.copy-success'))
-}
-
-// 下载
-const download = () => {
-  downloadStringAsFile(props.content, props.filename)
-}
-</script>
-
-<style lang="scss" scoped>
-button {
-  @apply rounded-lg px-3 py-1 flex items-center gap-2;
-}
-
-.icon {
-  @apply w-4 h-4;
-}
-</style>
diff --git a/vue/src/views/experiment/components/StopButton.vue b/vue/src/views/experiment/components/StopButton.vue
deleted file mode 100644
index ddcd3df8..00000000
--- a/vue/src/views/experiment/components/StopButton.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<template>
-  <button class="underline text-dimmest ml-2" :class="showColor ? '' : 'cursor-not-allowed'" @click="stop">
-    is over?
-  </button>
-</template>
-
-<script setup>
-/**
- * @description: 停止实验按钮
- * @file: StopButton.vue
- * @since: 2023-12-30 20:30:30
- **/
-import { useExperimentStore, useProjectStore } from '@swanlab-vue/store'
-import { computed } from 'vue'
-import { confirm } from '@swanlab-vue/components/confirm'
-import http from '@swanlab-vue/api/http'
-import { t } from '@swanlab-vue/i18n'
-import { message } from '@swanlab-vue/components/message'
-
-// ---------------------------------- 弹窗相关 ----------------------------------
-
-const experiment = useExperimentStore()
-const id = experiment.id
-const status = experiment.status
-
-// 展示hover时可点击样式
-const showColor = computed(() => {
-  return status === 0
-})
-
-// ---------------------------------- 确认停止 ----------------------------------
-
-const stop = () => {
-  confirm(t('experiment.index.header.stop.modal.title'), t('experiment.index.header.stop.modal.text')).then(
-    stop_experiment
-  )
-}
-
-const stop_experiment = async () => {
-  const { data } = await http.get(`/experiment/${id}/stop`)
-  if (!data) return
-  experiment.setStatus(data.status)
-  experiment.setFinishTime(data.finish_time)
-  useProjectStore().setExperimentStatus(id, -1, data.finish_time)
-  message.success('State changed')
-}
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/experiment/pages/chart/ChartPage.vue b/vue/src/views/experiment/pages/chart/ChartPage.vue
deleted file mode 100644
index a03dc584..00000000
--- a/vue/src/views/experiment/pages/chart/ChartPage.vue
+++ /dev/null
@@ -1,475 +0,0 @@
-<template>
-  <!-- 图表容器 -->
-  <div class="chart-page" v-if="status === 'success'">
-    <ChartsDashboard
-      :groups="groups"
-      :update-chart-status="updateChartStatus"
-      :update-namespace-status="updateNamespaceStatus"
-      :media="media"
-      :default-color="defaultColor"
-      :get-color="getColor"
-      :subscribe="on"
-      :unsubscribe="off"
-    />
-    <!-- 图表不存在 -->
-    <p class="font-semibold pt-5 text-center" v-if="groups.length === 0">Empty Chart</p>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 实验图表页，在本处将完成图表布局的初始化，注入订阅依赖等内容
- * @file: ChartPage.vue
- * @since: 2023-12-25 15:34:51
- **/
-import { useExperimentStore, useProjectStore } from '@swanlab-vue/store'
-import http from '@swanlab-vue/api/http'
-import { ref } from 'vue'
-import { useRoute } from 'vue-router'
-import { onUnmounted } from 'vue'
-import { updateChartStatus, updateNamespaceStatus, media } from '@swanlab-vue/api/chart'
-import ChartsDashboard from '@swanlab-vue/charts/ChartsDashboard.vue'
-const projectStore = useProjectStore()
-const experimentStore = useExperimentStore()
-const route = useRoute()
-
-// ---------------------------------- 颜色配置，注入色盘 ----------------------------------
-const defaultColor = projectStore.colors[0]
-const getColor = (() => {
-  // 遍历所有实验，实验名称为key，实验颜色为value
-  return (_, index) => {
-    return projectStore.colors[index]
-  }
-})()
-
-// ---------------------------------- 主函数：获取图表配置信息，渲染图表布局 ----------------------------------
-// 基于返回的namespcaes和charts，生成一个映射关系,称之为cnMap
-// key是chart.id, value是chart.id在charts中的索引
-let cnMap = new Map()
-let charts = []
-const groups = ref([])
-const status = ref('initing')
-;(async function () {
-  const { data } = await http.get(`/experiment/${experimentStore.id}/chart`)
-  // 遍历完了，此时cnMap拿到了,模版部分可以依据这些东西渲染了
-  parseCharts(data)
-  // console.log(cnMap)
-})().then(() => {
-  // console.log(eventEmitter)
-  // status在start函数中被设置为success
-  eventEmitter.start()
-  // 设置状态为success
-  status.value = 'success'
-})
-
-// ---------------------------------- 根据charts生成对应配置 ----------------------------------
-/**
- * 解析charts，生成groups和cnMap
- */
-const parseCharts = (data) => {
-  charts = data.charts || []
-  groups.value = data.namespaces || []
-  // 添加一个临时array，用于减少遍历次数
-  const chartsIdArray = charts.map((chart) => {
-    // 为每一个chart生成一个cid
-    chart._cid = cid(chart.id)
-    // 事实上这块需要注意的是，如果chart.error存在，那么chart.source将不会被渲染
-    // 但是为了保持代码平衡，这里还是需要将chart.source添加到eventEmitter中
-    // 这将在下面第一次请求数据的时候因为错误被停止
-    // 往轮询器中添加当前chart的source，即使重复也没事，因为轮询器中是set，默认去重
-    eventEmitter.addSource(chart.source)
-    console.info('addSource', chart.source, eventEmitter._sources)
-    // 返回id
-    return chart.id
-  })
-  // 标志，判断groups是否找到对应的chart
-  let found = false
-  // 遍历groups
-  const temp = groups.value
-  groups.value.forEach((group) => {
-    found = false
-    // 第二次调用以后，id是chart配置，所以id.id
-    group.charts.forEach((id) => {
-      // 如果id是Object，说明是一个chart配置，取id.id
-      if (typeof id === 'object') id = id.id
-      // 遍历charts，如果id和charts.charts_id相同，cnMap添加一个key
-      for (let i = 0; i < chartsIdArray.length; i++) {
-        const chartId = chartsIdArray[i]
-        if (chartId === id) {
-          found = true
-          cnMap.set(chartId, i)
-          break
-        }
-      }
-      if (!found) {
-        console.error('chart.id: ', id)
-        console.error('charts: ', charts)
-        console.error('groups: ', groups)
-        throw new Error('Charts and groups cannot correspond.')
-      }
-    })
-  })
-  // 最终传入的groups，是将charts中的chart_id替换为chart配置
-  temp.forEach((group) => {
-    group.charts = group.charts.map((id) => {
-      if (typeof id === 'object') return charts[cnMap.get(id.id)]
-      return charts[cnMap.get(id)]
-    })
-  })
-  groups.value = temp
-}
-
-// ---------------------------------- 发布、订阅模型 ----------------------------------
-// 请求映射表，反映映射状态
-class RequestMap {
-  constructor() {
-    // 映射表，key:tag ; value:'pending' 'success' 'error'
-    this._map = new Map()
-  }
-  /**
-   * 判断是否可以发起请求,只有第一次请求和上一次请求为success的时候能发起请求
-   */
-  canRequest(key) {
-    // 如果key不存在或者key对应的value不为pending，返回true
-    // console.log(!this._map.has(key) || this._map.get(key) === 'success')
-    // console.log(this._map)
-    return !this._map.has(key) || this._map.get(key) === 'success'
-  }
-
-  /**
-   * 将某个key的状态设置为等待中
-   * @param { string } key 对应tag
-   */
-  setPending(key) {
-    this._map.set(key, 'pending')
-  }
-
-  /**
-   * 将某个key的状态设置为成功
-   * @param { string } key 对应tag
-   */
-  setSuccess(key) {
-    this._map.set(key, 'success')
-  }
-
-  /**
-   * 将某个key的状态设置为失败
-   * @param { string } key 对应tag
-   */
-  setError(key) {
-    this._map.set(key, 'error')
-  }
-
-  /**
-   * 获取某个key的状态
-   */
-  get(key) {
-    return this._map.get(key)
-  }
-}
-
-// 轮询映射表，提供轮询间隔的设置、依据id清除轮询等功能
-class IntervalMap {
-  constructor() {
-    // 映射表，key:tag ; value:timer
-    this._map = new Map()
-    // 轮询间隔，key:tag ; value:interval
-    this._intervalMap = new Map()
-    // 回调函数，key:tag ; value:callback
-    this._callbackMap = new Map()
-  }
-  /**
-   * 输入长度，设置轮询间隔，如果轮询间隔没有变化，不会重新设置
-   * 1. 数据量小于100，每1秒请求一次
-   * 2. 数据量大于100，小于500，每1秒请求一次
-   * 3. 数据量大于500，小于1000，每2秒请求一次
-   * 4. 之后每增加500个数据，延时增加1秒，最大延时为8秒
-   *
-   * @param { string } key 对应tag
-   * @param { number } length 数据量，依据数据长度设置轮询间隔
-   * @param { Function } callback 回调函数，当轮询间隔发生变化时执行
-   */
-  setInterval(key, length, callback = undefined) {
-    // 如果length不存在，直接返回
-    if (length === undefined) return
-    // 如果callback不存在，且key对应的callback也不存在，直接返回
-    if (!callback && !this._callbackMap.get(key)) return
-    // console.warn('setInterval', key, length)
-    // 计算时间
-    let interval = 1000
-    if (length < 100) interval = 1000
-    else if (length < 500) interval = 1000
-    else if (length < 1000) interval = 2000
-    else {
-      const n = Math.floor((length - 1000) / 500)
-      interval = Math.min(8000, 2000 + n * 1000)
-    }
-    // console.log('setInterval', key, interval)
-    if (this._intervalMap.get(key) === interval) return // 如果轮询间隔没有变化，不会重新设置
-    // 此时重新设置轮询间隔，并且清除旧的轮询
-    this.clear(key)
-    // 重新设置轮询
-    // console.warn('重新设置轮询', key, interval)
-    // 拿到回调函数
-    callback = callback || this._callbackMap.get(key)
-    this._map.set(key, setInterval(callback, interval))
-    this._callbackMap.set(key, callback)
-    // 设置轮询间隔
-    this._intervalMap.set(key, interval)
-  }
-
-  /**
-   * 清除轮询
-   * @param { string } key 对应tag
-   */
-  clear(key) {
-    const timer = this._map.get(key)
-    if (timer) clearInterval(timer)
-    this._map.delete(key)
-    this._intervalMap.delete(key)
-    this._callbackMap.delete(key)
-  }
-
-  /**
-   * 清除所有轮询
-   */
-  clearAll() {
-    console.log('清除所有轮询')
-    this._map.forEach((timer) => {
-      console.log('clear', timer)
-      clearInterval(timer)
-    })
-    this._map.clear()
-    this._intervalMap.clear()
-  }
-}
-
-// 事件发射器
-class EventEmitter {
-  constructor() {
-    // 源列表，用于完成数据请求，去重
-    this._sources = new Set()
-    // 基于源列表形成的源对象，key为源列表的元素，value为请求到的数据
-    this._sourceMap = new Map()
-    // 订阅缓存，当_sourceMap的状态改变时更新相关内容，它与_sourceMap的key相同
-    this._distributeMap = new Map()
-    // 实验id
-    this._experiment_id = experimentStore.id
-    // 轮询id
-    this.timer = null
-    // 轮询id的映射表，key为tag，value为timer
-    this._timerMap = new IntervalMap()
-    // 请求间隔映射表，当此表中的数据发生变化时，重新设置轮询函数
-    this._intervalMap = new Map()
-    // 请求状态映射
-    this._request = new RequestMap()
-    // 是否执行过start函数
-    this._started = false
-  }
-
-  /**
-   * 更新_sourceMap中的内容，并且完成key的分发
-   * @param { string } key 源名称，对应_sourceMap的key
-   * @param { Object } data 源数据，会将旧的数据覆盖
-   * @param { Object } error 错误信息，如果存在，data将被设置为null
-   */
-  setSourceData(key, data, error) {
-    if (data) {
-      this._sourceMap.set(key, data)
-    }
-    console.log('setSourceData', key, data, error)
-    // 遍历对应key的_distributeMap，执行回调函数
-    const callbacks = this._distributeMap.get(key)
-    // 回调函数必须全部执行完毕后，才能将请求状态设置为success或者error
-    if (!callbacks) return this.setRequestStatus(key, error)
-    // callbacks存在，则是一个list，每个元素是一个回调函数，回调函数返回一个promise，当所有promise执行完毕后，设置请求状态
-    Promise.all(Array.from(callbacks.values()).map((callback) => callback(key, data, error))).then(() => {
-      console.log('all callbacks are executed')
-      this.setRequestStatus(key, error)
-      this._timerMap.setInterval(key, data?.list?.length)
-    })
-  }
-
-  /**
-   * 将source添加到模型的源列表中
-   * @param { Array } source 源列表，对应每个chart的source
-   */
-  addSource(source) {
-    source = source || []
-    source.map((s) => {
-      // 如果还没有执行过start函数，直接添加到源列表中
-      if (!this._started) return this._sources.add(s)
-      // 判断这个数据是否存在，即使set已经去重，但是需要开启轮询
-      if (this._sources.has(s)) return
-      // 添加到源列表中
-      this._sources.add(s)
-      // 发起请求
-      console.warn('添加源并开启轮询: ', s)
-      this._getTagRequestInterval(s)
-    })
-  }
-
-  /**
-   * 启动事件发射器
-   * 执行此函数，前端开始获取tag数据，并且依据实验状态判断是否关闭轮询等
-   *
-   */
-  start() {
-    // 遍历源列表，请求数据
-    const promises = []
-    this._sources.forEach((tag) => {
-      // promise all如果出现错误，会直接reject，不会执行后面的，所以这里不用它,使用for of
-      promises.push(this._getSoureceData(tag))
-    })
-    // 等待第一次请求完成，然后设置轮询
-    Promise.all(promises).then(() => {
-      // 如果实验状态不是0，停止轮询
-      if (!experimentStore.isRunning) return console.log('stop, experiment status is not 0')
-      // 遍历源列表，请求数据
-      this._sources.forEach((tag) => {
-        // 设置轮询
-        this._getTagRequestInterval(tag)
-      })
-      this._started = true
-    })
-  }
-
-  /**
-   * 运行此函数代表必然开启轮询
-   * @param { string } tag 源名称
-   */
-  _getTagRequestInterval(tag) {
-    // console.log('设置轮询', tag)
-    this._timerMap.setInterval(tag, this._sourceMap.get(tag)?.list.length || 0, () => {
-      // 在此处取出pinia中的charts配置，重新设置
-      experimentStore.charts && parseCharts(experimentStore.charts)
-      // 判断当前实验id和路由中的实验id是否相同，如果不同，停止轮询
-      if (Number(this._experiment_id) !== Number(route.params.experimentId)) {
-        this._timerMap.clearAll()
-        return console.log('stop, experiment id is not equal to route id')
-      }
-      this._getSoureceData(tag)
-      // 如果实验状态不是0，停止轮询
-      if (!experimentStore.isRunning) {
-        this._timerMap.clearAll()
-        return console.log('stop, experiment status is not 0')
-      }
-    })
-  }
-
-  /**
-   * 根据source获取数据，此时source又被称为tag
-   * @param { string } source 源名称
-   */
-  _getSoureceData(tag) {
-    // 如果无法请求，直接返回
-    if (!this._request.canRequest(tag)) return console.log('abandon')
-    // 在请求之前将_request设置为pending
-    this._request.setPending(tag)
-    return http
-      .get(`/experiment/${this._experiment_id}/tag/${tag}`)
-      .then((res) => {
-        // 更新_sourceMap
-        console.log('更新')
-        this.setSourceData(tag, res.data, null)
-      })
-      .catch((err) => {
-        // 更新_sourceMap
-        this.setSourceData(tag, null, err)
-      })
-  }
-
-  /**
-   * 设置请求状态
-   * @param { string } tag  源名称
-   * @param { object } error 错误信息，如果存在，设置为error，否则设置为success
-   */
-  setRequestStatus(tag, error) {
-    if (error) this._request.setError(tag)
-    else this._request.setSuccess(tag)
-  }
-
-  /**
-   * 订阅数据，当数据更新时，执行回调函数
-   * @param { array } tags 源列表，对应chart的source，可以是一个或多个
-   * @param { string } cid 订阅者id，对应chart的_cid
-   * @param { Function } callback 回调函数，当数据更新时执行订阅者的回调函数，这应该是一个Promise
-   * 回调函数接收三个参数，分别是tag、data和err
-   */
-  $on(tags, cid, callback) {
-    console.log('订阅', tags, cid)
-    tags = tags || []
-    tags.map((tag) => {
-      // 拿到已经订阅的回调函数
-      const callbacks = this._distributeMap.get(tag)
-      // 如果已订阅存在，直接添加
-      if (callbacks) callbacks.set(cid, callback)
-      // 否则，创建一个新的map，添加
-      else this._distributeMap.set(tag, new Map([[cid, callback]]))
-      // 如果这个tag的请求并不存在，发起请求
-      if (this._request.canRequest(tag)) {
-        console.warn('数据不存在，发起请求')
-        return this._getSoureceData(tag)
-      }
-      // 如果这个tag的请求已经失败，执行回调函数
-      if (this._request.get(tag) === 'error') {
-        console.warn('数据请求失败，执行回调函数')
-        return callback(tag, null, true)
-      }
-    })
-  }
-
-  /**
-   * 取消订阅，当chart被销毁时，需要手动取消订阅
-   * @param { string } tags 源列表，对应chart的source，可以是一个或多个
-   * @param { string } cid 订阅者id，对应chart的_cid
-   */
-  $off(tags, cid) {
-    if (!this?._distributeMap) return
-    tags = tags || []
-    tags.map((tag) => {
-      // 拿到已经订阅的回调函数
-      const callbacks = this._distributeMap.get(tag)
-      // 如果已订阅存在，直接添加
-      if (callbacks) callbacks.delete(cid)
-    })
-  }
-
-  /**
-   * 销毁订阅模型
-   */
-  delete() {
-    this._timerMap.clearAll()
-    // this._sources = null
-    // this._sourceMap = null
-    // this._distributeMap = null
-  }
-}
-
-// 初始化订阅模型
-const eventEmitter = new EventEmitter()
-
-// 依赖注入，注入订阅函数和取消订阅函数
-const on = (tags, cid, callback) => eventEmitter.$on(tags, cid, callback)
-const off = (tags, cid) => eventEmitter.$off(tags, cid)
-
-// 当前组件销毁时，取消相关行为
-onUnmounted(() => {
-  eventEmitter.delete()
-})
-
-// ---------------------------------- 工具函数：辅助渲染 ----------------------------------
-/**
- * 依据chart_id生成cid，这是一个唯一id，用于eventEmitter中的订阅id，也是dom对象的id
- * @param { string } chart_id
- */
-const cid = (chart_id) => {
-  return chart_id
-}
-</script>
-
-<style lang="scss" scoped>
-.chart-page {
-  @apply bg-higher min-h-[calc(100vh-163px)];
-}
-</style>
diff --git a/vue/src/views/experiment/pages/environment/EnvironmentPage.vue b/vue/src/views/experiment/pages/environment/EnvironmentPage.vue
deleted file mode 100644
index 7279a935..00000000
--- a/vue/src/views/experiment/pages/environment/EnvironmentPage.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<template>
-  <div
-    class="w-full h-full flex p-6 bg-higher"
-    :class="experimentStore.experiment.description ? 'min-h-[calc(100vh-195px)]' : 'min-h-[calc(100vh-163px)]'"
-  >
-    <SiderNav class="shrink-0 mr-6" />
-    <div class="w-full rounded-lg mr-6">
-      <RouterView></RouterView>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 实验环境页
- * @file: EnvironmentPage.vue
- * @since: 2024-01-09 15:14:05
- **/
-
-import SiderNav from './components/SiderNav.vue'
-import { useExperimentStore } from '@swanlab-vue/store'
-
-const experimentStore = useExperimentStore()
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/experiment/pages/environment/components/EnvGPUItem.vue b/vue/src/views/experiment/pages/environment/components/EnvGPUItem.vue
deleted file mode 100644
index b9d2ec21..00000000
--- a/vue/src/views/experiment/pages/environment/components/EnvGPUItem.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-<template>
-  <div class="flex flex-col gap-y-4 pt-4" v-if="system.gpu?.cores">
-    <!-- gpu-cores -->
-    <div class="w-full flex">
-      <span class="text-dimmer w-44">GPU count</span>
-      <span>{{ system.gpu?.cores }}</span>
-    </div>
-    <!-- gpu detail message -->
-    <div class="w-full flex" v-for="(gpu, i) in system.gpu.memory" :key="i">
-      <!-- gpu -->
-      <span class="text-dimmer w-44">GPU{{ i }}</span>
-      <!-- type -->
-      <span class="w-96 break-words whitespace-wrap shrink">{{ system.gpu.type[i] }}</span>
-      <!-- memory -->
-      <span class="shrink-0 pl-8">{{ gpu }} GB</span>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 一行分为三部分：gpu , gpu_type , gpu_memory，主要是展示作用
- * @file: EnvGPUItem.vue
- * @since: 2024-01-25 22:52:50
- **/
-
-import { useExperimentStore } from '@swanlab-vue/store'
-
-const experimentStore = useExperimentStore()
-const experiment = experimentStore.experiment
-const system = experiment.system
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/experiment/pages/environment/components/EnvItem.vue b/vue/src/views/experiment/pages/environment/components/EnvItem.vue
deleted file mode 100644
index a5dc8cce..00000000
--- a/vue/src/views/experiment/pages/environment/components/EnvItem.vue
+++ /dev/null
@@ -1,78 +0,0 @@
-<template>
-  <div class="w-full flex relative" v-if="envValue">
-    <!-- key -->
-    <span class="text-dimmer w-44 shrink-0">{{ $t(`experiment.env.keys.${envKey}`) }}</span>
-    <!-- value -->
-    <span class="item" :class="{ 'high-light': highLight, copy: copy }" @click="copy && copyText(envValue)">
-      <span v-if="!link">{{ envValue }}</span>
-      <a :href="envValue" target="_blank" class="hover:underline underline-offset-2" v-else>{{ envValue }}</a>
-      <SLIcon icon="copy" class="copy-button" v-if="copy" />
-    </span>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 一行分为两部分：env 和 value，主要是展示作用
- * @file: EnvItem.vue
- * @since: 2024-01-09 19:44:16
- **/
-
-import { message } from '@swanlab-vue/components/message'
-import { copyTextToClipboard } from '@swanlab-vue/utils/browser'
-
-defineProps({
-  envKey: {
-    type: [String, Number],
-    default: ''
-  },
-  envValue: {
-    type: [String, Number],
-    default: ''
-  },
-  highLight: {
-    type: Boolean,
-    default: false
-  },
-  link: {
-    type: Boolean,
-    default: false
-  },
-  copy: {
-    type: Boolean,
-    default: false
-  }
-})
-
-const copyText = (value) => {
-  copyTextToClipboard(value)
-  message.success('Copy!')
-}
-</script>
-
-<style lang="scss" scoped>
-.item {
-  @apply left-48 break-all;
-
-  &:hover {
-    .copy-button {
-      @apply opacity-100;
-    }
-  }
-
-  .copy-button {
-    @apply absolute w-4 h-4 top-1.5 right-1 transition-all duration-150 opacity-0;
-  }
-}
-
-.high-light {
-  @apply text-dimmer bg-higher px-2;
-}
-
-.copy {
-  @apply transition-all;
-  &:hover {
-    @apply rounded pr-8 pl-2 scale-y-100 bg-highest cursor-pointer;
-  }
-}
-</style>
diff --git a/vue/src/views/experiment/pages/environment/components/EnvItems.vue b/vue/src/views/experiment/pages/environment/components/EnvItems.vue
deleted file mode 100644
index 13ccc2fb..00000000
--- a/vue/src/views/experiment/pages/environment/components/EnvItems.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<template>
-  <div class="env-items" v-if="data?.length !== 0 && valid">
-    <EnvItem
-      :env-key="item.key"
-      :env-value="item.value"
-      v-for="item in data"
-      :key="item.key"
-      :highLight="item.highLight"
-      :copy="item.copy"
-      :link="item.link"
-    />
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 环境分块 - 每个分块包含多条环境信息
- * @file: EnvItems.vue
- * @since: 2024-01-09 20:03:21
- **/
-
-import { computed } from 'vue'
-import EnvItem from './EnvItem.vue'
-
-const props = defineProps({
-  data: {
-    type: Array,
-    default: () => {
-      return []
-    }
-  }
-})
-
-// 数据全为空时不合法
-const valid = computed(() => {
-  return props.data.filter((item) => item.value).length > 0
-})
-</script>
-
-<style lang="scss" scoped>
-.env-items {
-  @apply w-full py-5 flex-col space-y-5;
-
-  &:not(:last-child) {
-    @apply border-b;
-  }
-}
-</style>
diff --git a/vue/src/views/experiment/pages/environment/components/SiderNav.vue b/vue/src/views/experiment/pages/environment/components/SiderNav.vue
deleted file mode 100644
index 1de83e48..00000000
--- a/vue/src/views/experiment/pages/environment/components/SiderNav.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<template>
-  <div>
-    <router-link v-for="item in navs" :key="item.key" :to="item.to" active-class="active-router">
-      <span class="lg:w-48 lg:h-auto lg:px-4 lg:transition-none px-6 w-0 h-0 transition-all overflow-hidden">{{
-        $t(`experiment.env.navs.${item.key}`)
-      }}</span>
-      <SLIcon :icon="item.icon" class="w-4 h-4 lg:w-0 lg:h-0 transition-all absolute left-4" />
-    </router-link>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 环境页 - 侧边导航栏
- * @file: SiderNav.vue
- * @since: 2024-01-09 15:32:18
- **/
-
-const navs = [
-  {
-    key: 'index',
-    to: `index`,
-    icon: 'home'
-  },
-  {
-    key: 'hardware',
-    to: `hardware`,
-    icon: 'config'
-  },
-  {
-    key: 'requirements',
-    to: `requirements`,
-    icon: 'help'
-  }
-]
-</script>
-
-<style lang="scss" scoped>
-a {
-  @apply relative flex items-center h-11 text-default hover:bg-highest rounded-lg mb-2;
-}
-
-.active-router {
-  @apply bg-highest hover:bg-highest font-semibold;
-}
-</style>
diff --git a/vue/src/views/experiment/pages/environment/pages/EnvHardware.vue b/vue/src/views/experiment/pages/environment/pages/EnvHardware.vue
deleted file mode 100644
index fc2791c0..00000000
--- a/vue/src/views/experiment/pages/environment/pages/EnvHardware.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<template>
-  <div class="w-full border p-6 rounded-lg bg-default">
-    <h1 class="w-full text-xl font-semibold pb-4 border-b mb-2">{{ $t(`experiment.env.title.${route.name}`) }}</h1>
-    <EnvItems :data="item" v-for="item in environments" :key="item" />
-    <EnvGPUItem />
-    <div v-if="Object.keys(experimentStore.experiment.system).length === 0">
-      <p class="text-center pt-5">No hardware information</p>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 系统硬件信息
- * @file: EnvHardware.vue
- * @since: 2024-01-24 21:19:24
- **/
-
-import { computed } from 'vue'
-import { useExperimentStore } from '@swanlab-vue/store'
-import EnvItems from '../components/EnvItems.vue'
-import EnvGPUItem from '../components/EnvGPUItem.vue'
-import { useRoute } from 'vue-router'
-const experimentStore = useExperimentStore()
-const experiment = experimentStore.experiment
-const system = experiment.system
-const route = useRoute()
-const environments = computed(() => {
-  return [cpu.value]
-})
-
-// 系统硬件信息
-const cpu = computed(() => {
-  return [
-    {
-      key: 'cpu',
-      value: system.cpu
-    },
-    {
-      key: 'memory',
-      value: system.memory ? system.memory + 'GB' : ''
-    }
-  ]
-})
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/experiment/pages/environment/pages/EnvIndex.vue b/vue/src/views/experiment/pages/environment/pages/EnvIndex.vue
deleted file mode 100644
index ba40fb45..00000000
--- a/vue/src/views/experiment/pages/environment/pages/EnvIndex.vue
+++ /dev/null
@@ -1,134 +0,0 @@
-<template>
-  <div class="w-full border p-6 rounded-lg bg-default">
-    <h1 class="w-full text-xl font-semibold pb-4 border-b mb-2">{{ $t(`experiment.env.title.${route.name}`) }}</h1>
-    <EnvItems :data="item" v-for="item in environments" :key="item" />
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 实验环境 - 总览页
- * @file: IndexPage.vue
- * @since: 2024-01-09 15:47:20
- *
- * 这里会放一些实验配置信息
- **/
-
-import { useRoute } from 'vue-router'
-import { useExperimentStore, useProjectStore } from '@swanlab-vue/store'
-import { computed } from 'vue'
-import { formatTime } from '@swanlab-vue/utils/time'
-import EnvItems from '../components/EnvItems.vue'
-
-const route = useRoute()
-const experimentStore = useExperimentStore()
-const projectStore = useProjectStore()
-const experiment = experimentStore.experiment
-const system = experiment.system
-
-// ---------------------------------- 配置信息 ----------------------------------
-
-// 环境配置汇总
-const environments = computed(() => {
-  return [overview.value, systems.value, gits.value, swanlab.value]
-})
-
-// 实验相关（实验描述、时间、系统）
-const overview = computed(() => {
-  return [
-    // {
-    //   key: 'name',
-    //   value: experiment.name
-    // },
-    // {
-    //   key: 'description',
-    //   value: experiment.description
-    // },
-    {
-      key: 'start_time',
-      value: formatTime(experiment.create_time)
-    },
-    {
-      key: 'duration',
-      value: experimentStore.duration
-    },
-    {
-      key: 'hostname',
-      value: system.hostname
-    },
-    {
-      key: 'OS',
-      value: system.os
-    }
-  ]
-})
-
-// 系统环境相关
-const systems = computed(() => {
-  return [
-    {
-      key: 'python',
-      value: system.python
-    },
-    {
-      key: 'executable',
-      value: system.executable,
-      highLight: true,
-      copy: true
-    },
-    {
-      key: 'workspace',
-      value: system.cwd,
-      highLight: true,
-      copy: true
-    },
-    {
-      key: 'command',
-      value: system.command,
-      highLight: true,
-      copy: true
-    }
-  ]
-})
-
-// git 相关
-const gits = computed(() => {
-  return [
-    {
-      key: 'git_remote',
-      value: system.git_remote,
-      highLight: true,
-      link: true
-    },
-    {
-      key: 'git_branch',
-      value: system.git_info ? system.git_info[0] : ''
-    },
-    {
-      key: 'git_commit',
-      value: system.git_info ? system.git_info[1] : '',
-      copy: true
-    }
-  ]
-})
-
-// 硬件相关--v0.2.0 转移到System Hardware页面
-
-// swanlab 相关
-const swanlab = computed(() => {
-  return [
-    {
-      key: 'logdir',
-      value: projectStore.logdir + (projectStore.logdir.includes('/') ? '/' : '\\') + experiment.run_id,
-      highLight: true,
-      copy: true
-    },
-    {
-      key: 'swanlab',
-      value: `v${experiment.version}`
-    }
-  ]
-})
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/experiment/pages/environment/pages/EnvRequirements.vue b/vue/src/views/experiment/pages/environment/pages/EnvRequirements.vue
deleted file mode 100644
index 04f35a7b..00000000
--- a/vue/src/views/experiment/pages/environment/pages/EnvRequirements.vue
+++ /dev/null
@@ -1,126 +0,0 @@
-<template>
-  <div class="w-full border p-6 rounded-lg bg-default" v-if="requirements">
-    <div class="flex items-center justify-between border-b pb-4 mb-5">
-      <h1 class="text-xl font-semibold border-none basis-1/4">{{ $t(`experiment.env.title.${route.name}`) }}</h1>
-      <FuncBar
-        class="basis-1/3"
-        @input="search"
-        :content="requirements?.join('\n')"
-        :filename="filename"
-        :placeholder="$t('experiment.func-bar.placeholder.requirements')"
-      />
-    </div>
-    <template v-if="requirements.length !== 0 && requirements[0] !== ''">
-      <!-- 如果有依赖项 -->
-      <div class="px-6 py-4 bg-higher rounded max-h-[60vh] overflow-y-auto">
-        <p v-for="line in lines" :key="line">
-          <span v-show="!line.isTarget">{{ line.value }}</span>
-          <span v-show="line.isTarget">
-            <span
-              v-for="substring in line.value"
-              :key="substring"
-              :class="substring.toLowerCase() === searchValue ? ' bg-warning-dimmest' : ''"
-            >
-              {{ substring }}
-            </span>
-          </span>
-        </p>
-      </div>
-    </template>
-    <!-- 没有依赖项时占位 -->
-    <div class="w-full flex flex-wrap justify-center pt-10" v-else>
-      <SLIcon class="magnifier" icon="search"></SLIcon>
-      <div class="font-semibold w-full text-center pt-10">{{ $t('experiment.env.empty.requirements') }}</div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 依赖项
- * @file: EnvRequirements.vue
- * @since: 2024-01-09 16:01:55
- **/
-
-import { ref, computed } from 'vue'
-import { useExperimentStore } from '@swanlab-vue/store'
-import FuncBar from '@swanlab-vue/views/experiment/components/FuncBar.vue'
-import http from '@swanlab-vue/api/http'
-import { useRoute } from 'vue-router'
-const experimentStore = useExperimentStore()
-const requirements = ref()
-const route = useRoute()
-http
-  .get(`/experiment/${experimentStore.id}/requirements`)
-  .then(({ data }) => {
-    requirements.value = data.requirements
-  })
-  .catch(() => {
-    requirements.value = []
-  })
-
-// ---------------------------------- 搜索 ----------------------------------
-
-// 查找字符
-const searchValue = ref('')
-
-const search = (value) => {
-  searchValue.value = value.toLowerCase().trim()
-}
-
-// 对依赖的每一行进行一些特殊处理
-const lines = computed(() => {
-  return requirements.value.map((line) => {
-    // 查找内容不为空，并且该行含有查找内容，说明是目标行
-    const isTarget = searchValue.value !== '' && line.toLowerCase().includes(searchValue.value)
-
-    return {
-      isTarget,
-      value: isTarget ? splitStringBySearch(line, searchValue.value) : line
-    }
-  })
-})
-
-// 以查找字符串作为分割点，将一行字符串分割成数组，且忽略大小写
-function splitStringBySearch(target, substring) {
-  // 使用正则表达式进行大小写不敏感的分割，并保留分割点
-  let resultArray = target.split(new RegExp(`(${substring})`, 'i'))
-
-  // 去除数组中的空字符串
-  resultArray = resultArray.filter((item) => item !== '')
-
-  // 返回包含分割点的数组
-  return resultArray
-}
-
-// ---------------------------------- 下载成文件 ----------------------------------
-
-const filename = 'requirements.txt'
-</script>
-
-<style lang="scss" scoped>
-$duration: 3s;
-
-.magnifier {
-  @apply w-10 h-10;
-  animation: animloader $duration infinite;
-}
-
-@keyframes animloader {
-  0% {
-    transform: translate(-5px, -5px);
-  }
-  25% {
-    transform: translate(-5px, 5px);
-  }
-  50% {
-    transform: translate(5px, 5px);
-  }
-  75% {
-    transform: translate(5px, -5px);
-  }
-  100% {
-    transform: translate(-5px, -5px);
-  }
-}
-</style>
diff --git a/vue/src/views/experiment/pages/index/IndexPage.vue b/vue/src/views/experiment/pages/index/IndexPage.vue
deleted file mode 100644
index 5cc267cd..00000000
--- a/vue/src/views/experiment/pages/index/IndexPage.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-<template>
-  <ExperimentHeader />
-  <hr />
-  <ExperimentConfig />
-</template>
-
-<script setup>
-/**
- * @description: 实验-概览页
- * @file: ExperimentIndex.vue
- * @since: 2023-12-09 20:37:45
- **/
-import ExperimentHeader from './components/ExperimentHeader.vue'
-import ExperimentConfig from './components/ExperimentConfig.vue'
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/experiment/pages/index/components/DataTable.vue b/vue/src/views/experiment/pages/index/components/DataTable.vue
deleted file mode 100644
index b7c74e8f..00000000
--- a/vue/src/views/experiment/pages/index/components/DataTable.vue
+++ /dev/null
@@ -1,120 +0,0 @@
-<template>
-  <div class="table">
-    <!-- title and search -->
-    <div class="flex items-center justify-between border-b py-2 px-4 bg-higher rounded-t-lg">
-      <p class="pr-10 font-semibold">{{ title }}</p>
-      <SLSearch
-        class="bg-default max-w-[300px]"
-        :placeholder="$t('experiment.index.config.table.search')"
-        reverse
-        v-model="search"
-      />
-    </div>
-    <!-- table -->
-    <div class="w-full">
-      <!-- header -->
-      <div class="line bg-higher border-b">
-        <div class="header-item" v-for="item in column" :key="item.key" :title="item.title">{{ item.title }}</div>
-      </div>
-      <!-- body -->
-      <div class="max-h-[500px] overflow-y-auto">
-        <div class="data-line" v-for="(line, index) in tableData" :key="line.key" :line="line" :index="index">
-          <div class="body-item" v-for="item in line" :key="item?.key" :title="item">
-            {{ String(item) }}
-          </div>
-        </div>
-      </div>
-      <!-- empty body -->
-      <div class="w-full py-4 flex justify-center" v-if="tableData.length === 0">
-        {{ $t('experiment.index.config.table.empty') }}
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 展示实验配置、实验总结数据的表格
- * @file: DataTable.vue
- * @since: 2024-01-24 14:40:21
- **/
-
-import { ref, computed } from 'vue'
-
-// ---------------------------------- 组件接口 ----------------------------------
-
-const props = defineProps({
-  title: {
-    type: String,
-    default: ''
-  },
-  // 表格体的数据
-  data: {
-    type: Array,
-    default: () => {
-      return []
-    }
-  },
-  // 表头
-  column: {
-    type: Array,
-    default: () => {
-      return []
-    }
-  }
-})
-
-// ---------------------------------- 过滤 ----------------------------------
-
-const search = ref('')
-
-const tableData = computed(() => {
-  if (search.value === '') {
-    return props.data
-  }
-  return props.data.filter((item) => {
-    return Object.keys(item).some((key) => {
-      return String(item[key]).toLowerCase().includes(search.value.toLowerCase())
-    })
-  })
-})
-</script>
-
-<style lang="scss" scoped>
-.table {
-  @apply rounded-lg outline w-full;
-  outline-color: var(--outline-default);
-  outline-width: 1px;
-}
-
-.line {
-  @apply grid grid-cols-5 gap-3;
-}
-
-.data-line {
-  @apply line hover:bg-higher border-b last:border-none border-dimmest;
-  &:last-child {
-    @apply rounded-b-lg;
-  }
-}
-
-.header-item {
-  @apply border-r pl-4 my-3;
-  &:first-child {
-    @apply col-span-2;
-  }
-  &:last-child {
-    @apply border-none pl-0 col-span-3;
-  }
-}
-
-.body-item {
-  @apply py-3 pl-4 pr-2 break-words;
-  &:first-child {
-    @apply col-span-2;
-  }
-  &:last-child {
-    @apply border-none pl-0 col-span-3;
-  }
-}
-</style>
diff --git a/vue/src/views/experiment/pages/index/components/ExperimentConfig.vue b/vue/src/views/experiment/pages/index/components/ExperimentConfig.vue
deleted file mode 100644
index 39180e65..00000000
--- a/vue/src/views/experiment/pages/index/components/ExperimentConfig.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<template>
-  <div class="pr-5" icon="config" :title="$t('experiment.index.config.title')" :retract="false">
-    <div class="pl-6 w-full grid lg:grid-cols-2 lg:gap-10">
-      <div class="pt-4 w-full">
-        <DataTable class="w-full" table-border :column="column" :data="configs" title="Config" />
-      </div>
-      <div class="pt-4">
-        <DataTable table-border :column="column" :data="summaries" title="Summary" />
-      </div>
-    </div>
-    <div class="w-full h-6"></div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 实验的配置信息
- * @file: ExperimentConfig.vue
- * @since: 2023-12-11 17:07:31
- **/
-
-import DataTable from './DataTable.vue'
-import http from '@swanlab-vue/api/http'
-import { ref } from 'vue'
-import { useExperimentStore } from '@swanlab-vue/store'
-import { computed } from 'vue'
-import { formatNumber2SN } from '@swanlab-vue/utils/common'
-
-const experiment = ref(useExperimentStore().experiment)
-
-// 通用表头
-const column = [
-  {
-    title: 'key',
-    key: 'key'
-  },
-  {
-    title: 'value',
-    key: 'value'
-  }
-]
-
-// ---------------------------------- 转化实验配置为表格数据 ----------------------------------
-
-const configs = computed(() => {
-  const configs = []
-  for (const key in experiment.value.config) {
-    configs.push({
-      key,
-      value: experiment.value.config[key].value,
-      sort: experiment.value.config[key].sort
-    })
-  }
-  // 按照 sort 排序，然后删除 sort 属性
-  return configs
-    .sort((a, b) => {
-      return a.sort - b.sort
-    })
-    .map((item) => {
-      delete item.sort
-      return item
-    })
-})
-
-// ---------------------------------- 获取实验的总结数据 ----------------------------------
-
-const summaries = ref([])
-const experimentId = ref(useExperimentStore().id)
-http
-  .get(`/experiment/${experimentId.value}/summary`)
-  .then(({ data }) => {
-    summaries.value = (data.summaries || []).map((item) => {
-      return {
-        key: item.key,
-        value: isNaN(item.value) ? item.value : formatNumber2SN(item.value)
-      }
-    })
-  })
-  .catch((error) => {
-    console.error(error)
-  })
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/experiment/pages/index/components/ExperimentHeader.vue b/vue/src/views/experiment/pages/index/components/ExperimentHeader.vue
deleted file mode 100644
index 9960a987..00000000
--- a/vue/src/views/experiment/pages/index/components/ExperimentHeader.vue
+++ /dev/null
@@ -1,86 +0,0 @@
-<template>
-  <div class="w-full px-6 pb-2 text-default relative">
-    <!-- 实验信息 -->
-    <div class="flex justify-between pt-6 pb-2 flex-wrap">
-      <!-- 实验相关 -->
-      <div class="min-w-[400px] max-w-[50%] pr-4">
-        <div v-for="item in experiment_infos" :key="item.title">
-          <div class="flex pb-4" v-if="item.value || item.title === 'git'">
-            <div class="title">{{ $t(`experiment.index.header.experiment_infos.${item.title}`) }}</div>
-            <div v-if="!item.isLink" :title="item.value">{{ item.value }}</div>
-            <div v-else-if="item.title === 'git' && !item.value" class="text-dimmest">None</div>
-            <a :href="item.value" target="_blank" class="hover:underline break-all" v-else>{{ item.value }}</a>
-          </div>
-        </div>
-      </div>
-      <!-- 系统相关 -->
-      <div class="w-1/2 min-w-[400px] pl-5" v-if="experiment.system">
-        <div v-for="item in experiment_device" :key="item.title" class="flex pb-4">
-          <div class="title">{{ $t(`experiment.index.header.experiment_device.${item.title}`) }}</div>
-          <div :title="item.value">{{ item.value || 'Unkown' }}</div>
-        </div>
-        <SLButton theme="primary" hollow class="rounded-lg">
-          <router-link to="env" class="block px-3 py-1">All Details</router-link>
-        </SLButton>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 实验概览页-信息头部
- * @file: ExperimentHeader.vue
- * @since: 2023-12-11 14:43:51
- **/
-import { computed, ref } from 'vue'
-import { formatTime } from '@swanlab-vue/utils/time'
-import { useExperimentStore } from '@swanlab-vue/store'
-
-const experimentStore = useExperimentStore()
-const experiment = ref(experimentStore.experiment)
-
-// ---------------------------------- 实验信息 ----------------------------------
-
-const experiment_infos = computed(() => {
-  return [
-    {
-      title: 'start_time',
-      value: formatTime(experiment.value.create_time)
-    },
-    {
-      title: 'last_time',
-      value: experimentStore.duration
-    },
-    {
-      title: 'python',
-      value: experiment.value.system.python || ''
-    }
-  ]
-})
-
-// ---------------------------------- 设备信息 ----------------------------------
-
-const experiment_device = computed(() => {
-  return [
-    {
-      title: 'hostname',
-      value: experiment.value.system.hostname || ''
-    },
-    {
-      title: 'os',
-      value: experiment.value.system.os || ''
-    }
-  ]
-})
-</script>
-
-<style lang="scss" scoped>
-.icon {
-  @apply w-5 h-5 text-dimmest cursor-pointer hover:text-dimmer;
-}
-
-.title {
-  @apply min-w-[150px] text-dimmest;
-}
-</style>
diff --git a/vue/src/views/experiment/pages/index/components/TableLine.vue b/vue/src/views/experiment/pages/index/components/TableLine.vue
deleted file mode 100644
index 1aabe7ae..00000000
--- a/vue/src/views/experiment/pages/index/components/TableLine.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-<template>
-  <div class="table-line">
-    <!-- <p
-      class="border absolute max-w-[80%] left-10 translate-y-[-90%] top-0 text-center group-hover:block hidden px-3 py-1 text-sx scale-75 bg-higher rounded-lg"
-      :class="{ 'translate-y-0': index === 0 }"
-    >
-      {{ `${line.key}: ${line.value}` }}
-    </p> -->
-    <slot></slot>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 表格行
- * @file: TableLine.vue
- * @since: 2024-01-24 16:40:01
- **/
-
-defineProps({
-  line: {
-    type: Object,
-    default: () => {
-      return {}
-    }
-  },
-  index: {
-    type: [Number, String],
-    default: 0
-  }
-})
-</script>
-
-<style lang="scss" scoped>
-.table-line {
-  @apply w-full relative hover:bg-highest last:border-none;
-  @apply border-b border-dimmest;
-}
-</style>
diff --git a/vue/src/views/experiment/pages/log/LogPage.vue b/vue/src/views/experiment/pages/log/LogPage.vue
deleted file mode 100644
index 08579187..00000000
--- a/vue/src/views/experiment/pages/log/LogPage.vue
+++ /dev/null
@@ -1,306 +0,0 @@
-<template>
-  <div class="w-full h-full px-7 py-6 relative overflow-hidden">
-    <FuncBar
-      class="pb-6"
-      @input="search"
-      :content="download"
-      :placeholder="$t('experiment.func-bar.placeholder.log')"
-      :filename="filename"
-      v-if="logs"
-    />
-    <section class="log-container" @scroll="handleScroll">
-      <!-- 用于撑开容器，保证滚动条高度 -->
-      <div class="log-area" :style="{ height: areaHeight + 'px' }" v-if="logs">
-        <!-- 日志画板 -->
-        <div class="log-board" :style="{ top: computeTop + 'px' }">
-          <!-- 运行日志 -->
-          <div
-            class="log-line"
-            v-for="(line, index) in lines.slice(range[0], range[1] + 1)"
-            :key="line.value + range[0] + index"
-          >
-            <!-- 行号 -->
-            <span
-              class="w-8 text-right flex-shrink-0 text-dimmest select-none"
-              :class="{ 'text-negative-default': line.error }"
-              :style="{ width: indexWidth }"
-              >{{ line.index }}</span
-            >
-            <!-- 正常日志 -->
-            <div v-if="!line.error">
-              <!-- 如果没有搜索内容/不含搜索内容 -->
-              <span v-show="!line.isTarget">{{ line.value }} </span>
-              <!-- 如果有搜索内容且含有搜索内容 -->
-              <span v-show="line.isTarget">
-                <span
-                  v-for="substring in line.value"
-                  :key="substring"
-                  :class="{ 'bg-warning-dimmest': substring.toLowerCase() === searchValue }"
-                >
-                  {{ substring }}
-                </span>
-              </span>
-            </div>
-            <!-- 错误日志 -->
-            <div class="text-negative-default" v-else>{{ line.value }}</div>
-          </div>
-        </div>
-        <!-- 没有日志 -->
-        <div
-          class="w-full py-10 flex flex-col items-center text-lg font-semibold"
-          v-if="logs.length === 0 && errorLogs.length === 0"
-        >
-          <SLIcon class="magnifier" icon="search"></SLIcon>
-          <span>No Logs</span>
-        </div>
-      </div>
-      <div class="flex h-full items-center justify-center" v-else>
-        <SLLoding />
-      </div>
-    </section>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 实验-日志页
- * @file: LogPage.vue
- * @since: 2023-12-09 20:40:44
- **/
-
-import { ref, onMounted } from 'vue'
-import http from '@swanlab-vue/api/http'
-import { useExperimentStore } from '@swanlab-vue/store'
-import SLLoding from '@swanlab-vue/components/SLLoading.vue'
-import FuncBar from '../../components/FuncBar.vue'
-import { computed } from 'vue'
-import { debounce } from '@swanlab-vue/utils/common'
-import { addTaskToBrowserMainThread } from '@swanlab-vue/utils/browser'
-
-// ---------------------------------- 系统相关 ----------------------------------
-
-const experimentStore = useExperimentStore()
-const id = experimentStore.id
-
-// ---------------------------------- 获取日志和日志相关数据 ----------------------------------
-
-// 所有日志
-const logs = ref()
-
-// 分开行号和内容之后的日志
-const lines = computed(() => {
-  // 正常日志内容
-  const data = logs.value?.map((line, index) => {
-    const content = line
-    const isTarget = searchValue.value !== '' && content.toLowerCase().includes(searchValue.value)
-    return {
-      isTarget,
-      index: index + 1,
-      value: isTarget ? splitStringBySearch(content, searchValue.value) : content
-    }
-  })
-  // 正常内容最后一行的行号
-  const lastLineIndex = getLastLineIndex(data)
-  // 处理错误日志内容
-  errorLogs.value.map((line, index) => {
-    return data.push({
-      index: lastLineIndex + index + 1,
-      value: line,
-      error: true
-    })
-  })
-
-  return data
-})
-
-// 行号宽度
-const indexWidth = computed(() => {
-  let index = 1
-  const l = lines.value?.length
-  if (l) {
-    index = lines.value[l - 1].index === '' ? lines.value[l - 2].index : lines.value[l - 1].index
-  }
-  if (!index) return '0px'
-  return 8 * index.length + 'px'
-})
-
-// 获取正常内容最后一行的行号
-const getLastLineIndex = (data) => {
-  const maxIndex = data?.reduce((max, line) => {
-    if (!line.index) return max
-    return Number(line.index) > Number(max) ? line.index : max
-  }, 0)
-  return Number(maxIndex)
-}
-
-const download = computed(() => {
-  return logs.value?.join('\n') + errorLogs.value.join('\n')
-})
-
-const splitStringBySearch = (target, substring) => {
-  // 使用正则表达式进行大小写不敏感的分割，并保留分割点
-  let resultArray = target.split(new RegExp(`(${substring})`, 'i'))
-
-  // 去除数组中的空字符串
-  resultArray = resultArray.filter((item) => item !== '')
-
-  // 返回包含分割点的数组
-  return resultArray
-}
-
-// 错误日志
-const errorLogs = ref([])
-
-// ---------------------------------- 搜索 ----------------------------------
-
-const searchValue = ref('')
-
-const search = (value) => {
-  searchValue.value = value.toLowerCase()
-}
-
-// ---------------------------------- 下载 ----------------------------------
-
-const filename = 'print.log'
-
-// ---------------------------------- 动态渲染 ----------------------------------
-
-// log 渲染范围
-const range = ref([0, 0])
-// 行高
-const lineHeight = ref(16)
-// 最大额外渲染行数
-const addition = 30
-// log 区高度
-const areaHeight = computed(() => {
-  return lines.value?.length * lineHeight.value
-})
-
-const handleScroll = debounce((event) => {
-  const e = event.target
-  computeRange(e)
-}, 100)
-
-// 计算 log 的渲染范围
-const computeRange = (e) => {
-  const line_height = lineHeight.value
-  // 计算应该渲染多少条数据
-  const pageSize = Math.ceil(e.clientHeight / line_height)
-  // 计算第一条 log 的索引
-  const startIndex = Math.floor(e.scrollTop / line_height)
-  // 计算最后一条 log 的索引
-  const endIndex = startIndex + pageSize
-
-  // 如果距离顶部很近，把顶部到第一条中的log也渲染了
-  if (e.scrollTop <= addition * line_height) range.value[0] = 0
-  // 如果距离顶部比较远，仅渲染第一条上的 addition 条 log
-  else range.value[0] = startIndex - addition
-
-  // 如果距离底部很近，把最后一条到底部的log也渲染了
-  if (e.scrollTop + e.clientHeight >= e.scrollHeight - addition * line_height) range.value[1] = lines.value.length - 1
-  // 如果距离底部较远，仅渲染最后一条下的 addition 条 log
-  else range.value[1] = endIndex + addition
-}
-
-/**
- * 计算 log 行高
- * 没有使用计算属性，因为担心用户手动更改缩放等情况，不会触发响应式计算
- * 采用函数，对某些事件进行监听后调用以适应不同情况下的行高
- * 一般行高是 16px，基本不会出问题，所以目前只在初始化时计算，而没做另外的事件监听
- */
-const computeLineHeight = () => {
-  const e = document.querySelector('.log-line')
-  if (!e) return 0
-  const line_styles = window.getComputedStyle(e)
-  lineHeight.value = parseFloat(line_styles.getPropertyValue('line-height'))
-}
-
-const computeTop = computed(() => {
-  return range.value[0] * lineHeight.value
-})
-
-/**
- * 初始化：
- * 1. 获取日志数据
- * 2. 计算日志行高
- * 3. 计算日志渲染范围
- */
-onMounted(async () => {
-  // 获取日志数据
-  const { data } = await http.get(`/experiment/${id}/recent_log`).catch((error) => {
-    if (error.data.code === 3404) {
-      logs.value = []
-    } else {
-      console.error(error)
-    }
-    return
-  })
-  // 设置日志
-  logs.value = data.logs || []
-  if (data.error) errorLogs.value = data.error
-
-  // 保证渲染后再获取行高，再计算渲染范围
-  addTaskToBrowserMainThread(() => {
-    // 计算
-    computeRange(document.querySelector('.log-container'))
-    computeLineHeight()
-
-    // 判断是否出现滚动条，如果有滚动条，滚动到底部
-    if (document.querySelector('.log-container').scrollHeight > document.querySelector('.log-container').clientHeight) {
-      document.querySelector('.log-container').scrollTop = document.querySelector('.log-container').scrollHeight
-    }
-  })
-})
-</script>
-
-<style lang="scss" scoped>
-.log-container {
-  @apply bg-higher w-full rounded p-4 h-[76vh] overflow-auto relative;
-  font-size: 13px;
-  line-height: 16px;
-  font-family: 'JetBrains Mono', monospace;
-  letter-spacing: 0.1px;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-
-  .log-area {
-    @apply relative h-full break-all;
-    &::-webkit-scrollbar-track {
-      background: transparent;
-    }
-
-    .log-board {
-      @apply absolute pb-5 w-full;
-    }
-  }
-
-  .log-line {
-    @apply flex gap-2 whitespace-pre-wrap;
-  }
-}
-
-$duration: 3s;
-
-.magnifier {
-  @apply w-10 h-10;
-  animation: animloader $duration infinite;
-}
-
-@keyframes animloader {
-  0% {
-    transform: translate(-5px, -5px);
-  }
-  25% {
-    transform: translate(-5px, 5px);
-  }
-  50% {
-    transform: translate(5px, 5px);
-  }
-  75% {
-    transform: translate(5px, -5px);
-  }
-  100% {
-    transform: translate(-5px, -5px);
-  }
-}
-</style>
diff --git a/vue/src/views/help/HelpView.vue b/vue/src/views/help/HelpView.vue
deleted file mode 100644
index e4480480..00000000
--- a/vue/src/views/help/HelpView.vue
+++ /dev/null
@@ -1,13 +0,0 @@
-<template>
-  <div>help</div>
-</template>
-
-<script setup>
-/**
- * @description: 帮助页，
- * @file: HelpView.vue
- * @since: 2023-12-04 19:08:24
- **/
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/home/HomeView.vue b/vue/src/views/home/HomeView.vue
deleted file mode 100644
index d1d8627d..00000000
--- a/vue/src/views/home/HomeView.vue
+++ /dev/null
@@ -1,293 +0,0 @@
-<template>
-  <HomeLayout>
-    <template #top>
-      <!-- 项目创建时间、最近运行的时间、总实验数量 -->
-      <div class="w-80 flex flex-col gap-4 my-5 text-dimmer">
-        <div class="flex justify-between">
-          <div class="grow">{{ $t('home.overview.create') }}</div>
-          <p class="w-36 whitespace-nowrap">{{ createTime }}</p>
-        </div>
-        <div class="flex justify-between">
-          <div class="grow">{{ $t('home.overview.latest') }}</div>
-          <p class="w-36 whitespace-nowrap">{{ updateTime }}</p>
-        </div>
-        <div class="flex justify-between">
-          <div class="grow">{{ $t('home.overview.logdir') }}</div>
-          <p class="w-36 whitespace-nowrap">{{ projectStore.logdir }}</p>
-        </div>
-      </div>
-    </template>
-    <div class="pt-5 flex flex-col flex-grow flex-shrink-0">
-      <!-- 实验列表+实验统计 -->
-      <div class="flex mx-6 mb-4 items-center gap-1.5 flex-shrink-0">
-        <h2 class="text-xl font-semibold">{{ $t('home.list.title') }}</h2>
-        <span v-if="total" class="bg-highest text-default rounded-full px-3">{{ total }}</span>
-      </div>
-      <TableBar
-        class="pb-4 pt-1 px-5 flex-shrink-0"
-        :table-head="tableHead"
-        :table-body="tableBody"
-        :searchText="searchText"
-        :checked="onlySummary"
-        @update:checked="onlySummary = $event"
-        @update:searchText="searchText = $event"
-      />
-      <div class="w-full pb-10 flex flex-col overflow-scroll flex-grow basis-0" v-if="tags && init">
-        <!-- 实验表格 -->
-        <ExprTable sticky-header class="dashboard-table" :column="tableHead" :data="tableBody" last-row-gradient>
-          <template v-slot:name="{ row }">
-            <ExperimentName :name="row.name" :id="row.id" :color="getExperimentColor(row)" />
-          </template>
-          <template v-slot:status="{ row }">
-            <SLStatusLabel :name="row.name" :status="row.status" :url="'/experiment/' + row.id">
-              {{ $t('experiment.status.' + row.status) }}
-            </SLStatusLabel>
-          </template>
-          <template v-slot:create="{ row }">
-            {{ transTime(convertUtcToLocal(row.create_time)) }}
-          </template>
-          <template v-slot:duration="{ row }">
-            {{ getDuration(row) }}
-          </template>
-          <template v-for="item in configs" :key="item.slot" v-slot:[item.slot]="{ row }">
-            {{ row.config[item.slot]?.value != undefined ? row.config[item.slot]?.value : '-' }}
-          </template>
-        </ExprTable>
-      </div>
-      <EmptyTable v-else-if="experiments.length === 0 && init" />
-      <div v-else>
-        <div class="w-full flex" v-for="index in 3" :key="index">
-          <div v-for="i in 5" :key="i" class="skeleton w-full py-3 m-2"></div>
-        </div>
-      </div>
-    </div>
-  </HomeLayout>
-</template>
-
-<script setup>
-/**
- * @description: 首页视图，列出项目的基本信息
- * @file: HomeView.vue
- * @since: 2023-12-04 19:36:21
- **/
-import { useProjectStore } from '@swanlab-vue/store'
-import { formatTime, getDuration } from '@swanlab-vue/utils/time'
-import { computed, ref, onMounted } from 'vue'
-import SLStatusLabel from '@swanlab-vue/components/SLStatusLabel.vue'
-import ExperimentName from './components/ExperimentName.vue'
-import { transTime, convertUtcToLocal } from '@swanlab-vue/utils/time'
-import { t } from '@swanlab-vue/i18n'
-import http from '@swanlab-vue/api/http'
-import ExprTable from './components/ExprTable.vue'
-import EmptyTable from './components/EmptyTable.vue'
-import TableBar from './components/TableBar.vue'
-import { formatNumber2SN } from '@swanlab-vue/utils/common'
-
-const projectStore = useProjectStore()
-const experiments = computed(() => {
-  // 在最前面判断项目信息是否存在，不存在则是后端未开启/没有项目
-  if (typeof projectStore.experiments === 'undefined') {
-    return []
-  }
-  return projectStore.experiments
-})
-
-// ---------------------------------- 图表延迟 ----------------------------------
-
-const init = ref(false)
-onMounted(() => {
-  setTimeout(() => {
-    init.value = true
-  }, 500)
-})
-
-// ---------------------------------- 在此处处理项目创建时间、运行时间和总实验数量 ----------------------------------
-
-const createTime = computed(() => formatTime(projectStore.createTime))
-const updateTime = computed(() => formatTime(projectStore.updateTime))
-const total = computed(() => projectStore.experiments?.length || 0)
-
-// ---------------------------------- 表格配置 ----------------------------------
-
-const column = ref([
-  {
-    title: t('home.list.table.header.name'),
-    slot: 'name',
-    style: 'px-6',
-    width: 300,
-    border: true
-  },
-  {
-    title: t('home.list.table.header.status'),
-    slot: 'status',
-    type: 'status'
-  },
-  {
-    title: t('home.list.table.header.create'),
-    slot: 'create',
-    type: 'create_time'
-  },
-  {
-    title: t('home.list.table.header.duration'),
-    slot: 'duration',
-    type: 'duration'
-  }
-])
-
-// 遍历 experiments，添加配置
-const configs = ref([])
-;(() => {
-  // 寻找需要增加的表头
-  experiments.value.map((item) => {
-    Object.entries(item.config).forEach(([key]) => {
-      // 如果这个key已经存在configs中，跳过
-      if (configs.value.some((config) => config.title === key)) {
-        return
-      }
-      configs.value.push({
-        type: 'config',
-        slot: key,
-        title: key
-      })
-    })
-  })
-  // column.value.push(...configs.value)
-})()
-
-// ---------------------------------- 表格数据，同时还有tag的表头处理 ----------------------------------
-
-// 表格体数据
-const experiments_table = computed(() => {
-  return experiments.value.map((expr) => {
-    const summary = summaries.value[expr.name]
-    if (!summary) return {}
-    Promise.all(
-      Object.keys(summary).map(async (key) => {
-        expr[await hashString(key)] = isNaN(summary[key]) ? summary[key] : formatNumber2SN(summary[key])
-      })
-    )
-    return expr
-  })
-})
-
-// 项目里面的所有 tag 项，undefined 表示还没有初始化完，这个时候不加载表格
-const tags = ref()
-const summaries = ref({})
-http
-  .get('/project/summaries', {
-    params: {
-      // 传递前端显示的所有实验名称，使用字符串格式，每个实验名称之间使用逗号连接
-      experiment_names: (() => {
-        let experiment_names = []
-        experiments.value.forEach((experiment) => {
-          experiment_names.push(experiment.name)
-        })
-        return experiment_names.join(',')
-      })()
-    }
-  })
-  .then(async ({ data }) => {
-    // 增加tag对应的表头
-    tags.value = await Promise.all(
-      data.tags.map(async (tag) => {
-        const key = await hashString(tag)
-        return { key, title: tag }
-      })
-    )
-    // 保存tag总结数据
-    summaries.value = data.summaries
-  })
-
-// 哈希处理 key 避免和关键字重复
-async function hashString(inputString) {
-  // const encoder = new TextEncoder()
-  // const data = encoder.encode(inputString)
-
-  // const buffer = await crypto.subtle.digest('SHA-256', data)
-  // const hashArray = Array.from(new Uint8Array(buffer))
-  // const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join('')
-
-  // return hashHex
-  return 'swanlab-overview-table-key' + inputString
-}
-
-// ---------------------------------- 颜色选择 ----------------------------------
-
-const getExperimentColor = (experiment) => {
-  return experiment.light
-}
-// ---------------------------------- 筛选 ----------------------------------
-
-// 是否开启了表格筛选
-const onlySummary = ref(false)
-
-// 动态修改表头
-const tableHead = computed(() => {
-  if (!onlySummary.value) {
-    return [...column.value, ...configs.value, ...(tags.value || [])]
-  }
-
-  return [column.value[0], ...tags.value]
-})
-
-// ---------------------------------- 查找 ----------------------------------
-
-const searchText = ref('')
-
-const tableBody = computed(() => {
-  if (!searchText.value) {
-    return experiments_table.value
-  }
-  return experiments_table.value.filter((item) => {
-    return item.name.toLowerCase().includes(searchText.value.toLowerCase())
-  })
-})
-</script>
-
-<style lang="scss" scoped>
-// 最后一行每一个单元格右边框添加伪元素
-.dashboard-table {
-  @apply border-x-0 border-b-0;
-}
-.grow {
-  @apply text-dimmest;
-}
-
-@-webkit-keyframes skeleton-ani {
-  0% {
-    left: 0;
-  }
-
-  to {
-    left: 100%;
-  }
-}
-
-@keyframes skeleton-ani {
-  0% {
-    left: 0;
-  }
-
-  to {
-    left: 100%;
-  }
-}
-
-.skeleton {
-  background-image: linear-gradient(-45deg, #f5f5f5 40%, #fff 55%, #f5f5f5 63%);
-  list-style: none;
-  background-size: 400% 100%;
-  background-position: 100% 50%;
-  animation: skeleton-animation 2s ease infinite;
-}
-
-@keyframes skeleton-animation {
-  0% {
-    background-position: 100% 50%;
-  }
-
-  100% {
-    background-position: 0 50%;
-  }
-}
-</style>
diff --git a/vue/src/views/home/components/EmptyTable.vue b/vue/src/views/home/components/EmptyTable.vue
deleted file mode 100644
index 6c74523c..00000000
--- a/vue/src/views/home/components/EmptyTable.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-  <div class="flex-col justify-center items-center pt-10">
-    <div class="loader"></div>
-    <p class="text-center pt-10 text-lg">{{ $t('home.empty.table') }}</p>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 没有实验时的占位符
- * @file: EmptyTable.vue
- * @since: 2024-01-04 20:26:21
- **/
-</script>
-
-<style lang="scss" scoped>
-.loader {
-  width: 106px;
-  height: 56px;
-  display: block;
-  margin: 30px auto;
-  background-image: linear-gradient(#333 50px, transparent 0), linear-gradient(#333 50px, transparent 0),
-    linear-gradient(#333 50px, transparent 0), linear-gradient(#333 50px, transparent 0),
-    radial-gradient(circle 14px, #333 100%, transparent 0);
-  background-size: 48px 15px, 15px 35px, 15px 35px, 25px 15px, 28px 28px;
-  background-position: 25px 5px, 58px 20px, 25px 17px, 2px 37px, 76px 0px;
-  background-repeat: no-repeat;
-  position: relative;
-  transform: rotate(-45deg);
-  box-sizing: border-box;
-}
-
-.loader::after,
-.loader::before {
-  content: '';
-  position: absolute;
-  width: 56px;
-  height: 56px;
-  border: 6px solid #333;
-  border-radius: 50%;
-  left: -45px;
-  top: -10px;
-  background-repeat: no-repeat;
-  background-image: linear-gradient(#333 64px, transparent 0), linear-gradient(#333 66px, transparent 0),
-    radial-gradient(circle 4px, #333 100%, transparent 0);
-  background-size: 40px 1px, 1px 40px, 8px 8px;
-  background-position: center center;
-  box-sizing: border-box;
-  animation: rotation 0.3s linear infinite;
-}
-
-.loader::before {
-  left: 25px;
-  top: 60px;
-}
-
-@keyframes rotation {
-  0% {
-    transform: rotate(0deg);
-  }
-  100% {
-    transform: rotate(360deg);
-  }
-}
-</style>
diff --git a/vue/src/views/home/components/ExperimentName.vue b/vue/src/views/home/components/ExperimentName.vue
deleted file mode 100644
index 6bfeac75..00000000
--- a/vue/src/views/home/components/ExperimentName.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<template>
-  <RouterLink class="flex gap-2 items-center w-full" :to="`/experiment/${id}`">
-    <circle class="w-3 h-3 rounded-full flex-shrink-0" :style="{ backgroundColor: $props.color }" />
-    <p class="truncate" :title="name">{{ name }}</p>
-  </RouterLink>
-</template>
-
-<script setup>
-import { RouterLink } from 'vue-router'
-
-/**
- * @description: 展示实验名称和实验对应的颜色，并且可以完成实验跳转
- * @file: ExperimentName.vue
- * @since: 2023-12-08 20:16:08
- **/
-
-defineProps({
-  name: {
-    type: String,
-    required: true
-  },
-  color: {
-    type: String,
-    required: true
-  },
-  id: {
-    required: true
-  }
-})
-</script>
-
-<style lang="scss" scoped></style>
diff --git a/vue/src/views/home/components/ExprTable.vue b/vue/src/views/home/components/ExprTable.vue
deleted file mode 100644
index fa7ab632..00000000
--- a/vue/src/views/home/components/ExprTable.vue
+++ /dev/null
@@ -1,364 +0,0 @@
-<template>
-  <!-- 这一层是为了保证表格宽度，设计到边框长度、背景色长度等 -->
-  <div class="w-full" ref="wrapper">
-    <!-- 表格 -->
-    <div
-      :class="{ 'gradient-last-line': lastRowGradient, 'table-border': tableBorder }"
-      :style="{ width: typeof tableWidth === 'string' ? tableWidth : `${tableWidth}px` }"
-      ref="table"
-    >
-      <!-- 表头 -->
-      <div :class="{ 'sticky top-0 z-20': stickyHeader }">
-        <div class="border-y flex bg-higher table-header">
-          <!-- 表头项 -->
-          <div
-            v-for="(item, index) in column"
-            :key="item.key"
-            class="cell table-header-item"
-            :class="activeColumnIndex === index ? 'bg-highest' : 'bg-higher'"
-            @mouseover="handleMouseOver(index)"
-            @mouseout="handleMouseOver(-1)"
-            :style="{ width: elementWidths[index] }"
-            ref="columnsRef"
-          >
-            <p
-              class="overflow-hidden w-full h-full flex items-center"
-              :class="item.style || 'px-2 py-3'"
-              :title="item.title"
-            >
-              {{ item.title }}
-            </p>
-            <!-- 拖拽点 -->
-            <button
-              @mousedown="(e) => resize(e, index)"
-              :class="{ 'button-hover-tip': hoverColumnIndex === index }"
-              v-if="!column.unresizeable && !flexable"
-            />
-          </div>
-        </div>
-      </div>
-      <!-- 表体, 按一行一行来渲染 -->
-      <div
-        v-for="(dataColumn, dataIndex) in data"
-        :key="dataColumn"
-        :class="{ 'hover:bg-higher': resizeIndex === -1 }"
-        class="line"
-      >
-        <!-- 单元格 -->
-        <div
-          v-for="(item, index) in column"
-          :key="item.key"
-          :title="dataColumn[item.key]"
-          class="cell flex items-center px-2 py-3"
-          :class="[
-            item.style,
-            { 'hover:bg-primary-dimmest': resizeIndex === -1 },
-            activeColumnIndex === index ? 'bg-higher' : 'bg-default'
-          ]"
-          @mouseover="handleMouseOver(index)"
-          @mouseout="handleMouseOver(-1)"
-          :style="{ width: elementWidths[index] }"
-        >
-          <div class="w-full overflow-hidden" v-if="item.slot">
-            <slot :name="item.slot" v-bind:row="dataColumn" v-bind:index="dataIndex"></slot>
-          </div>
-          <!-- 文本格式 -->
-          <div class="w-full overflow-hidden" v-else>{{ dataColumn[item.key] || '-' }}</div>
-        </div>
-      </div>
-      <!-- 没有数据时，空占位 -->
-      <div v-if="!data.length" class="flex justify-center py-3 border border-t-0">
-        <div class="data-empty">{{ $t('common.table.empty') }}</div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 表格 —— 二次重构版
- * @file: ExprTable.vue
- * @since: 2023-12-28 22:37:21
- **/
-
-import { computed } from 'vue'
-import { onMounted } from 'vue'
-import { ref, watch, inject } from 'vue'
-
-// ---------------------------------- 组件接口 ----------------------------------
-
-const props = defineProps({
-  // 表格体的数据
-  data: {
-    type: Array,
-    default: () => {
-      return []
-    }
-  },
-  // 表头
-  column: {
-    type: Array,
-    default: () => {
-      return []
-    }
-  },
-  // 表格最大宽度
-  maxW: {
-    type: String,
-    default: 'none'
-  },
-  // 是否高亮预览
-  highLight: {
-    type: Boolean,
-    default: true
-  },
-  // 是否自动填充
-  flexable: {
-    type: Boolean,
-    default: false
-  },
-  // 最后一行渐变
-  lastRowGradient: {
-    type: Boolean,
-    default: false
-  },
-  // 表格边框,除了上方一直存在以外，其他边框都需要设置tableBorder时才出现
-  tableBorder: {
-    type: Boolean,
-    default: false
-  },
-  // 表头固定
-  stickyHeader: {
-    type: Boolean,
-    default: false
-  }
-})
-
-// ---------------------------------- 全局信息 ----------------------------------
-
-const columnsRef = ref(null)
-const widths = ref([])
-const table = ref(null)
-const wrapper = ref(null)
-
-/**
- * 动态计算表格宽度
- * 需要动态计算表格宽度的原因是，overflow 出现滚动条之后，子元素的 100% 将不会计算超出部分
- * 这会导致一些样式问题，而动态计算之后，只需要将表格元素的父元素设置 overflow 即可
- */
-const tableWidth = ref('100%')
-
-// 宽度转化，直接控制每列宽度
-const elementWidths = computed(() => {
-  if (props.flexable) {
-    const len = props.column.length
-    return new Array(len).fill((1 / len) * 100 + '%')
-  }
-  return widths.value.map((item) => {
-    return item?.value ? item?.value + 'px' : ''
-  })
-})
-
-// ---------------------------------- 样式相关 ----------------------------------
-
-const hoverColumnIndex = ref(-1) // 被hover得列的索引
-// 当前被选中的列的索引
-const activeColumnIndex = computed(() => {
-  return resizeIndex.value === -1 ? hoverColumnIndex.value : resizeIndex.value
-})
-
-onMounted(() => {
-  // 遍历列的设置
-  props.column.forEach((column, index) => {
-    const default_width = 150
-    widths.value[index] = {
-      value: 0,
-      extended: false
-    }
-    // 获取所有的列宽
-    // 如果自己设置了宽度
-    if (column.width) {
-      const target_width = column.width > default_width ? column.width : default_width
-      widths.value[index].value = column.border ? target_width - 2 : target_width
-      return
-    }
-    // 没自己设宽度，使用自然宽度
-    widths.value[index].value = default_width
-  })
-
-  // 监听视窗大小变化，以适配表格大小
-  handleTableWith()
-  window.addEventListener('resize', handleTableWith)
-})
-
-onUnmounted(() => {
-  window.removeEventListener('resize', handleTableWith)
-})
-
-/**
- * 动态计算表格宽度
- */
-const handleTableWith = () => {
-  let width = 0
-  widths.value.forEach(({ value }) => {
-    width += value
-  })
-  if (wrapper.value?.offsetWidth > width) {
-    if (wrapper.value.offsetWidth) tableWidth.value = wrapper.value.offsetWidth
-  } else {
-    tableWidth.value = width
-  }
-}
-
-const layoutSiderBar = inject('isSideBarShow')
-const siderWith = 288
-watch(layoutSiderBar, (newV) => {
-  if (props.flexable) return
-  if (!newV) {
-    tableWidth.value += siderWith
-  } else {
-    tableWidth.value -= siderWith
-  }
-})
-
-// ---------------------------------- resize 相关 ----------------------------------
-
-// 正在被重置宽度的列的索引
-const resizeIndex = ref(-1)
-// 开始时鼠标位置
-const startX = ref(0)
-// 开始时，目标列的宽度
-const startWidth = ref(0)
-// 最小的列宽
-const minCellWidth = ref(70)
-
-/**
- * 准备重置宽度
- * @param {object} event 点击事件对象
- * @param {number} index 目标列的索引
- */
-const resize = (event, index) => {
-  console.log(`准备重置第${index}的宽度`)
-  // 设置选取时不可以选中文本
-  document.body.style.userSelect = 'none'
-  document.body.style.cursor = 'col-resize'
-
-  // 记录被改变大小的列的索引
-  resizeIndex.value = index
-
-  // 记录初始化数据
-  startX.value = event.clientX
-  if (elementWidths.value[index].endsWith('%')) {
-    widths.value[index].value = columnsRef.value[index].offsetWidth
-  }
-  startWidth.value = widths.value[index].value
-
-  // 绑定全局事件
-  document.addEventListener('mousemove', handleMousemove)
-  document.addEventListener('mouseup', handleMouseup)
-}
-
-// 鼠标移动
-const handleMousemove = (e) => {
-  // 计算新宽度
-  let newWidth = startWidth.value + (e.clientX - startX.value)
-  // 对新宽度设置最小值
-  newWidth = newWidth < minCellWidth.value ? minCellWidth.value : newWidth
-  // 重置宽度
-  widths.value[resizeIndex.value].value = newWidth
-  columnsRef.value[resizeIndex.value] = newWidth
-}
-
-// 鼠标抬起，结束resize，清空状态，接触多余的事件绑定
-const handleMouseup = () => {
-  handleTableWith()
-  document.removeEventListener('mousemove', handleMousemove)
-  document.removeEventListener('mouseup', handleMouseup)
-  document.body.style.userSelect = 'auto'
-  document.body.style.cursor = ''
-  startX.value = 0
-  startWidth.value = 0
-  resizeIndex.value = -1
-}
-
-// ---------------------------------- 处理鼠标移入移出事件，设置颜色 ----------------------------------
-const handleMouseOver = (index) => {
-  hoverColumnIndex.value = index
-}
-</script>
-
-<style lang="scss" scoped>
-.gradient-last-line {
-  // 最后一行的cell添加伪元素渐变
-  .line:last-child .cell:first-child {
-    &:after {
-      content: '';
-      box-sizing: content-box;
-      position: absolute;
-      top: 100%;
-      left: 0;
-      height: 40px;
-      width: 100%;
-      border-right: 1px;
-      border-style: solid;
-      // 边框颜色从上到下渐变，从outline-default到transparent
-      border-image: linear-gradient(to bottom, var(--outline-default), transparent) 1;
-    }
-  }
-}
-
-.table-border {
-  .table-header {
-    @apply border-x;
-  }
-  .line {
-    @apply border-x;
-  }
-  .line:last-child {
-    @apply border-b;
-  }
-}
-
-.cell {
-  @apply text-left whitespace-nowrap shrink-0;
-  @apply box-border;
-  height: 54px;
-
-  &:first-child {
-    @apply sticky left-0 z-10 border-r;
-  }
-
-  &:not(:first-child) {
-    @apply z-0 relative;
-  }
-}
-
-.line {
-  @apply flex items-center;
-  &:hover {
-    .cell:not(:hover) {
-      background-color: var(--background-higher);
-    }
-  }
-}
-
-.table-header {
-  @apply relative;
-  .table-header-item {
-    &:hover {
-      button {
-        background-color: var(--foreground-dimmest);
-      }
-    }
-    button {
-      @apply w-1 h-full absolute right-0 top-0 cursor-col-resize;
-      @apply hover:w-2 hover:bg-primary-dimmer active:bg-primary-dimmer;
-      @apply transition-all duration-300;
-    }
-  }
-}
-
-.button-hover-tip {
-  background-color: var(--foreground-dimmest);
-}
-</style>
diff --git a/vue/src/views/home/components/TableBar.vue b/vue/src/views/home/components/TableBar.vue
deleted file mode 100644
index 9171348b..00000000
--- a/vue/src/views/home/components/TableBar.vue
+++ /dev/null
@@ -1,119 +0,0 @@
-<template>
-  <div class="w-full flex justify-between items-center">
-    <SLSearch
-      class="max-w-[400px]"
-      @input="(value) => $emit('update:searchText', value)"
-      :placeholder="$t('experiment.index.header.table-bar.placeholder')"
-    ></SLSearch>
-    <div class="flex gap-5 items-center pl-10">
-      <SLCheck
-        :label="$t('experiment.index.header.table-bar.summary')"
-        :checked="checked"
-        @update:checked="$emit('update:checked', $event)"
-      />
-      <SLButton
-        theme="default"
-        class="px-3 py-2 rounded-lg flex items-center gap-2"
-        @click="downloadCsv(tableHead, tableBody)"
-        hollow
-      >
-        <SLIcon icon="download" class="w-4 h-4" />
-        <span>{{ $t('experiment.index.header.table-bar.export') }}</span>
-      </SLButton>
-    </div>
-  </div>
-</template>
-
-<script setup>
-/**
- * @description: 表格上方功能栏：搜索、筛选、导出
- * @file: TableBar.vue
- * @since: 2024-01-16 17:52:07
- **/
-import SLCheck from '@swanlab-vue/components/SLCheck.vue'
-import { formatTime, getDuration } from '@swanlab-vue/utils/time'
-import { t } from '@swanlab-vue/i18n'
-import { getTimes } from '@swanlab-vue/utils/time'
-
-defineProps({
-  tableBody: {
-    type: Array,
-    default: () => []
-  },
-  tableHead: {
-    type: Array,
-    default: () => []
-  },
-  checked: {
-    type: Boolean,
-    default: false
-  },
-  searchText: {
-    type: String,
-    default: ''
-  }
-})
-
-defineEmits(['update:checked', 'update:searchText'])
-
-/**
- * @description 纯前端实现将表格数据导出为csv格式文件
- * @param {Array} headers 表格头配置项，headers中的key值与data中每一个item的属性名一一对应
- * @param {Array} data 表格数据
- * @param {String} fileName 导出的文件名称
- */
-function downloadCsv(header, data) {
-  if (!header || !data || !Array.isArray(header) || !Array.isArray(data) || !header.length || !data.length) {
-    return
-  }
-
-  // 拼接文件名
-  let { year, month, day, hour, minute, second } = getTimes(new Date().getTime() - 8 * 60 * 60 * 1000)
-  const fileName = `swanlab_experiments_${year}-${month}-${day}_${hour}-${minute}-${second}.csv`
-
-  var csvContent = 'data:text/csv;charset=utf-8,\ufeff'
-  // 头部数据，以逗号分隔
-  const _header = header.map((h) => h.title).join(',')
-  csvContent += _header + '\n'
-
-  // 遍历表体的每一行
-  data.forEach((item, index) => {
-    let dataString = ''
-    // 遍历一行中的每一列
-    for (let i = 0; i < header.length; i++) {
-      const head = header[i]
-      const label = head.key || head.slot
-      const value = head.type ? checkType(item, head.type, label) : item[label] || '-'
-      dataString += (typeof value === 'string' ? value.replace(/,/g, '，') : value) + ','
-    }
-    csvContent += index < data.length ? dataString.replace(/,$/, '\n') : dataString.replace(/,$/, '')
-  })
-
-  const a = document.createElement('a')
-  a.href = encodeURI(csvContent)
-  a.download = fileName
-  a.click()
-  window.URL.revokeObjectURL(csvContent)
-}
-
-/**
- * 特殊情况，即有些数据是需要处理，或具有特殊层级
- * @param {obj} item 对应行的数据对象
- * @param {string} type 类型
- * @param {string} key key 或 slot
- */
-const checkType = (item, type, key) => {
-  switch (type) {
-    case 'config':
-      return item.config[key] ? item.config[key].value : '-'
-    case 'create_time':
-      return formatTime(item.create_time)
-    case 'status':
-      return t(`experiment.status.${item.status}`)
-    case 'duration':
-      return getDuration(item)
-  }
-}
-</script>
-
-<style lang="scss" scoped></style>


Test patch:
diff --git a/test/start_server.py b/test/start_server.py
index 4175938b..a2c37867 100644
--- a/test/start_server.py
+++ b/test/start_server.py
@@ -11,8 +11,8 @@
 """
 import uvicorn
 from tutils.config import SWANLAB_LOG_DIR
-from swanlab.db import connect
-from swanlab.server.app import app
+from swanboard.db import connect
+from swanboard.app import app
 import os
 
 os.environ["SWANLAB_LOG_DIR"] = SWANLAB_LOG_DIR
diff --git a/test/unit/cli/pytest_cli_login.py b/test/unit/cli/pytest_cli_login.py
new file mode 100644
index 00000000..5fb24989
--- /dev/null
+++ b/test/unit/cli/pytest_cli_login.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/13 01:11
+@File: pytest_login.py
+@IDE: pycharm
+@Description:
+    测试命令登录
+"""
+
+from swanlab.env import get_swanlab_folder
+from swanlab.package import get_key, get_host_api
+from click.testing import CliRunner
+from swanlab.cli.main import cli
+from tutils import KEY
+from swanlab.error import ValidationError
+import os
+
+
+# noinspection PyTypeChecker
+def test_login_ok():
+    runner = CliRunner()
+
+    result = runner.invoke(cli, ["login", "--api-key", KEY])
+    assert result.exit_code == 0
+    path = os.path.join(get_swanlab_folder(), ".netrc")
+    assert os.path.exists(path)
+    assert get_key(path, get_host_api())[2] == KEY
+
+
+# noinspection PyTypeChecker
+def test_login_fail():
+    runner = CliRunner()
+    result = runner.invoke(cli, ["login", "--api-key", "123"])
+    assert result.exit_code == 1
+    assert isinstance(result.exception, ValidationError)
diff --git a/test/unit/cli/pytest_cli_logout.py b/test/unit/cli/pytest_cli_logout.py
new file mode 100644
index 00000000..f8122c00
--- /dev/null
+++ b/test/unit/cli/pytest_cli_logout.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/13 01:28
+@File: pytest_logout.py
+@IDE: pycharm
+@Description:
+    测试登出模块
+"""
+from click.testing import CliRunner
+from swanlab.cli import cli
+from tutils import KEY
+
+
+# noinspection PyTypeChecker
+def test_logout_ok(monkeypatch):
+    runner = CliRunner()
+    # 先登录
+    runner.invoke(cli, ["login", "--api-key", KEY])
+    monkeypatch.setattr("builtins.input", lambda x: "y")
+    result = runner.invoke(cli, ["logout"])
+    assert result.exit_code == 0
+
+
+# noinspection PyTypeChecker
+def test_logout_cancel(monkeypatch):
+    runner = CliRunner()
+    # 先登录
+    runner.invoke(cli, ["login", "--api-key", KEY])
+    monkeypatch.setattr("builtins.input", lambda x: "n")
+    result = runner.invoke(cli, ["logout"])
+    assert result.exit_code == 0
+
+
+# noinspection PyTypeChecker
+def test_logout_no_login():
+    runner = CliRunner()
+    result = runner.invoke(cli, ["logout"])
+    assert result.exit_code == 1
+    assert "You are not logged in." in result.output
diff --git a/test/unit/cli/pytest_cli_watch.py b/test/unit/cli/pytest_cli_watch.py
new file mode 100644
index 00000000..18094a38
--- /dev/null
+++ b/test/unit/cli/pytest_cli_watch.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+r"""
+@DATE: 2024/6/11 22:06
+@File: pytest_watch.py
+@IDE: pycharm
+@Description:
+    测试cli的watch命令
+"""
+import os
+import time
+import pytest
+from swanlab.cli import cli
+from click.testing import CliRunner
+import multiprocessing
+import requests
+import swanlab
+from tutils import TEMP_PATH
+from swanlab.env import get_swanlog_dir, reset_env, ROOT
+
+
+def mock_swanlog(path=None):
+    if path is None:
+        path = get_swanlog_dir()
+    os.makedirs(path, exist_ok=True)
+    reset_env()
+    swanlab.init(logdir=path, mode="local")
+    swanlab.log({"test": 1})
+    swanlab.finish()
+
+
+# 运行任务
+# noinspection PyTypeChecker
+def runner_watch(*args):
+    runner = CliRunner()
+    return runner.invoke(cli, ["watch", *args])
+
+
+# 测试能否ping通
+def ping(host="127.0.0.1", port=5092):
+    url = f"http://{host}:{port}"  # noqa
+    time.sleep(3)
+    response = requests.get(url, timeout=10)
+    assert response.status_code == 200
+
+
+@pytest.mark.parametrize("logdir, ping_args, args", [
+    # 无参数
+    [None, [], []],
+    # 指定logdir
+    [os.path.join(TEMP_PATH, "watch", "swanlog"), [], ["--logdir", os.path.join(TEMP_PATH, "watch", "swanlog")]],
+    # 指定host和port
+    [None, ["0.0.0.0", "5093"], ["--host", "0.0.0.0", "--port", "5093"]],
+    # 直接watch
+    [os.path.join(TEMP_PATH, "watch", "swanlog"), [], [os.path.join(TEMP_PATH, "watch", "swanlog")]],
+])
+def test_watch_ok(logdir, ping_args, args):
+    """
+    测试watch命令，正常情况
+    """
+    mock_swanlog(logdir)
+    p1 = multiprocessing.Process(target=runner_watch, args=args)
+    p2 = multiprocessing.Process(target=ping, args=ping_args)
+    p1.start()
+    p2.start()
+    p2.join()
+    p1.kill()
+    assert p2.exitcode == 0
+
+
+def test_watch_wrong_logdir():
+    """
+    测试watch命令，logdir不存在
+    """
+    result = runner_watch("wrong_logdir")
+    assert result.exit_code == 2
+    result = runner_watch("--logdir", "wrong_logdir")
+    assert result.exit_code == 2
+    reset_env()
+    os.environ[ROOT] = "wrong_logdir"
+    result = runner_watch()
+    assert result.exit_code == 2
+
+
+def test_watch_wrong_host_port():
+    """
+    测试watch命令，host和port错误
+    """
+    result = runner_watch("--host", "")
+    assert result.exit_code == 6
+    result = runner_watch("--port", "0")
+    assert result.exit_code == 2
+    result = runner_watch("--port", "65536")
+    assert result.exit_code == 2
+    # 如果ip被占用，会报错
+    p1 = multiprocessing.Process(target=runner_watch, args=["--port", "5092"])
+    p1.start()
+    time.sleep(3)
+    result = runner_watch("--port", "5092")
+    assert result.exit_code == 7
+    p1.kill()
diff --git a/test/unit/conftest.py b/test/unit/conftest.py
index 3b802128..b41df11e 100644
--- a/test/unit/conftest.py
+++ b/test/unit/conftest.py
@@ -8,16 +8,43 @@
     配置pytest
 """
 import pytest
-from tutils import clear, init_db, SWANLAB_DIR, SWANLAB_LOG_DIR, PACKAGE_PATH
+from tutils import clear, init_db, SWANLAB_DIR, SWANLAB_LOG_DIR, PACKAGE_PATH, TEMP_PATH
 import swanlab.env as E
 import shutil
 import os
 
 
+def count_files_in_directory(directory, exclude_prefixes=("__", ".", "~")):
+    """
+    计算目录下的文件数量
+    """
+    file_count, folder_count = 0, 0
+    for filename in os.listdir(directory):
+        file_path = os.path.join(directory, filename)
+        if os.path.isfile(file_path):
+            if any(filename.startswith(prefix) for prefix in exclude_prefixes):
+                continue
+            file_count += 1
+        elif os.path.isdir(file_path):
+            folder_count += 1
+            r = count_files_in_directory(file_path)
+            file_count += r[0]
+            folder_count += r[1]
+    return file_count, folder_count
+
+
 @pytest.fixture(scope="session", autouse=True)
 def setup_before_all():
     clear()
     init_db()
+    current_dir = os.path.dirname(os.path.abspath(__file__))
+    # 记住当前文件夹下的所有文件数量，用于测试结束后比较，确保测试结束后没有多余文件
+    pre_file_count, pre_folder_count = count_files_in_directory(current_dir)
+    yield
+    # 确保测试结束后没有多余文件
+    now_file_count, now_folder_count = count_files_in_directory(current_dir)
+    assert pre_file_count == now_file_count
+    assert pre_folder_count == now_folder_count
 
 
 @pytest.fixture(scope="function", autouse=True)
@@ -31,3 +58,4 @@ def setup_before_each():
     os.environ[E.DEV] = "TRUE"
     os.environ[E.ROOT] = SWANLAB_LOG_DIR
     os.environ[E.PACKAGE] = PACKAGE_PATH
+    os.environ[E.HOME] = TEMP_PATH
diff --git a/test/unit/data/pytest_sdk.py b/test/unit/data/pytest_sdk.py
index 9dbee356..b3cdbe3c 100644
--- a/test/unit/data/pytest_sdk.py
+++ b/test/unit/data/pytest_sdk.py
@@ -59,6 +59,7 @@ def test_init_local(self):
         assert get_run() is not None
 
     def test_init_cloud(self):
+        S.login(T.KEY)
         run = S.init(mode="cloud")
         assert os.environ[MODE] == "cloud"
         run.log({"TestInitMode": 1})  # 不会报错
@@ -88,6 +89,7 @@ def test_init_local_env(self):
 
     def test_init_cloud_env(self):
         os.environ[MODE] = "cloud"
+        S.login(T.KEY)
         run = S.init()
         assert os.environ[MODE] == "cloud"
         run.log({"TestInitMode": 1})
diff --git a/test/unit/pytest_env.py b/test/unit/pytest_env.py
index cfe2a692..4b44f4c7 100644
--- a/test/unit/pytest_env.py
+++ b/test/unit/pytest_env.py
@@ -7,12 +7,7 @@
 @Description:
     测试swanlab/env.py中的环境变量和工具函数
 """
-from swanlab.env import (
-    SwanLabMode,
-    get_mode,
-    reset_env,
-    MODE
-)
+from swanlab.env import SwanLabMode, get_mode, reset_env, MODE
 import swanlab.env as E
 from nanoid import generate
 from tutils import SWANLAB_LOG_DIR, PACKAGE_PATH, TEMP_PATH
@@ -136,14 +131,6 @@ def test_exist_ra_false(self):
             E.assert_exist(os.path.dirname(__file__), target_type="file", ra=False)
 
 
-def test_db_path():
-    """
-    测试数据库路径
-    """
-    os.environ[E.ROOT] = SWANLAB_LOG_DIR
-    assert E.get_db_path() == os.path.join(SWANLAB_LOG_DIR, "runs.swanlab")
-
-
 def test_env_reset():
     os.environ[E.ROOT] = SWANLAB_LOG_DIR
     E.init_env()
@@ -165,46 +152,6 @@ def test_is_dev():
     assert E.is_dev() is False
 
 
-class TestGetServer:
-    """
-    测试由环境变量设置服务器地址
-    """
-
-    def test_default_host(self):
-        """
-        测试默认地址
-        """
-        assert E.get_server_host() == "127.0.0.1"
-
-    def test_use_env_host(self):
-        """
-        测试使用环境变量
-        """
-        os.environ[E.HOST] = "127.0.0.2"
-        assert E.get_server_host() == "127.0.0.2"
-        reset_env()
-        os.environ[E.HOST] = generate()
-        with pytest.raises(ValueError):
-            E.get_server_host()
-
-    def test_default_port(self):
-        """
-        测试默认端口
-        """
-        assert E.get_server_port() == 5092
-
-    def test_use_env_port(self):
-        """
-        测试使用环境变量
-        """
-        os.environ[E.PORT] = "5093"
-        assert E.get_server_port() == 5093
-        reset_env()
-        os.environ[E.PORT] = generate()
-        with pytest.raises(ValueError):
-            E.get_server_port()
-
-
 class TestSwanLogDir:
 
     def test_default(self):
@@ -264,6 +211,7 @@ def test_use_env_dev(self):
         assert E.get_package_path() == os.environ[E.PACKAGE]
         # 需要注意的是测试时已经引入了swanlab包，而其默认执行了get_package_path函数
         from swanlab.package import package_path
+
         assert E.get_package_path() != package_path
         assert package_path == PACKAGE_PATH
 
diff --git a/test/unit/server/controller/utils/utils.py b/test/unit/server/controller/utils/utils.py
deleted file mode 100644
index 0bc30c2b..00000000
--- a/test/unit/server/controller/utils/utils.py
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-r"""
-@DATE: 2024/4/14 15:44
-@File: utils.py
-@IDE: pycharm
-@Description:
-    测试控制器中的工具
-"""
-import pytest
-from swanlab.server.controller.utils import *
-import random
-
-
-class TestLTTB:
-    @pytest.mark.parametrize("data, sample_length", [
-        ([{"data": random.random(), "index": i} for i in range(100)], 100),
-        ([{"data": random.random(), "index": i} for i in range(1500)], 1500),
-        ([{"data": random.random(), "index": i} for i in range(1502)], 1502),
-    ])
-    def test_no_sample(self, data, sample_length):
-        """
-        测试数据长度小于等于采样长度
-        """
-        result = lttb(data)
-        assert len(result) == len(data)
-
-    @pytest.mark.parametrize("data, sample_length", [
-        ([{"data": random.random(), "index": i} for i in range(1503)], 1500),
-        ([{"data": random.random(), "index": i} for i in range(1600)], 1500),
-        ([{"data": random.random(), "index": i} for i in range(5000)], 1500),
-    ])
-    def test_sample(self, data, sample_length):
-        """
-        测试数据长度大于采样长度
-        """
-        result = lttb(data)
-        assert len(result) == sample_length
diff --git a/vitest.config.ts b/vitest.config.ts
deleted file mode 100644
index b3c02c6a..00000000
--- a/vitest.config.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { defineConfig } from 'vitest/config'
-import path from 'path'
-
-export default defineConfig({
-  test: {
-    globals: true,
-    alias: {
-      '@swanlab-vue': path.resolve(__dirname, 'vue/src')
-    }
-  }
-})
diff --git a/vue/test/unit/utils/common.test.js b/vue/test/unit/utils/common.test.js
deleted file mode 100644
index b7138c86..00000000
--- a/vue/test/unit/utils/common.test.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import { uuid, formatNumber2SN } from '@swanlab-vue/utils/common'
-import { describe, test, expect } from 'vitest'
-
-describe('common.js utils', () => {
-  test('generate uuid', () => {
-    expect(uuid()).toHaveLength(10)
-  })
-
-  /**
-   * 对于格式化数据的测试
-   * 具体的格式规则可见 /vue/utils/common.js
-   * 这里的测试主要针对几个类型：
-   * 1. 正负数
-   * 2. 区间内的小数(小数点后的精度)
-   * 3. 区间内绝对值大于1的数(是否带小数部分)
-   * 4. 在第3点的基础上，小数位超出额定范围，抛弃小数
-   * 5. 超出区间的极小数
-   * 6. 超出区间的极大数
-   */
-
-  test('formatNumber2SN test for interval fraction numbers', () => {
-    const result1 = formatNumber2SN(0.12345)
-    expect(result1).toBe('0.1234')
-
-    const result2 = formatNumber2SN(-0.123456)
-    expect(result2).toBe('-0.1234')
-
-    const result3 = formatNumber2SN(-0.000000123456)
-    // 注意！这里有四舍五入
-    expect(result3).toBe('-0.0000001235')
-  })
-
-  test('formatNumber test1 for interval medium sized numbers', () => {
-    // 正常的正负数，直接输出
-    const result1 = formatNumber2SN(123312312)
-    expect(result1).toBe('123312312')
-
-    const result2 = formatNumber2SN(-21312312)
-    expect(result2).toBe('-21312312')
-
-    // 对小数部分，只截取到第一个非零数及其后三位，且自动去除无意义的0
-    const result3 = formatNumber2SN(1233.0403000001)
-    expect(result3).toBe('1233.0403')
-
-    const result4 = formatNumber2SN(-1233.0000123456)
-    expect(result4).toBe('-1233.00001234')
-
-    // 小数部分全为0，则只取整数部分
-    const result5 = formatNumber2SN(123321.0)
-    expect(result5).toBe('123321')
-  })
-
-  test('formatNumber test2 for interval medium sized numbers', () => {
-    const result1 = formatNumber2SN(12.00000000000001)
-    expect(result1).toBe('12')
-
-    const result2 = formatNumber2SN(-12.00000000000001)
-    expect(result2).toBe('-12')
-  })
-
-  test('formatNumber test for extreme decimals out of range', () => {
-    const result1 = formatNumber2SN(0.0000000000000000001234512)
-    expect(result1).toBe('1.2345e-19')
-
-    const result2 = formatNumber2SN(-0.0000000000000000001234512)
-    expect(result2).toBe('-1.2345e-19')
-  })
-
-  test('formatNumber test for maximum number exceeding the interval', () => {
-    const result1 = formatNumber2SN(1010000000000123)
-    expect(result1).toBe('1.01e+15')
-
-    const result2 = formatNumber2SN(-1010000000000123)
-    expect(result2).toBe('-1.01e+15')
-  })
-})