xiangyang 1 månad sedan
incheckning
e39371c108

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
dist/assets/Settings-Bf2-gp6a.js


+ 1 - 0
dist/assets/Settings-BtVU0MC4.css

@@ -0,0 +1 @@
+.settings-container[data-v-1d253436]{padding:20px}.settings-card[data-v-1d253436]{margin-top:20px}.card-header[data-v-1d253436]{display:flex;justify-content:space-between;align-items:center}.settings-content[data-v-1d253436]{padding:20px 0}.setting-item[data-v-1d253436]{display:flex;justify-content:space-between;align-items:center;padding:15px 0;border-bottom:1px solid #e6e6e6}.setting-item[data-v-1d253436]:last-child{border-bottom:none}.setting-label[data-v-1d253436]{font-size:14px;color:#606266}.dialog-content[data-v-1d253436]{padding:10px 0}.dialog-footer[data-v-1d253436]{display:flex;justify-content:flex-end;gap:10px}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
dist/assets/index-CkG-X7Uw.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 5 - 0
dist/assets/index-tieXVPxi.js


+ 14 - 0
dist/index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>角色管理系统</title>
+    <script type="module" crossorigin src="./assets/index-tieXVPxi.js"></script>
+    <link rel="stylesheet" crossorigin href="./assets/index-CkG-X7Uw.css">
+  </head>
+  <body>
+    <div id="app"></div>
+  </body>
+</html>

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>角色管理系统</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 1717 - 0
package-lock.json

@@ -0,0 +1,1717 @@
+{
+  "name": "anycallvue",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "anycallvue",
+      "version": "0.1.0",
+      "dependencies": {
+        "@element-plus/icons-vue": "^2.1.0",
+        "axios": "^1.11.0",
+        "dayjs": "^1.11.18",
+        "element-plus": "^2.3.9",
+        "vue": "^3.3.4",
+        "vue-router": "^4.5.1"
+      },
+      "devDependencies": {
+        "@vitejs/plugin-vue": "^6.0.1",
+        "vite": "^7.1.4"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+      "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.28.3",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
+      "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
+      "dependencies": {
+        "@babel/types": "^7.28.2"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.28.2",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
+      "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+      "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@element-plus/icons-vue": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+      "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
+      "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
+      "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
+      "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
+      "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
+      "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
+      "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
+      "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
+      "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
+      "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
+      "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
+      "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
+      "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
+      "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
+      "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
+      "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
+      "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
+      "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
+      "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
+      "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
+      "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
+      "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
+      "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
+      "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
+      "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
+      "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
+      "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@floating-ui/core": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+      "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.10"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+      "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+      "dependencies": {
+        "@floating-ui/core": "^1.7.3",
+        "@floating-ui/utils": "^0.2.10"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.10",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+      "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
+    },
+    "node_modules/@popperjs/core": {
+      "name": "@sxzz/popperjs-es",
+      "version": "2.11.7",
+      "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
+      "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-beta.29",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz",
+      "integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==",
+      "dev": true
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.0.tgz",
+      "integrity": "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.0.tgz",
+      "integrity": "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.0.tgz",
+      "integrity": "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.0.tgz",
+      "integrity": "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.0.tgz",
+      "integrity": "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.0.tgz",
+      "integrity": "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.0.tgz",
+      "integrity": "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.0.tgz",
+      "integrity": "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.0.tgz",
+      "integrity": "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.0.tgz",
+      "integrity": "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.0.tgz",
+      "integrity": "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.0.tgz",
+      "integrity": "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.0.tgz",
+      "integrity": "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.0.tgz",
+      "integrity": "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.0.tgz",
+      "integrity": "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.0.tgz",
+      "integrity": "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.0.tgz",
+      "integrity": "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.0.tgz",
+      "integrity": "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.0.tgz",
+      "integrity": "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.0.tgz",
+      "integrity": "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.0.tgz",
+      "integrity": "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true
+    },
+    "node_modules/@types/lodash": {
+      "version": "4.17.20",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
+      "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="
+    },
+    "node_modules/@types/lodash-es": {
+      "version": "4.17.12",
+      "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+      "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+      "dependencies": {
+        "@types/lodash": "*"
+      }
+    },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.16",
+      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
+      "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ=="
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
+      "integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==",
+      "dev": true,
+      "dependencies": {
+        "@rolldown/pluginutils": "1.0.0-beta.29"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.21",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz",
+      "integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==",
+      "dependencies": {
+        "@babel/parser": "^7.28.3",
+        "@vue/shared": "3.5.21",
+        "entities": "^4.5.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.21",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz",
+      "integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.21",
+        "@vue/shared": "3.5.21"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.21",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz",
+      "integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==",
+      "dependencies": {
+        "@babel/parser": "^7.28.3",
+        "@vue/compiler-core": "3.5.21",
+        "@vue/compiler-dom": "3.5.21",
+        "@vue/compiler-ssr": "3.5.21",
+        "@vue/shared": "3.5.21",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.18",
+        "postcss": "^8.5.6",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.21",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz",
+      "integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.21",
+        "@vue/shared": "3.5.21"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.21",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.21.tgz",
+      "integrity": "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==",
+      "dependencies": {
+        "@vue/shared": "3.5.21"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.21",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.21.tgz",
+      "integrity": "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==",
+      "dependencies": {
+        "@vue/reactivity": "3.5.21",
+        "@vue/shared": "3.5.21"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.21",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz",
+      "integrity": "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==",
+      "dependencies": {
+        "@vue/reactivity": "3.5.21",
+        "@vue/runtime-core": "3.5.21",
+        "@vue/shared": "3.5.21",
+        "csstype": "^3.1.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.21",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.21.tgz",
+      "integrity": "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.21",
+        "@vue/shared": "3.5.21"
+      },
+      "peerDependencies": {
+        "vue": "3.5.21"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.21",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.21.tgz",
+      "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw=="
+    },
+    "node_modules/@vueuse/core": {
+      "version": "9.13.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz",
+      "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.16",
+        "@vueuse/metadata": "9.13.0",
+        "@vueuse/shared": "9.13.0",
+        "vue-demi": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/core/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "9.13.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
+      "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "9.13.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
+      "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
+      "dependencies": {
+        "vue-demi": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+      "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.4",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.18",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
+      "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
+      "license": "MIT"
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/element-plus": {
+      "version": "2.11.1",
+      "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.11.1.tgz",
+      "integrity": "sha512-weYFIniyNXTAe9vJZnmZpYzurh4TDbdKhBsJwhbzuo0SDZ8PLwHVll0qycJUxc6SLtH+7A9F7dvdDh5CnqeIVA==",
+      "dependencies": {
+        "@ctrl/tinycolor": "^3.4.1",
+        "@element-plus/icons-vue": "^2.3.1",
+        "@floating-ui/dom": "^1.0.1",
+        "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
+        "@types/lodash": "^4.14.182",
+        "@types/lodash-es": "^4.17.6",
+        "@vueuse/core": "^9.1.0",
+        "async-validator": "^4.2.5",
+        "dayjs": "^1.11.13",
+        "escape-html": "^1.0.3",
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.21",
+        "lodash-unified": "^1.0.2",
+        "memoize-one": "^6.0.0",
+        "normalize-wheel-es": "^1.2.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
+      "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
+      "dev": true,
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.9",
+        "@esbuild/android-arm": "0.25.9",
+        "@esbuild/android-arm64": "0.25.9",
+        "@esbuild/android-x64": "0.25.9",
+        "@esbuild/darwin-arm64": "0.25.9",
+        "@esbuild/darwin-x64": "0.25.9",
+        "@esbuild/freebsd-arm64": "0.25.9",
+        "@esbuild/freebsd-x64": "0.25.9",
+        "@esbuild/linux-arm": "0.25.9",
+        "@esbuild/linux-arm64": "0.25.9",
+        "@esbuild/linux-ia32": "0.25.9",
+        "@esbuild/linux-loong64": "0.25.9",
+        "@esbuild/linux-mips64el": "0.25.9",
+        "@esbuild/linux-ppc64": "0.25.9",
+        "@esbuild/linux-riscv64": "0.25.9",
+        "@esbuild/linux-s390x": "0.25.9",
+        "@esbuild/linux-x64": "0.25.9",
+        "@esbuild/netbsd-arm64": "0.25.9",
+        "@esbuild/netbsd-x64": "0.25.9",
+        "@esbuild/openbsd-arm64": "0.25.9",
+        "@esbuild/openbsd-x64": "0.25.9",
+        "@esbuild/openharmony-arm64": "0.25.9",
+        "@esbuild/sunos-x64": "0.25.9",
+        "@esbuild/win32-arm64": "0.25.9",
+        "@esbuild/win32-ia32": "0.25.9",
+        "@esbuild/win32-x64": "0.25.9"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+      "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+    },
+    "node_modules/lodash-es": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+    },
+    "node_modules/lodash-unified": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
+      "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+      "peerDependencies": {
+        "@types/lodash-es": "*",
+        "lodash": "*",
+        "lodash-es": "*"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.18",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
+      "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/normalize-wheel-es": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+      "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw=="
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.6",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
+    "node_modules/rollup": {
+      "version": "4.50.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.0.tgz",
+      "integrity": "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==",
+      "dev": true,
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.50.0",
+        "@rollup/rollup-android-arm64": "4.50.0",
+        "@rollup/rollup-darwin-arm64": "4.50.0",
+        "@rollup/rollup-darwin-x64": "4.50.0",
+        "@rollup/rollup-freebsd-arm64": "4.50.0",
+        "@rollup/rollup-freebsd-x64": "4.50.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.50.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.50.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.50.0",
+        "@rollup/rollup-linux-arm64-musl": "4.50.0",
+        "@rollup/rollup-linux-loongarch64-gnu": "4.50.0",
+        "@rollup/rollup-linux-ppc64-gnu": "4.50.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.50.0",
+        "@rollup/rollup-linux-riscv64-musl": "4.50.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.50.0",
+        "@rollup/rollup-linux-x64-gnu": "4.50.0",
+        "@rollup/rollup-linux-x64-musl": "4.50.0",
+        "@rollup/rollup-openharmony-arm64": "4.50.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.50.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.50.0",
+        "@rollup/rollup-win32-x64-msvc": "4.50.0",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.14",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
+      "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+      "dev": true,
+      "dependencies": {
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/vite": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.4.tgz",
+      "integrity": "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw==",
+      "dev": true,
+      "dependencies": {
+        "esbuild": "^0.25.0",
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3",
+        "postcss": "^8.5.6",
+        "rollup": "^4.43.0",
+        "tinyglobby": "^0.2.14"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "lightningcss": "^1.21.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.21",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz",
+      "integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.21",
+        "@vue/compiler-sfc": "3.5.21",
+        "@vue/runtime-dom": "3.5.21",
+        "@vue/server-renderer": "3.5.21",
+        "@vue/shared": "3.5.21"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.5.1",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
+      "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    }
+  }
+}

+ 22 - 0
package.json

@@ -0,0 +1,22 @@
+{
+  "name": "anycallvue",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.1.0",
+    "axios": "^1.11.0",
+    "dayjs": "^1.11.18",
+    "element-plus": "^2.3.9",
+    "vue": "^3.3.4",
+    "vue-router": "^4.5.1"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^6.0.1",
+    "vite": "^7.1.4"
+  }
+}

+ 8 - 0
src/.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/src.iml" filepath="$PROJECT_DIR$/.idea/src.iml" />
+    </modules>
+  </component>
+</project>

+ 38 - 0
src/.idea/workspace.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ChangeListManager">
+    <list default="true" id="ca420f58-7720-4c19-b244-f543bc82cc9d" name="Changes" comment="" />
+    <option name="SHOW_DIALOG" value="false" />
+    <option name="HIGHLIGHT_CONFLICTS" value="true" />
+    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
+    <option name="LAST_RESOLUTION" value="IGNORE" />
+  </component>
+  <component name="ProjectId" id="3292YDDgq4sdAT2zwksikzHOQ4J" />
+  <component name="ProjectViewState">
+    <option name="hideEmptyMiddlePackages" value="true" />
+    <option name="showLibraryContents" value="true" />
+  </component>
+  <component name="PropertiesComponent">
+    <property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
+    <property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
+    <property name="WebServerToolWindowFactoryState" value="false" />
+    <property name="last_opened_file_path" value="$PROJECT_DIR$" />
+    <property name="settings.editor.selected.configurable" value="preferences.pluginManager" />
+    <property name="vue.rearranger.settings.migration" value="true" />
+  </component>
+  <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
+  <component name="TaskManager">
+    <task active="true" id="Default" summary="Default task">
+      <changelist id="ca420f58-7720-4c19-b244-f543bc82cc9d" name="Changes" comment="" />
+      <created>1756818512485</created>
+      <option name="number" value="Default" />
+      <option name="presentableId" value="Default" />
+      <updated>1756818512485</updated>
+      <workItem from="1756818513579" duration="3000" />
+    </task>
+    <servers />
+  </component>
+  <component name="TypeScriptGeneratedFilesManager">
+    <option name="version" value="3" />
+  </component>
+</project>

+ 1624 - 0
src/AdminDashboard.vue

@@ -0,0 +1,1624 @@
+<template>
+  <div id="app">
+    <!-- 路由视图 -->
+    <router-view />
+  </div>
+</template>
+
+<script>
+import { createApp } from 'vue';
+import { createRouter, createWebHistory } from 'vue-router';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import 'element-plus/dist/index.css';
+
+// 导入Element Plus图标
+import { 
+  Home, User, Music, Star, Plus, Upload, 
+  Edit, Delete, Logout, ArrowUp, ArrowDown 
+} from '@element-plus/icons-vue';
+
+// 模拟数据存储
+const store = {
+  // 用户信息
+  user: null,
+  
+  // 角色数据
+  roles: [
+
+  ],
+  
+  // 声音数据
+  voices: [
+    {
+      id: 1,
+      originalVoice: "original-alice.mp3",
+      clonedVoice: "cloned-alice.mp3",
+      gender: "female",
+      description: "Clear female voice with warm tone",
+      uploadTime: "2023-01-15"
+    },
+    {
+      id: 2,
+      originalVoice: "original-bob.mp3",
+      clonedVoice: "cloned-bob.mp3",
+      gender: "male",
+      description: "Deep male voice, professional style",
+      uploadTime: "2023-01-20"
+    },
+    {
+      id: 3,
+      originalVoice: "original-zhanghua.mp3",
+      clonedVoice: "cloned-zhanghua.mp3",
+      gender: "male",
+      description: "Standard Mandarin male voice",
+      uploadTime: "2023-02-05"
+    }
+  ],
+  
+  // 推荐数据
+  recommendations: [
+    {
+      id: 1,
+      roleId: 1,
+      callCount: 150,
+      sortOrder: 1
+    },
+    {
+      id: 2,
+      roleId: 3,
+      callCount: 120,
+      sortOrder: 2
+    }
+  ],
+  
+  // 登录
+  login(username, password) {
+    // 模拟登录验证
+    if (username && password) {
+      this.user = { id: 1, username, role: "admin" };
+      return true;
+    }
+    return false;
+  },
+  
+  // 注册
+  register(username, password, email) {
+    // 模拟注册
+    if (username && password && email) {
+      return true;
+    }
+    return false;
+  },
+  
+  // 登出
+  logout() {
+    this.user = null;
+  },
+  
+  // 角色管理
+  addRole(role) {
+    const newId = this.roles.length > 0 
+      ? Math.max(...this.roles.map(r => r.id)) + 1 
+      : 1;
+    const newRole = { ...role, id: newId, createdAt: new Date().toISOString().split('T')[0] };
+    this.roles.push(newRole);
+    return newRole;
+  },
+  
+  updateRole(updatedRole) {
+    const index = this.roles.findIndex(r => r.id === updatedRole.id);
+    if (index !== -1) {
+      this.roles[index] = { ...this.roles[index], ...updatedRole };
+      return true;
+    }
+    return false;
+  },
+  
+  deleteRole(id) {
+    const index = this.roles.findIndex(r => r.id === id);
+    if (index !== -1) {
+      this.roles.splice(index, 1);
+      // 同时删除相关推荐
+      this.recommendations = this.recommendations.filter(r => r.roleId !== id);
+      return true;
+    }
+    return false;
+  },
+  
+  // 声音管理
+  addVoice(voice) {
+    const newId = this.voices.length > 0 
+      ? Math.max(...this.voices.map(v => v.id)) + 1 
+      : 1;
+    const newVoice = { 
+      ...voice, 
+      id: newId, 
+      uploadTime: new Date().toISOString().split('T')[0] 
+    };
+    this.voices.push(newVoice);
+    return newVoice;
+  },
+  
+  // 推荐管理
+  addRecommendation(recommendation) {
+    // 检查角色是否存在
+    const roleExists = this.roles.some(r => r.id === recommendation.roleId);
+    if (!roleExists) return false;
+    
+    // 检查是否已存在该角色的推荐
+    const existingIndex = this.recommendations.findIndex(r => r.roleId === recommendation.roleId);
+    if (existingIndex !== -1) {
+      // 更新现有推荐
+      this.recommendations[existingIndex] = {
+        ...this.recommendations[existingIndex],
+        callCount: recommendation.callCount
+      };
+      return this.recommendations[existingIndex];
+    } else {
+      // 添加新推荐
+      const newId = this.recommendations.length > 0 
+        ? Math.max(...this.recommendations.map(r => r.id)) + 1 
+        : 1;
+      const newSortOrder = this.recommendations.length + 1;
+      const newRecommendation = { 
+        ...recommendation, 
+        id: newId,
+        sortOrder: newSortOrder
+      };
+      this.recommendations.push(newRecommendation);
+      return newRecommendation;
+    }
+  },
+  
+  updateRecommendationSortOrder(id, newSortOrder) {
+    const index = this.recommendations.findIndex(r => r.id === id);
+    if (index !== -1) {
+      // 调整其他推荐的排序
+      this.recommendations.forEach(rec => {
+        if (rec.id !== id) {
+          if (newSortOrder < rec.sortOrder) {
+            rec.sortOrder += 1;
+          }
+        }
+      });
+      this.recommendations[index].sortOrder = newSortOrder;
+      // 重新排序
+      this.recommendations.sort((a, b) => a.sortOrder - b.sortOrder);
+      return true;
+    }
+    return false;
+  },
+  
+  deleteRecommendation(id) {
+    const index = this.recommendations.findIndex(r => r.id === id);
+    if (index !== -1) {
+      const deletedSortOrder = this.recommendations[index].sortOrder;
+      this.recommendations.splice(index, 1);
+      // 调整剩余推荐的排序
+      this.recommendations.forEach(rec => {
+        if (rec.sortOrder > deletedSortOrder) {
+          rec.sortOrder -= 1;
+        }
+      });
+      return true;
+    }
+    return false;
+  }
+};
+
+// 路由守卫
+const requireAuth = (to, from, next) => {
+  if (!store.user && to.path !== '/login' && to.path !== '/register') {
+    next('/login');
+  } else {
+    next();
+  }
+};
+
+// 登录页面组件
+const LoginPage = {
+  template: `
+    <div class="login-container">
+      <el-card class="login-card">
+        <div slot="header" class="login-header">
+          <h2>管理后台登录</h2>
+        </div>
+        <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
+          <el-form-item prop="username">
+            <el-input v-model="loginForm.username" placeholder="用户名">
+              <template #prefix>
+                <el-icon class="el-input__icon"><User /></el-icon>
+              </template>
+            </el-input>
+          </el-form-item>
+          <el-form-item prop="password">
+            <el-input v-model="loginForm.password" type="password" placeholder="密码">
+              <template #prefix>
+                <el-icon class="el-input__icon"><Lock /></el-icon>
+              </template>
+            </el-input>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="handleLogin" class="login-button">登录</el-button>
+            <el-link type="primary" @click="$router.push('/register')" class="register-link">注册</el-link>
+          </el-form-item>
+        </el-form>
+      </el-card>
+    </div>
+  `,
+  data() {
+    return {
+      loginForm: {
+        username: '',
+        password: ''
+      },
+      loginRules: {
+        username: [
+          { required: true, message: '请输入用户名', trigger: 'blur' }
+        ],
+        password: [
+          { required: true, message: '请输入密码', trigger: 'blur' }
+        ]
+      }
+    };
+  },
+  methods: {
+    handleLogin() {
+      this.$refs.loginForm.validate((valid) => {
+        if (valid) {
+          const { username, password } = this.loginForm;
+          if (store.login(username, password)) {
+            ElMessage.success('登录成功');
+            this.$router.push('/dashboard');
+          } else {
+            ElMessage.error('用户名或密码错误');
+          }
+        }
+      });
+    }
+  },
+  components: {
+    User,
+    Lock: {
+      template: '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>'
+    }
+  }
+};
+
+// 注册页面组件
+const RegisterPage = {
+  template: `
+    <div class="register-container">
+      <el-card class="register-card">
+        <div slot="header" class="register-header">
+          <h2>用户注册</h2>
+        </div>
+        <el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form">
+          <el-form-item prop="username">
+            <el-input v-model="registerForm.username" placeholder="用户名">
+              <template #prefix>
+                <el-icon class="el-input__icon"><User /></el-icon>
+              </template>
+            </el-input>
+          </el-form-item>
+          <el-form-item prop="email">
+            <el-input v-model="registerForm.email" type="email" placeholder="邮箱">
+              <template #prefix>
+                <el-icon class="el-input__icon"><Mail /></el-icon>
+              </template>
+            </el-input>
+          </el-form-item>
+          <el-form-item prop="password">
+            <el-input v-model="registerForm.password" type="password" placeholder="密码">
+              <template #prefix>
+                <el-icon class="el-input__icon"><Lock /></el-icon>
+              </template>
+            </el-input>
+          </el-form-item>
+          <el-form-item prop="confirmPassword">
+            <el-input v-model="registerForm.confirmPassword" type="password" placeholder="确认密码">
+              <template #prefix>
+                <el-icon class="el-input__icon"><Lock /></el-icon>
+              </template>
+            </el-input>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="handleRegister" class="register-button">注册</el-button>
+            <el-link type="primary" @click="$router.push('/login')" class="login-link">已有账号?登录</el-link>
+          </el-form-item>
+        </el-form>
+      </el-card>
+    </div>
+  `,
+  data() {
+    return {
+      registerForm: {
+        username: '',
+        email: '',
+        password: '',
+        confirmPassword: ''
+      },
+      registerRules: {
+        username: [
+          { required: true, message: '请输入用户名', trigger: 'blur' }
+        ],
+        email: [
+          { required: true, message: '请输入邮箱', trigger: 'blur' },
+          { type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur', 'change'] }
+        ],
+        password: [
+          { required: true, message: '请输入密码', trigger: 'blur' },
+          { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
+        ],
+        confirmPassword: [
+          { required: true, message: '请确认密码', trigger: 'blur' },
+          { 
+            validator: (rule, value, callback) => {
+              if (value !== this.registerForm.password) {
+                callback(new Error('两次输入密码不一致'));
+              } else {
+                callback();
+              }
+            },
+            trigger: 'blur'
+          }
+        ]
+      }
+    };
+  },
+  methods: {
+    handleRegister() {
+      this.$refs.registerForm.validate((valid) => {
+        if (valid) {
+          const { username, password, email } = this.registerForm;
+          if (store.register(username, password, email)) {
+            ElMessage.success('注册成功,请登录');
+            this.$router.push('/login');
+          } else {
+            ElMessage.error('注册失败,请重试');
+          }
+        }
+      });
+    }
+  },
+  components: {
+    User,
+    Lock: {
+      template: '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>'
+    },
+    Mail: {
+      template: '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>'
+    }
+  }
+};
+
+// 主布局组件
+const MainLayout = {
+  template: `
+    <div class="main-layout">
+      <!-- 顶部导航栏 -->
+      <el-header class="main-header">
+        <div class="logo">
+          <el-icon><Star /></el-icon>
+          <span>角色管理系统</span>
+        </div>
+        <div class="user-info">
+          <span>{{ store.user.username }}</span>
+          <el-button type="text" @click="handleLogout" class="logout-btn">
+            <el-icon><Logout /></el-icon>
+          </el-button>
+        </div>
+      </el-header>
+      
+      <div class="main-content">
+        <!-- 侧边栏 -->
+        <el-aside class="sidebar">
+          <el-menu 
+            default-active="1" 
+            class="el-menu-vertical-demo"
+            @select="handleMenuSelect"
+            background-color="#0f172a"
+            text-color="#94a3b8"
+            active-text-color="#ffffff"
+          >
+            <el-menu-item index="1">
+              <el-icon><Home /></el-icon>
+              <span>首页</span>
+            </el-menu-item>
+            <el-menu-item index="2">
+              <el-icon><User /></el-icon>
+              <span>角色管理</span>
+            </el-menu-item>
+            <el-menu-item index="3">
+              <el-icon><Music /></el-icon>
+              <span>声音管理</span>
+            </el-menu-item>
+            <el-menu-item index="4">
+              <el-icon><Star /></el-icon>
+              <span>推荐管理</span>
+            </el-menu-item>
+          </el-menu>
+        </el-aside>
+        
+        <!-- 内容区域 -->
+        <el-main class="content-area">
+          <router-view />
+        </el-main>
+      </div>
+    </div>
+  `,
+  data() {
+    return {
+      store
+    };
+  },
+  methods: {
+    handleMenuSelect(index) {
+      switch(index) {
+        case '1':
+          this.$router.push('/dashboard');
+          break;
+        case '2':
+          this.$router.push('/roles');
+          break;
+        case '3':
+          this.$router.push('/voices');
+          break;
+        case '4':
+          this.$router.push('/recommendations');
+          break;
+      }
+    },
+    handleLogout() {
+      ElMessageBox.confirm(
+        '确定要退出登录吗?',
+        '确认退出',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'info'
+        }
+      ).then(() => {
+        store.logout();
+        ElMessage.success('已退出登录');
+        this.$router.push('/login');
+      }).catch(() => {
+        // 取消操作
+      });
+    }
+  },
+  components: {
+    Home, User, Music, Star, Logout
+  }
+};
+
+// 仪表盘组件
+const DashboardPage = {
+  template: `
+    <div class="dashboard">
+      <h1>欢迎使用角色管理系统</h1>
+      
+      <div class="stats-container">
+        <el-card class="stat-card">
+          <div class="stat-icon">
+            <el-icon><User /></el-icon>
+          </div>
+          <div class="stat-info">
+            <div class="stat-title">角色总数</div>
+            <div class="stat-value">{{ rolesCount }}</div>
+          </div>
+        </el-card>
+        
+        <el-card class="stat-card">
+          <div class="stat-icon">
+            <el-icon><Music /></el-icon>
+          </div>
+          <div class="stat-info">
+            <div class="stat-title">声音总数</div>
+            <div class="stat-value">{{ voicesCount }}</div>
+          </div>
+        </el-card>
+        
+        <el-card class="stat-card">
+          <div class="stat-icon">
+            <el-icon><Star /></el-icon>
+          </div>
+          <div class="stat-info">
+            <div class="stat-title">推荐角色数</div>
+            <div class="stat-value">{{ recommendationsCount }}</div>
+          </div>
+        </el-card>
+      </div>
+      
+      <div class="recent-activity">
+        <el-card>
+          <div slot="header">
+            <span>最近活动</span>
+          </div>
+          <el-timeline>
+            <el-timeline-item timestamp="今天 14:30">
+              创建了新角色 "John"
+            </el-timeline-item>
+            <el-timeline-item timestamp="昨天 10:15">
+              上传了新声音 "female-voice-03.mp3"
+            </el-timeline-item>
+            <el-timeline-item timestamp="2023-06-10 09:45">
+              更新了推荐列表
+            </el-timeline-item>
+          </el-timeline>
+        </el-card>
+      </div>
+    </div>
+  `,
+  data() {
+    return {
+      rolesCount: store.roles.length,
+      voicesCount: store.voices.length,
+      recommendationsCount: store.recommendations.length
+    };
+  },
+  components: {
+    Home, User, Music, Star
+  }
+};
+
+// 角色管理组件
+const RolesPage = {
+  template: `
+    <div class="roles-page">
+      <div class="page-header">
+        <h1>角色管理</h1>
+        <div class="action-buttons">
+          <el-button type="primary" @click="showCreateRoleDialog" class="btn-create">
+            <el-icon><Plus /></el-icon> 创建角色
+          </el-button>
+          <el-upload
+            class="upload-excel"
+            action=""
+            :auto-upload="false"
+            :on-change="handleExcelUpload"
+            accept=".xlsx, .xls"
+          >
+            <el-button type="success">
+              <el-icon><Upload /></el-icon> 导入角色
+            </el-button>
+          </el-upload>
+        </div>
+      </div>
+      
+      <el-card>
+        <el-table :data="roles" border class="roles-table">
+          <el-table-column prop="id" label="ID" width="80"></el-table-column>
+          <el-table-column label="头像" width="100">
+            <template #default="scope">
+              <el-avatar :src="scope.row.avatar" size="large"></el-avatar>
+            </template>
+          </el-table-column>
+          <el-table-column prop="name" label="姓名"></el-table-column>
+          <el-table-column prop="gender" label="性别" width="100">
+            <template #default="scope">
+              <el-tag :type="scope.row.gender === 'male' ? 'info' : scope.row.gender === 'female' ? 'success' : 'warning'">
+                {{ scope.row.gender === 'male' ? '男' : scope.row.gender === 'female' ? '女' : '中性' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="language" label="语种" width="120"></el-table-column>
+          <el-table-column prop="createdAt" label="创建时间" width="140"></el-table-column>
+          <el-table-column label="操作" width="180">
+            <template #default="scope">
+              <el-button type="text" @click="showEditRoleDialog(scope.row)" class="btn-edit">
+                <el-icon><Edit /></el-icon> 编辑
+              </el-button>
+              <el-button type="text" danger @click="handleDeleteRole(scope.row.id)" class="btn-delete">
+                <el-icon><Delete /></el-icon> 删除
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-card>
+      
+      <!-- 创建/编辑角色对话框 -->
+      <el-dialog 
+        :title="isEditing ? '编辑角色' : '创建角色'" 
+        v-model="roleDialogVisible"
+        width="600px"
+        :before-close="handleDialogClose"
+      >
+        <el-form 
+          ref="roleForm" 
+          :model="roleForm" 
+          :rules="roleRules"
+          label-width="100px"
+          class="role-form"
+        >
+          <el-form-item prop="name" label="角色姓名">
+            <el-input v-model="roleForm.name"></el-input>
+          </el-form-item>
+          
+          <el-form-item prop="description" label="人设描述">
+            <el-input 
+              v-model="roleForm.description" 
+              type="textarea" 
+              rows="3"
+            ></el-input>
+          </el-form-item>
+          
+          <el-form-item prop="gender" label="性别">
+            <el-select v-model="roleForm.gender" placeholder="请选择性别">
+              <el-option label="男" value="male"></el-option>
+              <el-option label="女" value="female"></el-option>
+              <el-option label="中性" value="neutral"></el-option>
+            </el-select>
+          </el-form-item>
+          
+          <el-form-item prop="language" label="使用语种">
+            <el-select v-model="roleForm.language" placeholder="请选择语种">
+              <el-option label="中文" value="Chinese"></el-option>
+              <el-option label="英文" value="English"></el-option>
+              <el-option label="日语" value="Japanese"></el-option>
+              <el-option label="韩语" value="Korean"></el-option>
+            </el-select>
+          </el-form-item>
+          
+          <el-form-item label="头像">
+            <el-upload
+              class="avatar-uploader"
+              action=""
+              :auto-upload="false"
+              :on-change="handleAvatarUpload"
+              accept="image/*"
+            >
+              <img 
+                v-if="roleForm.avatar" 
+                :src="roleForm.avatar" 
+                class="avatar"
+              >
+              <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+            </el-upload>
+          </el-form-item>
+          
+          <el-form-item label="声音文件">
+            <el-upload
+              class="voice-uploader"
+              action=""
+              :auto-upload="false"
+              :on-change="handleVoiceUpload"
+              accept="audio/*"
+            >
+              <el-button size="small" type="primary">
+                <el-icon><Upload /></el-icon> 上传声音文件
+              </el-button>
+              <div v-if="voiceFileName" class="voice-file-name">{{ voiceFileName }}</div>
+            </el-upload>
+          </el-form-item>
+        </el-form>
+        
+        <template #footer>
+          <el-button @click="roleDialogVisible = false">取消</el-button>
+          <el-button 
+            type="primary" 
+            @click="isEditing ? updateRole() : createRole()"
+          >
+            确定
+          </el-button>
+        </template>
+      </el-dialog>
+      
+      <!-- 导入确认对话框 -->
+      <el-dialog 
+        title="导入角色" 
+        v-model="importDialogVisible"
+        width="500px"
+      >
+        <div v-if="importFileName" class="import-file-info">
+          即将导入文件: {{ importFileName }}
+        </div>
+        <div v-else>
+          请先选择Excel文件
+        </div>
+        
+        <template #footer>
+          <el-button @click="importDialogVisible = false">取消</el-button>
+          <el-button 
+            type="primary" 
+            @click="confirmImport"
+            :disabled="!importFileName"
+          >
+            确认导入
+          </el-button>
+        </template>
+      </el-dialog>
+    </div>
+  `,
+  data() {
+    return {
+      roles: store.roles,
+      roleDialogVisible: false,
+      importDialogVisible: false,
+      isEditing: false,
+      currentRoleId: null,
+      roleForm: {
+        name: '',
+        description: '',
+        gender: '',
+        language: '',
+        avatar: '',
+        voiceFile: null
+      },
+      voiceFileName: '',
+      importFileName: '',
+      roleRules: {
+        name: [
+          { required: true, message: '请输入角色姓名', trigger: 'blur' }
+        ],
+        description: [
+          { required: true, message: '请输入人设描述', trigger: 'blur' }
+        ],
+        gender: [
+          { required: true, message: '请选择性别', trigger: 'change' }
+        ],
+        language: [
+          { required: true, message: '请选择语种', trigger: 'change' }
+        ]
+      }
+    };
+  },
+  methods: {
+    showCreateRoleDialog() {
+      this.isEditing = false;
+      this.currentRoleId = null;
+      this.roleForm = {
+        name: '',
+        description: '',
+        gender: '',
+        language: '',
+        avatar: '',
+        voiceFile: null
+      };
+      this.voiceFileName = '';
+      this.roleDialogVisible = true;
+    },
+    
+    showEditRoleDialog(role) {
+      this.isEditing = true;
+      this.currentRoleId = role.id;
+      this.roleForm = {
+        name: role.name,
+        description: role.description,
+        gender: role.gender,
+        language: role.language,
+        avatar: role.avatar,
+        voiceFile: null
+      };
+      this.voiceFileName = ''; // 重置声音文件名
+      this.roleDialogVisible = true;
+    },
+    
+    handleAvatarUpload(file) {
+      // 模拟处理图片上传
+      const reader = new FileReader();
+      reader.onload = (e) => {
+        this.roleForm.avatar = e.target.result;
+      };
+      reader.readAsDataURL(file.raw);
+    },
+    
+    handleVoiceUpload(file) {
+      this.voiceFileName = file.name;
+      // 实际项目中应上传文件到服务器
+    },
+    
+    createRole() {
+      this.$refs.roleForm.validate((valid) => {
+        if (valid) {
+          // 模拟声音文件处理
+          let newVoiceId = null;
+          if (this.voiceFileName) {
+            // 模拟添加声音
+            const newVoice = {
+              originalVoice: this.voiceFileName,
+              clonedVoice: `cloned-${this.voiceFileName}`,
+              gender: this.roleForm.gender,
+              description: `Voice for ${this.roleForm.name}`
+            };
+            const addedVoice = store.addVoice(newVoice);
+            newVoiceId = addedVoice.id;
+          }
+          
+          // 创建角色
+          const newRole = {
+            ...this.roleForm,
+            voiceId: newVoiceId
+          };
+          store.addRole(newRole);
+          
+          this.roleDialogVisible = false;
+          ElMessage.success('角色创建成功');
+          this.roles = [...store.roles]; // 刷新列表
+        }
+      });
+    },
+    
+    updateRole() {
+      this.$refs.roleForm.validate((valid) => {
+        if (valid) {
+          // 处理声音文件更新
+          if (this.voiceFileName) {
+            // 模拟更新声音
+          }
+          
+          // 更新角色
+          const updatedRole = {
+            id: this.currentRoleId,
+            ...this.roleForm
+          };
+          store.updateRole(updatedRole);
+          
+          this.roleDialogVisible = false;
+          ElMessage.success('角色更新成功');
+          this.roles = [...store.roles]; // 刷新列表
+        }
+      });
+    },
+    
+    handleDeleteRole(id) {
+      ElMessageBox.confirm(
+        '确定要删除这个角色吗?删除后相关数据也会被移除。',
+        '确认删除',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }
+      ).then(() => {
+        if (store.deleteRole(id)) {
+          ElMessage.success('角色删除成功');
+          this.roles = [...store.roles]; // 刷新列表
+        } else {
+          ElMessage.error('角色删除失败');
+        }
+      }).catch(() => {
+        // 取消删除
+      });
+    },
+    
+    handleExcelUpload(file) {
+      this.importFileName = file.name;
+      this.importDialogVisible = true;
+    },
+    
+    confirmImport() {
+      // 模拟导入处理
+      ElMessage.success(`文件 ${this.importFileName} 导入成功`);
+      this.importDialogVisible = false;
+      this.importFileName = '';
+      // 实际项目中应解析Excel文件并添加角色
+    },
+    
+    handleDialogClose() {
+      this.$refs.roleForm.resetFields();
+    }
+  },
+  components: {
+    Plus, Upload, Edit, Delete
+  }
+};
+
+// 声音管理组件
+const VoicesPage = {
+  template: `
+    <div class="voices-page">
+      <div class="page-header">
+        <h1>声音管理</h1>
+        <el-button type="primary" @click="showUploadVoiceDialog" class="btn-upload">
+          <el-icon><Upload /></el-icon> 上传新声音
+        </el-button>
+      </div>
+      
+      <el-card>
+        <el-table :data="voices" border class="voices-table">
+          <el-table-column prop="id" label="声音ID" width="100"></el-table-column>
+          <el-table-column prop="gender" label="性别" width="100">
+            <template #default="scope">
+              <el-tag :type="scope.row.gender === 'male' ? 'info' : scope.row.gender === 'female' ? 'success' : 'warning'">
+                {{ scope.row.gender === 'male' ? '男' : scope.row.gender === 'female' ? '女' : '中性' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="原始声音">
+            <template #default="scope">
+              <div class="audio-player">
+                <span class="audio-name">{{ scope.row.originalVoice }}</span>
+                <audio 
+                  :src="getAudioSrc(scope.row.originalVoice)" 
+                  controls 
+                  class="audio-control"
+                  preload="none"
+                ></audio>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="克隆声音">
+            <template #default="scope">
+              <div class="audio-player">
+                <span class="audio-name">{{ scope.row.clonedVoice }}</span>
+                <audio 
+                  :src="getAudioSrc(scope.row.clonedVoice)" 
+                  controls 
+                  class="audio-control"
+                  preload="none"
+                ></audio>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column prop="description" label="描述"></el-table-column>
+          <el-table-column prop="uploadTime" label="上传时间" width="140"></el-table-column>
+        </el-table>
+      </el-card>
+      
+      <!-- 上传声音对话框 -->
+      <el-dialog 
+        title="上传新声音" 
+        v-model="uploadVoiceDialogVisible"
+        width="600px"
+      >
+        <el-form 
+          ref="voiceForm" 
+          :model="voiceForm" 
+          :rules="voiceRules"
+          label-width="100px"
+        >
+          <el-form-item prop="gender" label="声音性别">
+            <el-select v-model="voiceForm.gender" placeholder="请选择性别">
+              <el-option label="男" value="male"></el-option>
+              <el-option label="女" value="female"></el-option>
+              <el-option label="中性" value="neutral"></el-option>
+            </el-select>
+          </el-form-item>
+          
+          <el-form-item prop="description" label="声音描述">
+            <el-input 
+              v-model="voiceForm.description" 
+              type="textarea" 
+              rows="3"
+            ></el-input>
+          </el-form-item>
+          
+          <el-form-item prop="voiceFile" label="声音文件">
+            <el-upload
+              class="voice-uploader"
+              action=""
+              :auto-upload="false"
+              :on-change="handleVoiceFileUpload"
+              accept="audio/*"
+            >
+              <el-button size="small" type="primary">
+                <el-icon><Upload /></el-icon> 选择声音文件
+              </el-button>
+              <div v-if="voiceFileName" class="voice-file-name">{{ voiceFileName }}</div>
+            </el-upload>
+          </el-form-item>
+        </el-form>
+        
+        <template #footer>
+          <el-button @click="uploadVoiceDialogVisible = false">取消</el-button>
+          <el-button 
+            type="primary" 
+            @click="uploadVoice"
+          >
+            上传并克隆
+          </el-button>
+        </template>
+      </el-dialog>
+    </div>
+  `,
+  data() {
+    return {
+      voices: store.voices,
+      uploadVoiceDialogVisible: false,
+      voiceForm: {
+        gender: '',
+        description: '',
+        voiceFile: null
+      },
+      voiceFileName: '',
+      voiceRules: {
+        gender: [
+          { required: true, message: '请选择声音性别', trigger: 'change' }
+        ],
+        description: [
+          { required: true, message: '请输入声音描述', trigger: 'blur' }
+        ],
+        voiceFile: [
+          { required: true, message: '请选择声音文件', trigger: 'change' }
+        ]
+      }
+    };
+  },
+  methods: {
+    showUploadVoiceDialog() {
+      this.voiceForm = {
+        gender: '',
+        description: '',
+        voiceFile: null
+      };
+      this.voiceFileName = '';
+      this.uploadVoiceDialogVisible = true;
+    },
+    
+    handleVoiceFileUpload(file) {
+      this.voiceFileName = file.name;
+      this.voiceForm.voiceFile = file;
+    },
+    
+    uploadVoice() {
+      this.$refs.voiceForm.validate((valid) => {
+        if (valid) {
+          // 模拟上传和克隆过程
+          this.$message.success('正在上传并克隆声音...');
+          
+          // 模拟异步处理
+          setTimeout(() => {
+            const newVoice = {
+              originalVoice: this.voiceFileName,
+              clonedVoice: `cloned-${this.voiceFileName}`,
+              gender: this.voiceForm.gender,
+              description: this.voiceForm.description
+            };
+            
+            store.addVoice(newVoice);
+            this.voices = [...store.voices]; // 刷新列表
+            
+            this.uploadVoiceDialogVisible = false;
+            ElMessage.success('声音上传和克隆成功');
+          }, 1500);
+        }
+      });
+    },
+    
+    getAudioSrc(fileName) {
+      // 实际项目中应返回真实的音频URL
+      return `audio/${fileName}`;
+    }
+  },
+  components: {
+    Upload
+  }
+};
+
+// 推荐管理组件
+const RecommendationsPage = {
+  template: `
+    <div class="recommendations-page">
+      <div class="page-header">
+        <h1>推荐管理</h1>
+        <el-button type="primary" @click="showAddRecommendationDialog" class="btn-add">
+          <el-icon><Plus /></el-icon> 添加推荐
+        </el-button>
+      </div>
+      
+      <el-card>
+        <el-table :data="formattedRecommendations" border class="recommendations-table">
+          <el-table-column prop="sortOrder" label="排序序号" width="100"></el-table-column>
+          <el-table-column label="角色头像" width="100">
+            <template #default="scope">
+              <el-avatar :src="scope.row.avatar" size="large"></el-avatar>
+            </template>
+          </el-table-column>
+          <el-table-column prop="name" label="角色姓名"></el-table-column>
+          <el-table-column prop="callCount" label="通话次数" width="120"></el-table-column>
+          <el-table-column prop="roleId" label="角色ID" width="100"></el-table-column>
+          <el-table-column label="排序调整" width="120">
+            <template #default="scope">
+              <div class="sort-buttons">
+                <el-button 
+                  icon="ArrowUp" 
+                  size="mini" 
+                  @click="moveUp(scope.row.recId)"
+                  :disabled="scope.row.sortOrder === 1"
+                ></el-button>
+                <el-button 
+                  icon="ArrowDown" 
+                  size="mini" 
+                  @click="moveDown(scope.row.recId)"
+                  :disabled="scope.row.sortOrder === formattedRecommendations.length"
+                ></el-button>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="180">
+            <template #default="scope">
+              <el-button type="text" @click="showEditRecommendationDialog(scope.row)" class="btn-edit">
+                <el-icon><Edit /></el-icon> 编辑
+              </el-button>
+              <el-button type="text" danger @click="handleDeleteRecommendation(scope.row.recId)" class="btn-delete">
+                <el-icon><Delete /></el-icon> 删除
+              </el-button>
+            </template>
+          </el-table>
+        </el-table>
+      </el-card>
+      
+      <!-- 添加/编辑推荐对话框 -->
+      <el-dialog 
+        :title="isEditingRec ? '编辑推荐' : '添加推荐'" 
+        v-model="recommendationDialogVisible"
+        width="500px"
+      >
+        <el-form 
+          ref="recommendationForm" 
+          :model="recommendationForm" 
+          :rules="recommendationRules"
+          label-width="100px"
+        >
+          <el-form-item prop="roleId" label="角色ID">
+            <el-input 
+              v-model="recommendationForm.roleId" 
+              type="number"
+              placeholder="请输入角色ID"
+            ></el-input>
+            <el-button 
+              type="text" 
+              @click="showRoleSelectionDialog"
+              v-if="!isEditingRec"
+              class="select-role-btn"
+            >
+              选择角色
+            </el-button>
+          </el-form-item>
+          
+          <el-form-item v-if="selectedRole" label="角色信息">
+            <div class="selected-role-info">
+              <el-avatar :src="selectedRole.avatar" size="small"></el-avatar>
+              <span>{{ selectedRole.name }} - {{ selectedRole.description }}</span>
+            </div>
+          </el-form-item>
+          
+          <el-form-item prop="callCount" label="通话次数">
+            <el-input 
+              v-model="recommendationForm.callCount" 
+              type="number"
+              placeholder="请输入通话次数"
+            ></el-input>
+          </el-form-item>
+        </el-form>
+        
+        <template #footer>
+          <el-button @click="recommendationDialogVisible = false">取消</el-button>
+          <el-button 
+            type="primary" 
+            @click="isEditingRec ? updateRecommendation() : addRecommendation()"
+          >
+            确定
+          </el-button>
+        </template>
+      </el-dialog>
+      
+      <!-- 角色选择对话框 -->
+      <el-dialog 
+        title="选择角色" 
+        v-model="roleSelectionDialogVisible"
+        width="700px"
+      >
+        <el-table 
+          :data="availableRoles" 
+          border
+          @row-click="selectRoleFromTable"
+          class="select-role-table"
+        >
+          <el-table-column prop="id" label="ID" width="80"></el-table-column>
+          <el-table-column label="头像" width="80">
+            <template #default="scope">
+              <el-avatar :src="scope.row.avatar" size="small"></el-avatar>
+            </template>
+          </el-table-column>
+          <el-table-column prop="name" label="姓名"></el-table-column>
+          <el-table-column prop="description" label="描述"></el-table-column>
+          <el-table-column prop="gender" label="性别" width="80">
+            <template #default="scope">
+              <el-tag :type="scope.row.gender === 'male' ? 'info' : scope.row.gender === 'female' ? 'success' : 'warning'">
+                {{ scope.row.gender === 'male' ? '男' : scope.row.gender === 'female' ? '女' : '中性' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+        </el-table>
+        
+        <template #footer>
+          <el-button @click="roleSelectionDialogVisible = false">关闭</el-button>
+        </template>
+      </el-dialog>
+    </div>
+  `,
+  data() {
+    return {
+      recommendations: store.recommendations,
+      roles: store.roles,
+      recommendationDialogVisible: false,
+      roleSelectionDialogVisible: false,
+      isEditingRec: false,
+      currentRecId: null,
+      recommendationForm: {
+        roleId: '',
+        callCount: 0
+      },
+      selectedRole: null,
+      recommendationRules: {
+        roleId: [
+          { required: true, message: '请输入角色ID', trigger: 'blur' },
+          { 
+            validator: (rule, value, callback) => {
+              const roleExists = this.roles.some(r => r.id === parseInt(value));
+              if (!roleExists) {
+                callback(new Error('该角色ID不存在'));
+              } else {
+                callback();
+              }
+            },
+            trigger: 'blur'
+          }
+        ],
+        callCount: [
+          { required: true, message: '请输入通话次数', trigger: 'blur' },
+          { type: 'number', min: 0, message: '通话次数必须大于等于0', trigger: 'blur' }
+        ]
+      }
+    };
+  },
+  computed: {
+    // 格式化推荐列表,关联角色信息
+    formattedRecommendations() {
+      return this.recommendations.map(rec => {
+        const role = this.roles.find(r => r.id === rec.roleId) || {};
+        return {
+          ...role,
+          ...rec,
+          recId: rec.id // 保存推荐ID,用于编辑和删除
+        };
+      }).sort((a, b) => a.sortOrder - b.sortOrder);
+    },
+    
+    // 可推荐的角色(排除已推荐的)
+    availableRoles() {
+      const recommendedRoleIds = this.recommendations.map(rec => rec.roleId);
+      return this.roles.filter(role => !recommendedRoleIds.includes(role.id));
+    }
+  },
+  methods: {
+    showAddRecommendationDialog() {
+      this.isEditingRec = false;
+      this.currentRecId = null;
+      this.recommendationForm = {
+        roleId: '',
+        callCount: 0
+      };
+      this.selectedRole = null;
+      this.recommendationDialogVisible = true;
+    },
+    
+    showEditRecommendationDialog(rec) {
+      this.isEditingRec = true;
+      this.currentRecId = rec.recId;
+      this.recommendationForm = {
+        roleId: rec.roleId,
+        callCount: rec.callCount
+      };
+      this.selectedRole = this.roles.find(r => r.id === rec.roleId) || null;
+      this.recommendationDialogVisible = true;
+    },
+    
+    showRoleSelectionDialog() {
+      this.roleSelectionDialogVisible = true;
+    },
+    
+    selectRoleFromTable(row) {
+      this.recommendationForm.roleId = row.id;
+      this.selectedRole = row;
+      this.roleSelectionDialogVisible = false;
+    },
+    
+    addRecommendation() {
+      this.$refs.recommendationForm.validate((valid) => {
+        if (valid) {
+          const newRec = {
+            roleId: parseInt(this.recommendationForm.roleId),
+            callCount: parseInt(this.recommendationForm.callCount)
+          };
+          
+          const addedRec = store.addRecommendation(newRec);
+          if (addedRec) {
+            this.recommendations = [...store.recommendations];
+            this.recommendationDialogVisible = false;
+            ElMessage.success('推荐添加成功');
+          } else {
+            ElMessage.error('推荐添加失败,角色不存在');
+          }
+        }
+      });
+    },
+    
+    updateRecommendation() {
+      this.$refs.recommendationForm.validate((valid) => {
+        if (valid) {
+          const updatedRec = {
+            id: this.currentRecId,
+            roleId: parseInt(this.recommendationForm.roleId),
+            callCount: parseInt(this.recommendationForm.callCount)
+          };
+          
+          const result = store.addRecommendation(updatedRec);
+          if (result) {
+            this.recommendations = [...store.recommendations];
+            this.recommendationDialogVisible = false;
+            ElMessage.success('推荐更新成功');
+          } else {
+            ElMessage.error('推荐更新失败');
+          }
+        }
+      });
+    },
+    
+    handleDeleteRecommendation(id) {
+      ElMessageBox.confirm(
+        '确定要删除这个推荐吗?',
+        '确认删除',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }
+      ).then(() => {
+        if (store.deleteRecommendation(id)) {
+          ElMessage.success('推荐删除成功');
+          this.recommendations = [...store.recommendations];
+        } else {
+          ElMessage.error('推荐删除失败');
+        }
+      }).catch(() => {
+        // 取消删除
+      });
+    },
+    
+    moveUp(recId) {
+      const rec = this.recommendations.find(r => r.id === recId);
+      if (rec && rec.sortOrder > 1) {
+        store.updateRecommendationSortOrder(recId, rec.sortOrder - 1);
+        this.recommendations = [...store.recommendations];
+      }
+    },
+    
+    moveDown(recId) {
+      const rec = this.recommendations.find(r => r.id === recId);
+      if (rec && rec.sortOrder < this.recommendations.length) {
+        store.updateRecommendationSortOrder(recId, rec.sortOrder + 1);
+        this.recommendations = [...store.recommendations];
+      }
+    }
+  },
+  components: {
+    Plus, Edit, Delete, ArrowUp, ArrowDown
+  }
+};
+
+// 定义路由
+const routes = [
+  { path: '/login', component: LoginPage },
+  { path: '/register', component: RegisterPage },
+  { 
+    path: '/', 
+    component: MainLayout,
+    beforeEnter: requireAuth,
+    children: [
+      { path: '/dashboard', component: DashboardPage },
+      { path: '/roles', component: RolesPage },
+      { path: '/voices', component: VoicesPage },
+      { path: '/recommendations', component: RecommendationsPage },
+      { path: '', redirect: '/dashboard' }
+    ]
+  },
+  { path: '/:pathMatch(.*)*', redirect: '/login' }
+];
+
+// 创建路由实例
+const router = createRouter({
+  history: createWebHistory(),
+  routes
+});
+
+// 注册所有图标
+for (const [key, component] of Object.entries({ 
+  Home, User, Music, Star, Plus, Upload, 
+  Edit, Delete, Logout, ArrowUp, ArrowDown 
+})) {
+  app.component(key, component);
+}
+
+// 使用插件
+app.use(ElementPlus);
+app.use(router);
+
+// 挂载应用
+app.mount('#app');
+</script>
+
+<style>
+/* 全局样式 */
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  background-color: #f5f7fa;
+  color: #333;
+}
+
+#app {
+  min-height: 100vh;
+}
+
+/* 登录和注册页面样式 */
+.login-container,
+.register-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh;
+  background-color: #f0f2f5;
+}
+
+.login-card,
+.register-card {
+  width: 400px;
+}
+
+.login-header,
+.register-header {
+  text-align: center;
+  margin-bottom: 20px;
+}
+
+.login-button,
+.register-button {
+  width: 100%;
+}
+
+.register-link,
+.login-link {
+  display: block;
+  text-align: center;
+  margin-top: 15px;
+}
+
+/* 主布局样式 */
+.main-layout {
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.main-header {
+  background-color: #ffffff;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 20px;
+  height: 60px;
+}
+
+.logo {
+  display: flex;
+  align-items: center;
+  font-size: 18px;
+  font-weight: bold;
+}
+
+.logo .el-icon {
+  margin-right: 10px;
+  font-size: 24px;
+  color: #409eff;
+}
+
+.user-info {
+  display: flex;
+  align-items: center;
+}
+
+.logout-btn {
+  margin-left: 10px;
+}
+
+.main-content {
+  display: flex;
+  flex: 1;
+}
+
+.sidebar {
+  width: 200px !important;
+  background-color: #0f172a;
+  height: calc(100vh - 60px);
+}
+
+.content-area {
+  flex: 1;
+  padding: 20px;
+  background-color: #f5f7fa;
+}
+
+/* 仪表盘样式 */
+.stats-container {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+  gap: 20px;
+  margin-bottom: 30px;
+}
+
+.stat-card {
+  display: flex;
+  align-items: center;
+  padding: 20px;
+}
+
+.stat-icon {
+  font-size: 40px;
+  margin-right: 20px;
+  color: #409eff;
+}
+
+.stat-info {
+  flex: 1;
+}
+
+.stat-title {
+  font-size: 14px;
+  color: #909399;
+}
+
+.stat-value {
+  font-size: 24px;
+  font-weight: bold;
+  margin-top: 5px;
+}
+
+.recent-activity {
+  margin-top: 30px;
+}
+
+/* 表格页面通用样式 */
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 10px;
+}
+
+.table-actions {
+  display: flex;
+  gap: 5px;
+}
+
+/* 表单样式 */
+.form-container {
+  max-width: 600px;
+}
+
+.form-footer {
+  margin-top: 20px;
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+/* 推荐管理特定样式 */
+.sort-buttons {
+  display: flex;
+  flex-direction: column;
+}
+</style>

+ 29 - 0
src/App.vue

@@ -0,0 +1,29 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'App'
+}
+</script>
+
+<style>
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  background-color: #f5f7fa;
+  color: #333;
+}
+
+#app {
+  min-height: 100vh;
+}
+</style>

+ 143 - 0
src/components/LoginPage.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="login-container">
+    <el-card class="login-card">
+      <div class="login-header">
+        <h2>Welcome Back</h2>
+        <p>Sign in to manage your AI characters</p>
+      </div>
+      <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
+        <div class="form-label">Email Address</div>
+        <el-form-item prop="username">
+          <el-input v-model="loginForm.username" placeholder="admin@example.com">
+          </el-input>
+        </el-form-item>
+        <div class="form-label">Password</div>
+        <el-form-item prop="password">
+          <el-input v-model="loginForm.password" type="password" placeholder="********">
+          </el-input>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleLogin" class="login-button">Login</el-button>
+        </el-form-item>
+        <div class="register-link">
+          Don't have an account? <a href="#">Register</a>
+        </div>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { ElMessage } from 'element-plus'
+import { useStore } from '../store'
+
+export default {
+  name: 'LoginPage',
+  data() {
+    return {
+      loginForm: {
+        username: '',
+        password: ''
+      },
+      loginRules: {
+        username: [
+          { required: true, message: 'Please enter your email', trigger: 'blur' }
+        ],
+        password: [
+          { required: true, message: 'Please enter your password', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  methods: {
+    handleLogin() {
+      this.$refs.loginForm.validate((valid) => {
+        if (valid) {
+          const store = useStore()
+          const { username, password } = this.loginForm
+          if (store.login(username, password)) {
+            ElMessage.success('Login successful')
+            this.$router.push('/')
+          } else {
+            ElMessage.error('Invalid email or password')
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.login-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh;
+  background-color: #f0f2f5;
+}
+
+.login-card {
+  width: 400px;
+  border-radius: 8px;
+}
+
+.login-header {
+  text-align: center;
+  margin-bottom: 20px;
+}
+
+.login-header h2 {
+  font-size: 24px;
+  margin-bottom: 8px;
+}
+
+.login-header p {
+  color: #606266;
+  font-size: 14px;
+  margin: 0;
+}
+
+.form-label {
+  font-size: 14px;
+  color: #606266;
+  margin-bottom: 8px;
+}
+
+.login-button {
+  width: 100%;
+  height: 40px;
+  border-radius: 4px;
+  background-color: #ff6b00;
+  border-color: #ff6b00;
+  margin-top: 10px;
+}
+
+.login-button:hover {
+  background-color: #e66000;
+  border-color: #e66000;
+}
+
+.register-link {
+  text-align: center;
+  margin-top: 15px;
+  font-size: 14px;
+}
+
+.register-link a {
+  color: #ff6b00;
+  text-decoration: none;
+}
+
+.register-link a:hover {
+  text-decoration: underline;
+}
+
+:deep(.el-input__inner) {
+  height: 40px;
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 20px;
+}
+</style>

+ 201 - 0
src/components/MainLayout.vue

@@ -0,0 +1,201 @@
+<template>
+  <div class="main-layout">
+    <el-container>
+      <el-aside width="200px">
+        <div class="logo">AI <span class="hub">HUB</span></div>
+        <el-menu
+          :router="true"
+          :default-active="activeMenu"
+          class="sidebar-menu"
+          background-color="#f8f8f8"
+          text-color="#333"
+          active-text-color="#ff6b00"
+        >
+          <el-menu-item index="/roles">
+            <div class="menu-item-content">
+              <el-icon><UserFilled /></el-icon>
+              <span>Role Management</span>
+            </div>
+          </el-menu-item>
+          <el-menu-item index="/voices">
+            <div class="menu-item-content">
+              <el-icon><Microphone /></el-icon>
+              <span>Voice Management</span>
+            </div>
+          </el-menu-item>
+          <el-menu-item index="/recommendations">
+            <div class="menu-item-content">
+              <el-icon><StarFilled /></el-icon>
+              <span>Recommendations</span>
+            </div>
+          </el-menu-item>
+          <el-menu-item index="/settings">
+            <div class="menu-item-content">
+              <el-icon><Setting /></el-icon>
+              <span>Settings</span>
+            </div>
+          </el-menu-item>
+        </el-menu>
+      </el-aside>
+      <el-container>
+        <el-header>
+          <div class="header-search">
+            <el-input
+              placeholder="Search roles, voices..."
+              prefix-icon="Search"
+            >
+            </el-input>
+          </div>
+          <div class="header-right">
+            <div class="user-info">
+              <span class="username">Admin User</span>
+              <span class="user-role">System Administrator</span>
+            </div>
+            <el-avatar class="user-avatar" :size="32" @click="$router.push('/settings')"></el-avatar>
+          </div>
+        </el-header>
+        <el-main>
+          <router-view />
+        </el-main>
+      </el-container>
+    </el-container>
+  </div>
+</template>
+
+<script>
+import { UserFilled, StarFilled, Setting, Search, Microphone } from '@element-plus/icons-vue'
+import { useStore } from '../store'
+import { ElMessage } from 'element-plus'
+
+export default {
+  name: 'MainLayout',
+  components: {
+    UserFilled,
+    StarFilled,
+    Setting,
+    Search,
+    Microphone
+  },
+  computed: {
+    activeMenu() {
+      return this.$route.path
+    }
+  },
+  methods: {
+    logout() {
+      const store = useStore()
+      store.logout()
+      ElMessage.success('Logged out successfully')
+      this.$router.push('/login')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.main-layout {
+  height: 100vh;
+}
+
+.el-container {
+  height: 100%;
+}
+
+.el-aside {
+  background-color: #f8f8f8;
+  color: #333;
+  width: 200px;
+  border-right: 1px solid #e6e6e6;
+}
+
+.logo {
+  height: 60px;
+  line-height: 60px;
+  text-align: center;
+  font-size: 24px;
+  font-weight: bold;
+  color: #333;
+  padding-left: 20px;
+  text-align: left;
+}
+
+.hub {
+  color: #ff6b00;
+}
+
+.sidebar-menu {
+  border-right: none;
+}
+
+.menu-item-content {
+  display: flex;
+  align-items: center;
+}
+
+.menu-item-content .el-icon {
+  margin-right: 10px;
+  font-size: 18px;
+}
+
+.el-header {
+  background-color: #fff;
+  color: #333;
+  line-height: 60px;
+  border-bottom: 1px solid #e6e6e6;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 20px;
+}
+
+.header-search {
+  width: 300px;
+}
+
+.header-right {
+  display: flex;
+  align-items: center;
+}
+
+.notification-badge {
+  margin-right: 20px;
+  font-size: 20px;
+  cursor: pointer;
+}
+
+.user-info {
+  display: flex;
+  flex-direction: column;
+  margin-right: 10px;
+  line-height: 1.2;
+}
+
+.username {
+  font-size: 14px;
+  font-weight: bold;
+}
+
+.user-role {
+  font-size: 12px;
+  color: #909399;
+}
+
+.user-avatar {
+  cursor: pointer;
+}
+
+.el-main {
+  background-color: #f0f2f5;
+  padding: 20px;
+}
+
+:deep(.el-menu-item.is-active) {
+  background-color: #fff3e8;
+  color: #ff6b00;
+  border-left: 3px solid #ff6b00;
+}
+
+:deep(.el-menu-item:hover) {
+  background-color: #fff3e8;
+}
+</style>

+ 17 - 0
src/main.js

@@ -0,0 +1,17 @@
+import { createApp } from 'vue'
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+
+// 注册所有图标
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
+
+app.use(ElementPlus)
+app.use(router)
+app.mount('#app')

+ 69 - 0
src/router/index.js

@@ -0,0 +1,69 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+import { useStore } from '../store'
+import LoginPage from '../components/LoginPage.vue'
+import MainLayout from '../components/MainLayout.vue'
+import Dashboard from '../views/Dashboard.vue'
+import RoleManagement from '../views/RoleManagement.vue'
+import VoiceManagement from '../views/VoiceManagement.vue'
+import RecommendationManagement from '../views/RecommendationManagement.vue'
+
+// 路由守卫
+const requireAuth = (to, from, next) => {
+  const store = useStore()
+  if (!store.user && to.path !== '/login') {
+    next('/login')
+  } else {
+    next()
+  }
+}
+
+const routes = [
+  {
+    path: '/login',
+    name: 'Login',
+    component: LoginPage
+  },
+  {
+    path: '/',
+    component: MainLayout,
+    beforeEnter: requireAuth,
+    children: [
+      {
+        path: '',
+        name: 'Dashboard',
+        component: Dashboard
+      },
+      {
+        path: 'roles',
+        name: 'RoleManagement',
+        component: RoleManagement
+      },
+      {
+        path: 'voices',
+        name: 'VoiceManagement',
+        component: VoiceManagement
+      },
+      {
+        path: 'recommendations',
+        name: 'RecommendationManagement',
+        component: RecommendationManagement
+      },
+      {
+        path: 'settings',
+        name: 'Settings',
+        component: () => import('../views/Settings.vue')
+      }
+    ]
+  },
+  {
+    path: '/:pathMatch(.*)*',
+    redirect: '/login'
+  }
+]
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes
+})
+
+export default router

+ 18 - 0
src/service/anycallService.js

@@ -0,0 +1,18 @@
+import service from './api'; // 导入配置好的 axios 实例
+
+// 定义所有用户相关的 API 请求函数
+export default {
+    anycallPage(pageData) {
+        return service.post(`/anycall/selectLib`, pageData);
+    },
+    updateCallings(data) {
+        return service.post(`/anycall/updateCallings`, data);
+    },
+    updateTopFlag(data) {
+        return service.post(`/anycall/updateTopFlag`, data);
+    },
+    voiceList(data) {
+        return service.post(`/anycall/selectVoiceList`, data);
+    },
+};
+

+ 16 - 0
src/service/api.js

@@ -0,0 +1,16 @@
+import axios from 'axios'
+
+// 创建axios实例
+const service = axios.create({
+    baseURL: 'http://127.0.0.1:8080',
+    withCredentials: false, // 跨域请求时是否需要使用凭证
+    headers: {
+        Accept: 'application/json',
+        'Content-Type': 'application/json',
+        'accessToken': 'rcOBHJ0Hb8h5xgM/CWtNd8RBhA6WS4OPyJcxrxk4xPZtzeh5PtRXVDA7Um0NZA6NQmnbnZgWB0nNPb8iCrneQj4badFveWLrFq4LrySto3pIo/Zg1dJubbwmu3Vr1LCbSYyVIFrrgt9PXiA85kb9g38FSG3KTSi3AEY/UgjLNLBtH2+91YXKEy2KRZV3v75f',
+    },
+    // 超时
+    timeout: 10000
+})
+
+export default service

+ 13 - 0
src/service/cdnService.js

@@ -0,0 +1,13 @@
+import cdnService from "./cdnapi";
+import service from "./api"; // 导入配置好的 axios 实例
+
+// 定义所有用户相关的 API 请求函数
+export default {
+    upload(formData) {
+        return cdnService.post(`/cmn/upload`, formData);
+    },
+    uploadVoice(file) {
+        return cdnService.post(`/cmn/upload2Wav`, file);
+    },
+};
+

+ 16 - 0
src/service/cdnapi.js

@@ -0,0 +1,16 @@
+import axios from 'axios'
+
+// 创建axios实例
+const cdnService = axios.create({
+    baseURL: 'https://dev.nexthuman.cn/fara/cdn',
+    withCredentials: false, // 跨域请求时是否需要使用凭证
+    headers: {
+        Accept: 'application/json',
+        'Content-Type': 'application/json',
+        'accessToken': 'rcOBHJ0Hb8h5xgM/CWtNd8RBhA6WS4OPyJcxrxk4xPZtzeh5PtRXVDA7Um0NZA6NQmnbnZgWB0nNPb8iCrneQj4badFveWLrFq4LrySto3pIo/Zg1dJubbwmu3Vr1LCbSYyVIFrrgt9PXiA85kb9g38FSG3KTSi3AEY/UgjLNLBtH2+91YXKEy2KRZV3v75f',
+    },
+    // 超时
+    timeout: 10000
+})
+
+export default cdnService

+ 124 - 0
src/store/index.js

@@ -0,0 +1,124 @@
+// 模拟数据存储
+import anycallService from "../service/anycallService";
+
+const store = {
+  // 用户信息
+  user: null,
+  
+  // 角色数据
+  roles: [
+  ],
+  
+  // 声音数据
+  voices: [
+
+  ],
+  
+  // 推荐数据
+  recommendations: [
+  ],
+
+
+  // 登录
+  login(username, password) {
+    // 模拟登录验证
+    if (username && password) {
+      this.user = { id: 1, username, role: "admin" };
+      return true;
+    }
+    return false;
+  },
+  
+  // 登出
+  logout() {
+    this.user = null;
+  },
+};
+
+export function useStore() {
+  let roleData = {
+    page: 1,
+    size: 10,
+    name: ""
+  };
+  let topRoleData = {
+    page: 1,
+    size: 999,
+    name: "",
+    topFlag: true
+  };
+  let data = {
+    page: 1,
+    size: 10,
+    system: true,
+    gender: 1
+  };
+
+  async function fetchRoles() {
+
+    try {
+      const response = await anycallService.anycallPage(roleData);
+
+      store.roles = response.data.data.content.map(item => ({
+        id: item.id || '',
+        name: item.name || '',
+        prompt: item.prompt || '',
+        callings: item.callings || '',
+        language: item.language || '',
+        photo: item.photo || '',
+        voice: item.voice || '',
+        voiceName: item.voiceName || '',
+        topFlag: item.topFlag || '',
+      }));
+      store.rolesCount = response.data.data.total;
+
+    } catch (err) {
+      // error.value = err.message || '获取数据失败';
+    }
+  }
+
+  async function fetchVoices() {
+
+    try {
+      const response = await anycallService.voiceList(data);
+      store.voices = response.data.data.content.map(item => ({
+        id: item.id || '',
+        name: item.name || '',
+        gender: item.gender || '',
+        ctime: new Date(item.ctime).getFullYear()+'-'+new Date(item.ctime).getMonth()+'-'+new Date(item.ctime).getDay() || '',
+      }));
+      store.voicesCount = response.data.data.total;
+
+      console.log(response.data.data.total);
+    } catch (err) {
+      // error.value = err.message || '获取数据失败';
+    }
+  }
+
+  async function fetchRecommendations() {
+
+    try {
+      const response = await anycallService.anycallPage(topRoleData);
+
+      store.recommendations = response.data.data.content.map(item => ({
+        id: item.id || '',
+        name: item.name || '',
+        prompt: item.prompt || '',
+        callings: item.callings || '',
+        language: item.language || '',
+        photo: item.photo || '',
+        voice: item.voice || '',
+        voiceName: item.voiceName || '',
+      }));
+      store.recommendationsCount = response.data.data.total;
+
+    } catch (err) {
+    }
+  }
+
+  fetchRecommendations();
+  fetchRoles();
+  fetchVoices();
+
+  return store;
+}

+ 174 - 0
src/views/Dashboard.vue

@@ -0,0 +1,174 @@
+<template>
+  <div class="dashboard">
+    <h1>欢迎使用角色管理系统</h1>
+    
+    <div class="stats-container">
+      <el-card class="stat-card">
+        <div class="stat-icon">
+          <el-icon><UserFilled /></el-icon>
+        </div>
+        <div class="stat-info">
+          <div class="stat-title">角色总数</div>
+          <div class="stat-value">{{ rolesCount }}</div>
+        </div>
+      </el-card>
+      
+      <el-card class="stat-card">
+        <div class="stat-icon voice-icon">
+          <el-icon><Microphone /></el-icon>
+        </div>
+        <div class="stat-info">
+          <div class="stat-title">声音总数</div>
+          <div class="stat-value">{{ voicesCount }}</div>
+        </div>
+      </el-card>
+      
+      <el-card class="stat-card">
+        <div class="stat-icon recommend-icon">
+          <el-icon><StarFilled /></el-icon>
+        </div>
+        <div class="stat-info">
+          <div class="stat-title">推荐总数</div>
+          <div class="stat-value">{{ recommendationsCount }}</div>
+        </div>
+      </el-card>
+    </div>
+    
+    <div class="roles-list">
+      <h2>角色列表</h2>
+      <el-table :data="roles" style="width: 100%">
+        <el-table-column prop="name" label="名称" />
+        <el-table-column prop="gender" label="性别" />
+        <el-table-column prop="language" label="语言" />
+        <el-table-column label="头像">
+          <template #default="scope">
+            <el-avatar :src="scope.row.avatar" />
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <Pagination
+        :current-page="currentPage"
+        :per-page="perPage"
+        :total-items="totalItems"
+        :max-displayed-pages="maxDisplayedPages"
+        :show-info="true"
+        @page-changed="onPageChange"
+    />
+  </div>
+</template>
+
+<script>
+import { UserFilled, StarFilled } from '@element-plus/icons-vue'
+import { useStore } from '../store'
+import Pagination from './util/Pagination.vue'
+import anycallService from "../service/anycallService";
+
+
+export default {
+  name: 'Dashboard',
+  components: {
+    Pagination,
+    UserFilled,
+    StarFilled,
+    Microphone: {
+      template: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1em" height="1em"><path fill="currentColor" d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.91-3c-.49 0-.9.36-.98.85C16.52 14.2 14.47 16 12 16s-4.52-1.8-4.93-4.15c-.08-.49-.49-.85-.98-.85-.61 0-1.09.54-1 1.14.49 3 2.89 5.35 5.91 5.78V20c0 .55.45 1 1 1s1-.45 1-1v-2.08c3.02-.43 5.42-2.78 5.91-5.78.1-.6-.39-1.14-1-1.14z"/></svg>'
+    }
+  },
+  data() {
+    const store = useStore()
+    return {
+
+      currentPage: 1,
+      perPage: 10,
+      totalItems: store.rolesCount,
+      maxDisplayedPages: 5,
+
+      roles: store.roles,
+      rolesCount: store.rolesCount,
+      voicesCount: store.voicesCount,
+      recommendationsCount: store.recommendations.length
+    }
+  },
+  methods: {
+    async onPageChange(page) {
+      this.currentPage = page
+      // 这里可以添加加载数据的逻辑
+      let pageData = {
+        page: page,
+        size: this.perPage,
+        name: ""
+      };
+      const response = await anycallService.anycallPage(pageData);
+      this.roles = response.data.data.content.map(item => ({
+        id: item.id || '',
+        name: item.name || '',
+        prompt: item.prompt || '',
+        callings: item.callings || '',
+        language: item.language || '',
+        photo: item.photo || '',
+        voice: item.voice || '',
+        voiceName: item.voiceName || '',
+      }));
+      this.rolesCount = response.data.data.total;
+    }
+
+
+  }
+}
+</script>
+
+<style scoped>
+.dashboard {
+  padding: 20px;
+}
+
+h1 {
+  margin-bottom: 30px;
+  color: #303133;
+}
+
+.stats-container {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+  gap: 20px;
+  margin-bottom: 30px;
+}
+
+.stat-card {
+  display: flex;
+  align-items: center;
+  padding: 20px;
+}
+
+.stat-icon {
+  font-size: 40px;
+  margin-right: 20px;
+  color: #409eff;
+}
+
+.stat-info {
+  flex: 1;
+}
+
+.stat-title {
+  font-size: 14px;
+  color: #909399;
+}
+
+.stat-value {
+  font-size: 24px;
+  font-weight: bold;
+  margin-top: 5px;
+}
+
+.roles-list {
+  margin-top: 30px;
+}
+
+h2 {
+  margin-bottom: 20px;
+  color: #303133;
+}
+</style>

+ 307 - 0
src/views/RecommendationManagement.vue

@@ -0,0 +1,307 @@
+<template>
+  <div class="recommendation-management">
+    <div class="header">
+      <h2>Recommendation Management</h2>
+      <div class="header-actions">
+<!--        <el-button type="primary" @click="showAddDialog">-->
+<!--          Add Recommendation-->
+<!--        </el-button>-->
+      </div>
+    </div>
+
+    <el-table :data="recommendations" v-loading="loading" style="width: 100%" class="recommendation-table">
+      <el-table-column label="recommendation" min-width="200">
+        <template #default="scope">
+          <div class="role-info">
+            <div class="role-details">
+              <div class="role-name">{{ scope.row.name }}</div>
+              <div class="role-id">ID: {{ scope.row.id }}</div>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column prop="callCount" label="Call Count" width="120" >
+        <template #default="scope">
+          <div class="sort-actions">
+            <span class="sort-order">{{ scope.row.callings }}</span>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+
+  </div>
+</template>
+
+<script>
+import { Delete, ArrowUp, ArrowDown, Upload, Edit } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useStore } from '../store'
+
+
+export default {
+  name: 'RecommendationManagement',
+  components: {
+    Delete,
+    ArrowUp,
+    ArrowDown,
+    Upload,
+    Edit
+  },
+  data() {
+    const store = useStore()
+    return {
+      currentPage: 1,
+      perPage: 10,
+      totalItems: store.recommendationsCount,
+      maxDisplayedPages: 5,
+
+      recommendations: store.recommendations,
+      roles: store.roles,
+      loading: false,
+      dialogVisible: false,
+      dialogType: 'add', // 'add' or 'edit'
+      currentRecommendationId: null,
+      importDialogVisible: false,
+      importFileName: '',
+      recommendationForm: {
+        roleId: '',
+        callCount: 0
+      },
+      rules: {
+      }
+    }
+  },
+  computed: {
+
+  },
+  methods: {
+    // 处理Excel上传
+    handleExcelUpload(file) {
+      this.importFileName = file.name;
+      this.importDialogVisible = true;
+      return false; // 阻止默认上传
+    },
+    
+    // Show edit dialog
+    showEditDialog(recommendation) {
+      this.dialogType = 'edit'
+      this.currentRecommendationId = recommendation.id
+      this.recommendationForm = {
+        roleId: recommendation.roleId,
+        callCount: recommendation.callCount
+      }
+      this.dialogVisible = true
+    },
+    
+    // Move up
+    moveUp(recommendation) {
+      const store = useStore()
+      const currentIndex = recommendation.sortOrder - 1
+      if (currentIndex > 0) {
+        const prevRec = store.recommendations.find(rec => rec.sortOrder === currentIndex)
+        const currentRec = store.recommendations.find(rec => rec.sortOrder === currentIndex + 1)
+        
+        if (prevRec && currentRec) {
+          // Create new array to trigger reactivity
+          const newRecommendations = [...store.recommendations]
+          const prevIndex = newRecommendations.findIndex(rec => rec.id === prevRec.id)
+          const currentIndex = newRecommendations.findIndex(rec => rec.id === currentRec.id)
+          
+          // Swap sort orders
+          newRecommendations[prevIndex].sortOrder += 1
+          newRecommendations[currentIndex].sortOrder -= 1
+          
+          // Update store
+          store.recommendations = newRecommendations
+          this.recommendations = store.recommendations
+        }
+      }
+    },
+    
+    // Move down
+    moveDown(recommendation) {
+      const store = useStore()
+      const currentIndex = recommendation.sortOrder - 1
+      if (currentIndex < store.recommendations.length - 1) {
+        const nextRec = store.recommendations.find(rec => rec.sortOrder === currentIndex + 2)
+        const currentRec = store.recommendations.find(rec => rec.sortOrder === currentIndex + 1)
+        
+        if (nextRec && currentRec) {
+          // Create new array to trigger reactivity
+          const newRecommendations = [...store.recommendations]
+          const nextIndex = newRecommendations.findIndex(rec => rec.id === nextRec.id)
+          const currentIndex = newRecommendations.findIndex(rec => rec.id === currentRec.id)
+          
+          // Swap sort orders
+          newRecommendations[nextIndex].sortOrder -= 1
+          newRecommendations[currentIndex].sortOrder += 1
+          
+          // Update store
+          store.recommendations = newRecommendations
+          this.recommendations = store.recommendations
+        }
+      }
+    },
+    
+    // Confirm delete
+    confirmDelete(recommendation) {
+      ElMessageBox.confirm(
+        `Are you sure you want to delete this recommendation?`,
+        'Warning',
+        {
+          confirmButtonText: 'Delete',
+          cancelButtonText: 'Cancel',
+          type: 'warning'
+        }
+      ).then(() => {
+        const store = useStore()
+        store.deleteRecommendation(recommendation.id)
+        this.recommendations = store.recommendations
+        ElMessage.success('Recommendation deleted successfully')
+      }).catch(() => {})
+    },
+    
+    // Submit form
+    submitForm() {
+      this.$refs.recommendationFormRef.validate((valid) => {
+        if (valid) {
+          const store = useStore()
+          
+          if (this.dialogType === 'add') {
+            const maxSortOrder = Math.max(...store.recommendations.map(rec => rec.sortOrder), 0)
+            
+            // Create new recommendation
+            const newRecommendation = {
+              id: Date.now().toString(),
+              roleId: this.recommendationForm.roleId,
+              callCount: this.recommendationForm.callCount,
+              sortOrder: maxSortOrder + 1
+            }
+            
+            // Create new array to trigger reactivity
+            const newRecommendations = [...store.recommendations, newRecommendation]
+            store.recommendations = newRecommendations
+            this.recommendations = newRecommendations
+            
+            ElMessage.success('Recommendation added successfully')
+          } else if (this.dialogType === 'edit') {
+            // Update existing recommendation
+            const newRecommendations = [...store.recommendations]
+            const index = newRecommendations.findIndex(rec => rec.id === this.currentRecommendationId)
+            
+            if (index !== -1) {
+              newRecommendations[index] = {
+                ...newRecommendations[index],
+                roleId: this.recommendationForm.roleId,
+                callCount: this.recommendationForm.callCount
+              }
+              
+              store.recommendations = newRecommendations
+              this.recommendations = newRecommendations
+              ElMessage.success('Recommendation updated successfully')
+            }
+          }
+          
+          this.dialogVisible = false
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style>
+.recommendation-management {
+  padding: 20px;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.header h2 {
+  font-size: 24px;
+  font-weight: 600;
+  margin: 0;
+}
+
+.header-actions {
+  display: flex;
+  gap: 10px;
+}
+
+.import-btn {
+  border-color: #dcdfe6;
+}
+
+.recommendation-table {
+  margin-top: 20px;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.role-info {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.role-details {
+  display: flex;
+  flex-direction: column;
+}
+
+.role-name {
+  font-weight: 500;
+}
+
+.role-id {
+  font-size: 12px;
+  color: #909399;
+}
+
+.sort-actions {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+}
+
+.sort-order {
+  font-size: 16px;
+  font-weight: 500;
+}
+
+.sort-buttons {
+  display: flex;
+  gap: 5px;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 10px;
+}
+
+.edit-btn {
+      color: #67c23a;
+    }
+    
+    .delete-btn {
+      color: #f56c6c;
+    }
+
+.recommendation-dialog .el-form-item {
+  margin-bottom: 20px;
+}
+  .role-input-container {
+    position: relative;
+  }
+  
+  .input-id-hint {
+    margin-top: 5px;
+    color: #909399;
+    font-size: 12px;
+  }
+</style>

+ 844 - 0
src/views/RoleManagement.vue

@@ -0,0 +1,844 @@
+<template>
+  <div class="role-management">
+    <div class="header">
+      <h2>Role Management</h2>
+      <div class="header-actions">
+        <el-upload
+          class="import-btn"
+          action="javascript:;"
+          :show-file-list="false"
+          :before-upload="handleExcelUpload"
+          accept=".xlsx,.xls"
+        >
+          <el-button type="text" plain>
+            <el-icon><Upload /></el-icon>Import
+          </el-button>
+        </el-upload>
+<!--        <el-button type="primary" @click="getRoleData">-->
+<!--          select Role-->
+<!--        </el-button>-->
+        <el-button type="primary" @click="showAddDialog">
+          Create Role
+        </el-button>
+      </div>
+    </div>
+    
+    <el-table :data="roles" v-loading="loading" style="width: 100%" class="role-table">
+      <el-table-column label="Role" min-width="100">
+        <template #default="scope">
+          <div class="role-info">
+            <el-avatar :src="scope.row.avatar" v-if="scope.row.avatar"></el-avatar>
+            <el-avatar v-else>{{ scope.row.name.charAt(0) }}</el-avatar>
+            <div class="role-details">
+              <div class="role-name">{{ scope.row.name }}</div>
+              <div class="role-id" hidden>ID: {{ scope.row.id }}</div>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+<!--      <el-table-column prop="description" label="Persona" min-width="200" />-->
+      <el-table-column prop="callings" label="callings" width="200">
+        <template #default="scope">
+          {{ scope.row.callings }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="prompt" label="prompt" width="200">
+        <template #default="scope">
+          {{ scope.row.prompt }}
+        </template>
+      </el-table-column>
+<!--      <el-table-column prop="gender" label="Gender" width="100">-->
+<!--        <template #default="scope">-->
+<!--          {{ scope.row.gender === 'male' ? 'Male' : scope.row.gender === 'female' ? 'Female' : 'Other' }}-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+      <el-table-column prop="language" label="Language" width="100">
+        <template #default="scope">
+          {{ scope.row.language === 'zh' ? 'Chinese' :
+             scope.row.language === 'en' ? 'English' :
+             scope.row.language === 'jp' ? 'Japanese' :
+             scope.row.language === 'kr' ? 'Korean' : scope.row.language }}
+        </template>
+      </el-table-column>
+      <el-table-column label="USE VOICE" width="150">
+        <template #default="scope">
+            <div class="voice-details">
+              <div class="voice-name">{{ scope.row.voiceName }}</div>
+              <div class="voice-id" hidden>ID: {{ scope.row.voiceId }}</div>
+            </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="Actions" width="150">
+        <template #default="scope">
+          <div class="action-buttons">
+            <el-button type="text" @click="editRole(scope.row)">
+              <el-icon><Edit /></el-icon>
+            </el-button>
+<!--            <el-button type="primary" @click="editRole(scope.row)">-->
+<!--                推荐-->
+<!--            </el-button>-->
+            <el-button type="primary" @click="changeTopFlag(scope.row);onPageChange(currentPage);">
+              {{ scope.row.topFlag ===true ? '取消推荐': '推荐'    }}
+            </el-button>
+<!--            <el-button type="text" class="delete-btn" @click="confirmDelete(scope.row)">-->
+<!--              <el-icon><Delete /></el-icon>-->
+<!--            </el-button>-->
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 导入确认对话框 -->
+    <el-dialog 
+      title="导入角色"
+      v-model="importDialogVisible"
+      width="500px"
+    >
+      <div v-if="importFileName" class="import-file-info">
+        即将导入文件: {{ importFileName }}
+      </div>
+      <div v-else>
+        请先选择Excel文件
+      </div>
+      
+      <template #footer>
+        <el-button @click="importDialogVisible = false">取消</el-button>
+        <el-button 
+          type="primary" 
+          @click="confirmImport"
+          :disabled="!importFileName"
+        >
+          确认导入
+        </el-button>
+      </template>
+    </el-dialog>
+
+    <!-- Add/Edit Role Dialog -->
+    <el-dialog
+      :title="dialogType === 'add' ? 'Create New Role' : 'Edit Role'"
+      v-model="dialogVisible"
+      width="500px"
+      class="role-dialog"
+    >
+      <el-form
+          ref="voiceFormRef"
+          :model="roleForm"
+          :rules="rules"
+          label-width="120px"
+      >
+        <el-form-item label="role callings" prop="name">
+          <el-input v-model="roleForm.callings" placeholder="Enter callings" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">Cancel</el-button>
+        <el-button type="primary" @click="updateCallings(roleForm.id,roleForm.callings); onPageChange(currentPage);dialogVisible = false">
+          {{ dialogType === 'add' ? 'Create' : 'Save Changes' }}
+        </el-button>
+      </template>
+    </el-dialog>
+
+
+    <!-- Add Dialog -->
+    <el-dialog
+      :title="'Create New Role'"
+      v-model="addDialogVisible"
+      width="500px"
+      class="role-dialog"
+    >
+      <el-form
+          :model="roleForm"
+          :rules="rules"
+          label-width="120px"
+      >
+        <el-form-item label="name" prop="name">
+          <el-input v-model="roleForm.name" placeholder="Enter name" />
+        </el-form-item>
+        <el-form-item label="prompt" prop="prompt">
+          <el-input v-model="roleForm.prompt" placeholder="Enter prompt" />
+        </el-form-item>
+        <el-form-item label="Photo" prop="Photo">
+          <el-input v-model="roleForm.photo" placeholder="Enter Photo" />
+        </el-form-item>
+        <el-form-item label="Language" prop="Language">
+          <el-input v-model="roleForm.language" placeholder="Enter Language" />
+        </el-form-item>
+
+        <el-form-item label="照片文件">
+          <el-upload
+              class="avatar-uploader"
+              action=""
+              :auto-upload="false"
+              :on-change="handlePhotoUpload"
+              accept="image/*"
+          >
+            <el-button size="small" type="primary">
+              <el-icon><Upload /></el-icon> 上传照片
+            </el-button>
+            <div v-if="photoFileName" class="photo-file-name">{{ photoFileName }}</div>
+          </el-upload>
+        </el-form-item>
+
+        <el-form-item label="声音文件">
+          <el-upload
+              class="voice-uploader"
+              action=""
+              :auto-upload="false"
+              :on-change="handleVoiceUpload"
+              accept="audio/*"
+          >
+            <el-button size="small" type="primary">
+              <el-icon><Upload /></el-icon> 上传声音文件
+            </el-button>
+            <div v-if="voiceFileName" class="voice-file-name">{{ voiceFileName }}</div>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="addDialogVisible = false">Cancel</el-button>
+        <el-button type="primary" @click="addCallings(roleForm.id,roleForm.callings); onPageChange(currentPage); addDialogVisible = false">
+          {{  'Save Changes' }}
+        </el-button>
+      </template>
+    </el-dialog>
+
+
+    <Pagination
+        :current-page="currentPage"
+        :per-page="perPage"
+        :total-items="totalItems"
+        :max-displayed-pages="maxDisplayedPages"
+        :show-info="true"
+        @page-changed="onPageChange"
+    />
+  </div>
+
+</template>
+
+<script>
+import { Edit, Delete, Upload, Headset, CircleClose, Loading } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useStore } from '../store'
+import anycallService from '../service/anycallService.js';
+import Pagination from './util/Pagination.vue'
+import cdnService from "../service/cdnService";
+
+
+
+export default {
+  name: 'RoleManagement',
+  components: {
+    Pagination,
+    Edit,
+    Delete,
+    Upload,
+    Headset,
+    CircleClose,
+    Loading
+  },
+  data() {
+    const store = useStore()
+    return {
+
+      currentPage: 1,
+      perPage: 10,
+      totalItems: store.rolesCount,
+      maxDisplayedPages: 5,
+
+      file: null,
+      roles: store.roles,
+      voices: store.voices,
+      loading: false,
+      dialogVisible: false,
+      addDialogVisible: false,
+      dialogType: 'add', // 'add' or 'edit'
+      importDialogVisible: false,
+      importFileName: '',
+      roleForm: {
+        id: '',
+        name: '',
+        description: '',
+        gender: '',
+        language: '',
+        avatar: '',
+        voiceId: '',
+        photoFileName: '',
+        voiceFileName: '',
+        clonedVoiceFileName: '',
+        isCloning: false,
+        clonedVoice: false
+      },
+      currentRole: null,
+      rules: {
+        name: [
+          {required: true, message: '请输入角色名称', trigger: 'blur'}
+        ],
+        description: [
+          {required: true, message: '请输入角色描述', trigger: 'blur'}
+        ],
+        gender: [
+          {required: true, message: '请选择性别', trigger: 'change'}
+        ],
+        language: [
+          {required: true, message: '请选择语言', trigger: 'change'}
+        ],
+        voiceFileName: [
+          {required: true, message: '请上传原始声音文件', trigger: 'blur'}
+        ]
+      }
+    };
+  },
+  methods: {
+    handlePhotoUpload(file) {
+      // cdnService.uploadVoice(file);
+      this.file = file;
+      let formData = {
+        file: this.file.raw
+      }
+      let response = cdnService.upload(formData);
+      console.log(response.data);
+      // this.photoFileName = file.name;
+      // todo 实际项目中应上传文件到服务器
+    },
+    handleVoiceUpload(file) {
+
+      let response = cdnService.uploadVoice(file);
+      console.log(response.data);
+      // this.voiceFileName = file.name;
+      // todo 实际项目中应上传文件到服务器
+    },
+
+    async onPageChange(page) {
+      this.currentPage = page
+      // 这里可以添加加载数据的逻辑
+      let pageData = {
+        page: page,
+        size: this.perPage,
+        name: ""
+      };
+      const response = await anycallService.anycallPage(pageData);
+      this.roles = response.data.data.content.map(item => ({
+        id: item.id || '',
+        name: item.name || '',
+        prompt: item.prompt || '',
+        callings: item.callings || '',
+        language: item.language || '',
+        photo: item.photo || '',
+        voice: item.voice || '',
+        voiceName: item.voiceName || '',
+        topFlag: item.topFlag || '',
+      }));
+      this.rolesCount = response.data.data.total;
+    },
+
+    async changeTopFlag(row) {
+      // console.log(row.id);
+      // console.log(row.id);
+      // console.log(row.topFlag !== true);
+      let data = {
+        cloneId: row.id,
+        topFlag: row.topFlag !== true
+      };
+      console.log(data);
+      const response = await anycallService.updateTopFlag(data);
+      ElMessage.success('推荐状态修改成功')
+    },
+
+    async addCallings(id,callings){
+      let data = {
+        cloneId: id,
+        callings: callings
+      };
+
+      // const response = await anycallService.updateCallings(data);
+      ElMessage.success('添加成功')
+      const store = useStore()
+      this.roles = store.roles
+    },
+
+    async updateCallings(id,callings){
+      let data = {
+        cloneId: id,
+        callings: callings
+      };
+
+      const response = await anycallService.updateCallings(data);
+      ElMessage.success('callings修改成功')
+      const store = useStore()
+      this.roles = store.roles
+    },
+
+
+    // 处理Excel上传
+    handleExcelUpload(file) {
+      this.importFileName = file.name;
+      this.importDialogVisible = true;
+      return false; // 阻止默认上传
+    },
+    
+    // 确认导入
+    confirmImport() {
+      // 模拟导入处理
+      ElMessage.success(`文件 ${this.importFileName} 导入成功`);
+      this.importDialogVisible = false;
+      this.importFileName = '';
+      // 实际项目中应解析Excel文件并添加角色
+    },
+    
+    // 上传前检查头像
+    beforeUploadAvatar(file) {
+      const isImage = ['image/jpeg', 'image/png', 'image/gif'].includes(file.type) || 
+                    file.name.endsWith('.jpg') || file.name.endsWith('.jpeg') || 
+                    file.name.endsWith('.png') || file.name.endsWith('.gif');
+      const isLt5M = file.size / 1024 / 1024 < 5;
+      
+      if (!isImage) {
+        ElMessage.error('请上传图片格式文件!');
+      }
+      if (!isLt5M) {
+        ElMessage.error('上传文件大小不能超过5MB!');
+      }
+      
+      return isImage && isLt5M;
+    },
+    
+    // 上传前检查声音文件 - 只接受原始声音源文件
+    beforeUploadVoice(file) {
+      const isAudio = ['audio/mpeg', 'audio/wav', 'audio/mp3'].includes(file.type) || file.name.endsWith('.mp3') || file.name.endsWith('.wav');
+      const isLt20M = file.size / 1024 / 1024 < 20;
+      
+      if (!isAudio) {
+        ElMessage.error('请上传音频格式文件!');
+      }
+      if (!isLt20M) {
+        ElMessage.error('上传文件大小不能超过20MB!');
+      }
+      
+      return isAudio && isLt20M;
+    },
+    
+    // 处理声音上传成功
+    handleVoiceUploadSuccess(file) {
+      // 模拟上传成功,实际应用中应该根据服务器返回设置文件信息
+      this.roleForm.voiceFileName = file.name;
+      ElMessage.success('原始声音文件上传成功');
+      
+      // 模拟系统自动克隆过程
+      this.roleForm.isCloning = true;
+      
+      // 模拟克隆过程的延迟
+            setTimeout(() => {
+              // 生成克隆文件名
+              const originalFileName = file.name;
+              const clonedFileName = originalFileName.replace('original-', 'cloned-');
+              
+              // 如果不在表单提交过程中,直接更新表单状态
+              if (!this.dialogVisible) {
+                this.roleForm.clonedVoiceFileName = clonedFileName;
+                this.roleForm.isCloning = false;
+                this.roleForm.clonedVoice = true;
+                ElMessage.success('已从原始声音源文件成功克隆声音');
+              }
+              // 如果是在表单提交过程中,更新声音数据中的克隆状态
+              else if (this.roleForm.voiceId) {
+                const store = useStore();
+                store.updateVoice(this.roleForm.voiceId, {
+                  clonedVoice: clonedFileName,
+                  isCloning: false
+                });
+                // 刷新本地数据
+                this.voices = store.voices;
+                this.roles = store.roles;
+                ElMessage.success('角色的声音克隆已完成');
+              }
+            }, 2000); // 模拟2秒的克隆时间
+    },
+    
+    // 移除声音文件
+    removeVoiceFile() {
+      this.roleForm.voiceFileName = '';
+      this.roleForm.clonedVoiceFileName = '';
+      this.roleForm.isCloning = false;
+      this.roleForm.clonedVoice = false;
+      this.roleForm.voiceId = '';
+    },
+    
+    // 自定义HTTP请求处理器 - 模拟上传过程,阻止实际请求
+    handleHttpRequest(options) {
+      // 立即阻止所有默认行为,这是最关键的一步
+      if (options && options.onSuccess && typeof options.onSuccess === 'function') {
+        console.log('Custom HTTP request handler triggered, BLOCKING actual request');
+        
+        // 创建一个完整的模拟成功响应对象
+        const mockResponse = {
+          code: 200,
+          message: 'Success',
+          data: {
+            fileName: options.file.name,
+            url: '#'
+          },
+          status: 200,
+          statusText: 'OK'
+        };
+        
+        // 直接调用成功回调函数,模拟成功上传
+        setTimeout(() => {
+          try {
+            options.onSuccess(mockResponse, options.file);
+          } catch (error) {
+            console.error('Error in onSuccess callback:', error);
+          }
+        }, 100);
+        
+        // 在所有条件下都返回false,确保阻止默认行为
+        return false;
+      }
+      
+      console.error('Invalid options or missing onSuccess callback');
+      
+      // 即使出错也返回false阻止请求
+      return false;
+    },
+
+    // 根据ID获取声音
+    getVoiceById(voiceId) {
+      return this.voices.find(v => v.id === voiceId);
+    },
+    
+    // 播放声音
+    playVoice(voiceId, isCloned = false) {
+      // 这里是模拟播放声音的逻辑
+      // 在实际应用中,应该根据voiceId获取声音文件并播放
+      if (isCloned) {
+        const voice = this.getVoiceById(voiceId);
+        if (voice) {
+          ElMessage.success(`正在播放克隆声音: ${voice.clonedVoice}`);
+        } else {
+          ElMessage.success('正在播放克隆声音');
+        }
+      } else {
+        // 查找对应的声音名称
+        const voice = this.getVoiceById(voiceId);
+        if (voice) {
+          ElMessage.success(`正在播放声音: ${voice.name}`);
+        } else {
+          ElMessage.success('正在播放原始声音');
+        }
+      }
+    },
+    
+    // 处理头像上传成功
+    handleAvatarUploadSuccess(file) {
+      try {
+        // 模拟上传成功,实际应用中应该根据服务器返回设置图片URL
+        // 使用FileReader读取图片文件,以便在本地预览
+        const reader = new FileReader();
+        reader.onload = (e) => {
+          this.roleForm.avatar = e.target.result;
+        };
+        reader.readAsDataURL(file);
+        
+        ElMessage.success('头像上传成功');
+      } catch (error) {
+        console.error('Error uploading avatar:', error);
+        ElMessage.error('处理头像图片失败');
+        this.handleUploadError();
+      }
+    },
+    
+    // 处理上传错误
+    handleUploadError(err, file, fileList) {
+      // 阻止默认错误处理
+      if (err && err.status === 404) {
+        ElMessage.warning('模拟环境中上传端点不可用');
+        return false;
+      }
+      ElMessage.error('上传失败,请重试');
+      return false;
+    },
+    
+    // 移除头像
+    removeAvatar() {
+      this.roleForm.avatar = '';
+      ElMessage.success('Avatar removed');
+    },
+
+    showAddDialog() {
+      this.roleForm = {
+        id: '',
+        name: '',
+        description: '',
+        gender: '',
+        language: '',
+        avatar: '',
+        voiceId: '',
+        voiceFileName: '',
+        clonedVoiceFileName: '',
+        isCloning: false,
+        clonedVoice: false
+      }
+      this.addDialogVisible = true
+    },
+    editRole(role) {
+      this.dialogType = 'edit'
+      this.currentRole = role
+      
+      // 初始化表单数据
+      this.roleForm = { ...role }
+      
+      // 如果有voiceId,查找对应的声音文件信息
+      if (role.voiceId) {
+        const voice = this.getVoiceById(role.voiceId)
+        if (voice) {
+          this.roleForm.voiceFileName = voice.originalVoice
+          this.roleForm.clonedVoiceFileName = voice.clonedVoice
+          this.roleForm.clonedVoice = !!voice.clonedVoice
+        }
+      }
+      
+      this.dialogVisible = true
+    },
+    confirmDelete(role) {
+      ElMessageBox.confirm(
+        `Are you sure you want to delete role "${role.name}"?`,
+        'Warning',
+        {
+          confirmButtonText: 'Delete',
+          cancelButtonText: 'Cancel',
+          type: 'warning'
+        }
+      ).then(() => {
+        const store = useStore()
+        store.deleteRole(role.id)
+        this.roles = store.roles
+        ElMessage.success('Role deleted successfully')
+      }).catch(() => {})
+    },
+
+    submitForm() {
+      this.$refs.roleFormRef.validate((valid) => {
+        if (valid) {
+          const store = useStore()
+          
+          // 先创建或更新声音数据
+          if (this.dialogType === 'add' || !this.roleForm.voiceId) {
+            // 创建新声音
+            // 检查克隆是否已完成,如果未完成则标记为克隆中
+            const isCloning = this.roleForm.isCloning || !this.roleForm.clonedVoice;
+            const newVoice = {
+              id: Date.now().toString(),
+              name: `${this.roleForm.name} Voice`,
+              originalVoice: this.roleForm.voiceFileName,
+              clonedVoice: isCloning ? null : this.roleForm.clonedVoiceFileName,
+              gender: this.roleForm.gender,
+              description: `${this.roleForm.name}的声音`,
+              uploadTime: new Date().toLocaleDateString(),
+              isCloning: isCloning
+            }
+            store.addVoice(newVoice)
+            this.roleForm.voiceId = newVoice.id
+          } else {
+            // 更新现有声音
+      const isUpdatingCloning = this.roleForm.isCloning || !this.roleForm.clonedVoice;
+      store.updateVoice(this.roleForm.voiceId, {
+        originalVoice: this.roleForm.voiceFileName,
+        clonedVoice: isUpdatingCloning ? null : this.roleForm.clonedVoiceFileName,
+        gender: this.roleForm.gender,
+        isCloning: isUpdatingCloning
+      })
+          }
+          
+          // 然后创建或更新角色
+          if (this.dialogType === 'add') {
+            store.addRole({
+              ...this.roleForm,
+              id: Date.now().toString(),
+              createdAt: new Date().toLocaleString()
+            })
+            ElMessage.success('角色创建成功')
+          } else {
+            store.updateRole(this.currentRole.id, this.roleForm)
+            ElMessage.success('角色更新成功')
+          }
+          
+          // 更新本地数据
+          this.roles = store.roles
+          this.voices = store.voices
+          this.dialogVisible = false
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style>
+.role-management {
+  padding: 20px;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.header h2 {
+  font-size: 24px;
+  font-weight: 600;
+  margin: 0;
+}
+
+.header-actions {
+  display: flex;
+  gap: 10px;
+}
+
+.import-btn {
+  border-color: #dcdfe6;
+}
+
+/* 克隆中状态样式 */
+.loading-container {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  color: #909399;
+}
+
+.loading-icon {
+  animation: rotate 1s linear infinite;
+  font-size: 14px;
+}
+
+.loading-text {
+  font-size: 12px;
+}
+
+@keyframes rotate {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+.role-table {
+  margin-top: 20px;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.role-info {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.role-details {
+  display: flex;
+  flex-direction: column;
+}
+
+.role-name {
+  font-weight: 500;
+}
+
+.role-id {
+  font-size: 12px;
+  color: #909399;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 10px;
+}
+
+.delete-btn {
+  color: #f56c6c;
+}
+
+.role-dialog .el-form-item {
+  margin-bottom: 20px;
+}
+
+.avatar-uploader {
+  display: inline-block;
+  width: 100px;
+  height: 100px;
+  border-radius: 50%;
+  overflow: hidden;
+  border: 1px dashed #d9d9d9;
+  cursor: pointer;
+  position: relative;
+  background-color: #f0f2f5;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.avatar-uploader:hover {
+  border-color: #409eff;
+}
+
+.avatar {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.avatar-actions {
+  margin-top: 10px;
+}
+
+.avatar-uploader .el-button {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background-color: transparent;
+  border: none;
+}
+
+.upload-voice {
+  margin-bottom: 10px;
+}
+
+.file-name {
+  display: flex;
+  align-items: center;
+  padding: 8px 12px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  margin-top: 5px;
+}
+
+.file-name .el-button {
+  margin-left: auto;
+}
+
+.cloning-status {
+  display: flex;
+  align-items: center;
+  padding: 8px 12px;
+  margin-top: 5px;
+  color: #67c23a;
+}
+
+.cloning-status .el-icon {
+  margin-right: 5px;
+  animation: spin 1s linear infinite;
+}
+
+
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+</style>

+ 133 - 0
src/views/Settings.vue

@@ -0,0 +1,133 @@
+<template>
+  <div class="settings-container">
+    <h1>Settings</h1>
+    
+    <el-card class="settings-card">
+      <template #header>
+        <div class="card-header">
+          <span>System Settings</span>
+        </div>
+      </template>
+      
+      <div class="settings-content">
+        <div class="setting-item">
+          <span class="setting-label">User Profile</span>
+          <el-button type="primary" @click="showConfirmDialog">Settings</el-button>
+        </div>
+        
+        <div class="setting-item">
+          <span class="setting-label">Account Security</span>
+          <el-button type="primary" @click="showConfirmDialog">Settings</el-button>
+        </div>
+        
+        <div class="setting-item">
+          <span class="setting-label">Notification Preferences</span>
+          <el-button type="primary" @click="showConfirmDialog">Settings</el-button>
+        </div>
+      </div>
+    </el-card>
+  </div>
+  
+  <!-- 二次确认弹窗 -->
+  <el-dialog
+    v-model="dialogVisible"
+    title="Settings Confirmation"
+    width="30%"
+    :before-close="handleClose"
+  >
+    <div class="dialog-content">
+      <p>Please confirm your action:</p>
+      <p>This will open the settings panel. Would you like to proceed?</p>
+    </div>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="dialogVisible = false">Cancel</el-button>
+        <el-button type="primary" @click="handleConfirm">Confirm</el-button>
+        <el-button type="danger" @click="handleLogout">Logout</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script>
+import { useStore } from '../store'
+import { ElMessage } from 'element-plus'
+
+export default {
+  name: 'Settings',
+  data() {
+    return {
+      dialogVisible: false
+    }
+  },
+  methods: {
+    showConfirmDialog() {
+      this.dialogVisible = true
+    },
+    
+    handleClose(done) {
+      this.dialogVisible = false
+    },
+    
+    handleConfirm() {
+      ElMessage.success('Settings opened successfully')
+      this.dialogVisible = false
+    },
+    
+    handleLogout() {
+      const store = useStore()
+      store.logout()
+      ElMessage.success('Logged out successfully')
+      this.dialogVisible = false
+      this.$router.push('/login')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.settings-container {
+  padding: 20px;
+}
+
+.settings-card {
+  margin-top: 20px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.settings-content {
+  padding: 20px 0;
+}
+
+.setting-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 15px 0;
+  border-bottom: 1px solid #e6e6e6;
+}
+
+.setting-item:last-child {
+  border-bottom: none;
+}
+
+.setting-label {
+  font-size: 14px;
+  color: #606266;
+}
+
+.dialog-content {
+  padding: 10px 0;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+</style>

+ 514 - 0
src/views/VoiceManagement.vue

@@ -0,0 +1,514 @@
+<template>
+  <div class="voice-management">
+    <div class="header">
+      <h2>Voice Management</h2>
+      <div class="header-actions">
+<!--        <el-upload-->
+<!--          class="import-btn"-->
+<!--          action="javascript:;"-->
+<!--          :show-file-list="false"-->
+<!--          :before-upload="handleExcelUpload"-->
+<!--          accept=".xlsx,.xls"-->
+<!--        >-->
+<!--          <el-button type="text">-->
+<!--            <el-icon><Upload /></el-icon>Import-->
+<!--          </el-button>-->
+<!--        </el-upload>-->
+<!--        <el-button type="primary" @click="showAddDialog">-->
+<!--          Add Voice-->
+<!--        </el-button>-->
+      </div>
+    </div>
+
+    <el-table :data="voices" v-loading="loading" style="width: 100%" class="voice-table">
+      <el-table-column label="Voice Name" min-width="200">
+        <template #default="scope">
+          <div class="voice-info">
+<!--            <div class="voice-icon">-->
+<!--              <el-icon><Microphone /></el-icon>-->
+<!--            </div>-->
+            <div class="voice-name">{{ scope.row.name }}</div>
+          </div>
+        </template>
+      </el-table-column>
+<!--      <el-table-column prop="description" label="Voice Description" min-width="250" />-->
+      <el-table-column prop="gender" label="Gender" width="200">
+        <template #default="scope">
+          {{ scope.row.gender === '1' ? 'Male' : 'Female' }}
+        </template>
+      </el-table-column>
+<!--      <el-table-column label="Original Voice" width="150">-->
+<!--        <template #default="scope">-->
+<!--          <el-button type="text" @click="playVoice(scope.row.originalVoice)">-->
+<!--            <el-icon><VideoPlay /></el-icon> 试听-->
+<!--          </el-button>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+      <el-table-column prop="createTime" label="create Date" width="120" >
+        <template #default="scope">
+          {{ scope.row.ctime}}
+        </template>
+      </el-table-column>
+<!--      <el-table-column label="Actions" width="150">-->
+<!--        <template #default="scope">-->
+<!--          <div class="action-buttons">-->
+<!--            <el-button type="text" @click="editVoice(scope.row)">-->
+<!--              <el-icon><Edit /></el-icon>-->
+<!--            </el-button>-->
+<!--            <el-button type="text" class="delete-btn" @click="confirmDelete(scope.row)">-->
+<!--              <el-icon><Delete /></el-icon>-->
+<!--            </el-button>-->
+<!--          </div>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+    </el-table>
+
+    <!-- 导入确认对话框 -->
+    <el-dialog 
+      title="导入声音"
+      v-model="importDialogVisible"
+      width="500px"
+    >
+      <div v-if="importFileName" class="import-file-info">
+        即将导入文件: {{ importFileName }}
+      </div>
+      <div v-else>
+        请先选择Excel文件
+      </div>
+      
+      <template #footer>
+        <el-button @click="importDialogVisible = false">取消</el-button>
+        <el-button 
+          type="primary" 
+          @click="confirmImport"
+          :disabled="!importFileName"
+        >
+          确认导入
+        </el-button>
+      </template>
+    </el-dialog>
+
+    <!-- Add Voice Dialog -->
+    <el-dialog
+      :title="dialogType === 'add' ? 'Add Voice' : 'Edit Voice'"
+      v-model="dialogVisible"
+      width="500px"
+      class="voice-dialog"
+    >
+      <el-form
+        ref="voiceFormRef"
+        :model="voiceForm"
+        :rules="rules"
+        label-width="120px"
+      >
+        <el-form-item label="Voice Name" prop="name">
+          <el-input v-model="voiceForm.name" placeholder="Enter voice name" />
+        </el-form-item>
+        <el-form-item label="Description" prop="description">
+          <el-input 
+            v-model="voiceForm.description" 
+            type="textarea" 
+            placeholder="Enter voice description" 
+            rows="3"
+          />
+        </el-form-item>
+        <el-form-item label="Gender" prop="gender">
+          <el-select v-model="voiceForm.gender" placeholder="Select gender" style="width: 100%">
+            <el-option label="Male" value="male" />
+            <el-option label="Female" value="female" />
+          </el-select>
+        </el-form-item>
+<!--        <el-form-item label="Original Voice" prop="originalVoice">-->
+<!--          <el-upload-->
+<!--            :show-file-list="false"-->
+<!--            :before-upload="beforeUploadVoice"-->
+<!--            :on-success="(response, file) => handleVoiceUploadSuccess(file, 'originalVoice')"-->
+<!--            :on-error="handleUploadError"-->
+<!--            class="upload-voice"-->
+<!--          >-->
+<!--            <el-button plain>-->
+<!--              <el-icon><Upload /></el-icon>-->
+<!--              {{ voiceForm.originalVoice ? 'Change File' : 'Upload File' }}-->
+<!--            </el-button>-->
+<!--          </el-upload>-->
+<!--          <div v-if="voiceForm.originalVoice" class="file-name">-->
+<!--            {{ voiceForm.originalVoice }}-->
+<!--            <el-button type="text" size="small" @click="removeVoiceFile('originalVoice')">-->
+<!--              <el-icon><CircleClose /></el-icon>-->
+<!--            </el-button>-->
+<!--          </div>-->
+<!--        </el-form-item>-->
+        <el-form-item label="Cloned Voice" prop="clonedVoice">
+          <div v-if="voiceForm.originalVoice && !voiceForm.clonedVoice" class="cloning-status">
+            <el-icon><Loading /></el-icon>
+            <span>正在从原始声音源文件克隆...</span>
+          </div>
+          <div v-else-if="voiceForm.clonedVoice" class="file-name">
+            {{ voiceForm.clonedVoice }}
+            <el-button type="text" size="small" @click="removeVoiceFile('clonedVoice')">
+              <el-icon><CircleClose /></el-icon>
+            </el-button>
+          </div>
+          <div v-else>
+            <span class="no-clone-hint">请先上传原始声音源文件</span>
+          </div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">Cancel</el-button>
+        <el-button type="primary" @click="submitForm">
+          {{ dialogType === 'add' ? 'Add' : 'Save Changes' }}
+        </el-button>
+      </template>
+    </el-dialog>
+
+
+    <Pagination
+        :current-page="currentPage"
+        :per-page="perPage"
+        :total-items="totalItems"
+        :max-displayed-pages="maxDisplayedPages"
+        :show-info="true"
+        @page-changed="onPageChange"
+    />
+  </div>
+</template>
+
+<script>
+import { Edit, Delete, VideoPlay, Upload, Microphone, CircleClose, Loading } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useStore } from '../store'
+import Pagination from './util/Pagination.vue'
+import anycallService from "../service/anycallService";
+
+
+export default {
+  name: 'VoiceManagement',
+  components: {
+    Pagination,
+    Edit,
+    Delete,
+    VideoPlay,
+    Upload,
+    CircleClose,
+    Loading,
+    Microphone: {
+      template: `<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-029747aa="">
+        <path fill="currentColor" d="M512 128a128 128 0 0 0-128 128v256a128 128 0 1 0 256 0V256a128 128 0 0 0-128-128zm0 64a64 64 0 0 1 64 64v256a64 64 0 1 1-128 0V256a64 64 0 0 1 64-64zM320 512a32 32 0 0 0-32 32v64a224 224 0 0 0 448 0v-64a32 32 0 0 0-64 0v64a160 160 0 0 1-320 0v-64a32 32 0 0 0-32-32z"></path>
+      </svg>`
+    }
+  },
+  data() {
+    const store = useStore()
+    return {
+      currentPage: 1,
+      perPage: 10,
+      totalItems: store.voicesCount,
+      maxDisplayedPages: 5,
+
+      voices: store.voices,
+      loading: false,
+      dialogVisible: false,
+      dialogType: 'add', // 'add' or 'edit'
+      importDialogVisible: false,
+      importFileName: '',
+      voiceForm: {
+        id: '',
+        description: '',
+        gender: '',
+        originalVoice: '',
+        clonedVoice: '',
+        uploadTime: ''
+      },
+      currentVoice: null,
+      rules: {
+        name: [
+          { required: true, message: 'Please enter voice name', trigger: 'blur' }
+        ],
+        description: [
+          { required: true, message: 'Please enter voice description', trigger: 'blur' }
+        ],
+        gender: [
+          { required: true, message: 'Please select gender', trigger: 'change' }
+        ],
+        originalVoice: [
+          { required: true, message: 'Please upload original voice file', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  methods: {
+    async onPageChange(page) {
+      this.currentPage = page
+      // 这里可以添加加载数据的逻辑
+      let pageData = {
+        page: page,
+        size: this.perPage,
+        system: true,
+        gender: 1
+      };
+      const response = await anycallService.voiceList(pageData);
+      this.voices = response.data.data.content.map(item => ({
+        id: item.id || '',
+        name: item.name || '',
+        gender: item.gender || '',
+        ctime: new Date(item.ctime).getFullYear()+'-'+new Date(item.ctime).getMonth()+'-'+new Date(item.ctime).getDay() || '',
+      }
+      ));
+      this.rolesCount = response.data.data.total;
+    },
+
+
+    // 处理Excel上传
+    handleExcelUpload(file) {
+      this.importDialogVisible = true;
+      return false; // 阻止默认上传
+    },
+    
+    // 确认导入
+    confirmImport() {
+      // 模拟导入处理
+      ElMessage.success(`文件 ${this.importFileName} 导入成功`);
+      this.importDialogVisible = false;
+      this.importFileName = '';
+      // 实际项目中应解析Excel文件并添加声音
+    },
+    
+    // Play voice (simulation)
+    playVoice(filename) {
+      ElMessage.info(`Playing voice file: ${filename} (simulation)`)
+    },
+    
+    // Show add dialog
+    showAddDialog() {
+      this.dialogType = 'add'
+      this.voiceForm = {
+        id: '',
+        description: '',
+        gender: '',
+        originalVoice: '',
+        clonedVoice: '',
+        uploadTime: ''
+      }
+      this.dialogVisible = true
+    },
+    
+    // Edit voice
+    editVoice(voice) {
+      this.dialogType = 'edit'
+      this.currentVoice = voice
+      this.voiceForm = { ...voice }
+      this.dialogVisible = true
+    },
+    
+    // Confirm delete
+    confirmDelete(voice) {
+      ElMessageBox.confirm(
+        `Are you sure you want to delete voice "${voice.description}"?`,
+        'Warning',
+        {
+          confirmButtonText: 'Delete',
+          cancelButtonText: 'Cancel',
+          type: 'warning'
+        }
+      ).then(() => {
+        const store = useStore()
+        store.deleteVoice(voice.id)
+        this.voices = store.voices
+        ElMessage.success('Voice deleted successfully')
+      }).catch(() => {})
+    },
+    
+    // 上传前检查 - 只接受原始声音源文件
+    beforeUploadVoice(file) {
+      const isAudio = ['audio/mpeg', 'audio/wav', 'audio/mp3'].includes(file.type) || file.name.endsWith('.mp3') || file.name.endsWith('.wav');
+      const isLt20M = file.size / 1024 / 1024 < 20;
+      
+      if (!isAudio) {
+        ElMessage.error('请上传音频格式文件!');
+      }
+      if (!isLt20M) {
+        ElMessage.error('上传文件大小不能超过20MB!');
+      }
+      
+      return isAudio && isLt20M;
+    },
+    
+    // 处理上传成功
+    handleVoiceUploadSuccess(file, field) {
+      // 模拟上传成功,实际应用中应该根据服务器返回设置文件路径
+      this.voiceForm[field] = file.name;
+      ElMessage.success(`${field === 'originalVoice' ? '原始' : '克隆'} 声音文件上传成功`);
+      
+      // 如果上传的是原始声音,模拟系统自动克隆过程
+      if (field === 'originalVoice' && !this.voiceForm.clonedVoice) {
+        // 模拟克隆过程的延迟
+        setTimeout(() => {
+          // 生成克隆文件名
+          const originalFileName = file.name;
+          const clonedFileName = originalFileName.replace('original-', 'cloned-');
+          this.voiceForm.clonedVoice = clonedFileName;
+          ElMessage.success('已从原始声音源文件成功克隆声音');
+        }, 2000); // 模拟2秒的克隆时间
+      }
+    },
+    
+    // 处理上传错误
+    handleUploadError() {
+      ElMessage.error('File upload failed');
+    },
+    
+    // 移除已上传的文件
+    removeVoiceFile(field) {
+      this.voiceForm[field] = '';
+    },
+    
+    // Submit form
+    submitForm() {
+      this.$refs.voiceFormRef.validate((valid) => {
+        if (valid) {
+          const store = useStore()
+          if (this.dialogType === 'add') {
+            store.addVoice({
+              ...this.voiceForm,
+              id: Date.now().toString(),
+              uploadTime: new Date().toLocaleDateString()
+            })
+            ElMessage.success('Voice added successfully')
+          } else {
+            store.updateVoice(this.currentVoice.id, this.voiceForm)
+            ElMessage.success('Voice updated successfully')
+          }
+          this.voices = store.voices
+          this.dialogVisible = false
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style>
+.voice-management {
+  padding: 20px;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.header h2 {
+  font-size: 24px;
+  font-weight: 600;
+  margin: 0;
+}
+
+.header-actions {
+  display: flex;
+  gap: 10px;
+}
+
+.import-btn {
+  border-color: #dcdfe6;
+}
+
+.voice-table {
+  margin-top: 20px;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.voice-info {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.voice-icon {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  background-color: #f0f2f5;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #409eff;
+}
+
+.voice-details {
+  display: flex;
+  flex-direction: column;
+}
+
+.voice-name {
+  font-weight: 500;
+}
+
+.voice-id {
+  font-size: 12px;
+  color: #909399;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 10px;
+}
+
+.delete-btn {
+  color: #f56c6c;
+}
+
+.upload-box {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.file-name {
+  color: #909399;
+  font-size: 14px;
+}
+
+.voice-dialog .el-form-item {
+  margin-bottom: 20px;
+}
+
+.upload-voice {
+  display: inline-block;
+}
+
+.file-name {
+  margin-top: 10px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  font-size: 14px;
+  color: #606266;
+}
+
+.cloning-status {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  color: #409eff;
+  font-size: 14px;
+}
+
+.no-clone-hint {
+  color: #909399;
+  font-size: 14px;
+}
+
+.voice-info {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.voice-name {
+  font-weight: 500;
+}
+</style>

+ 193 - 0
src/views/util/Pagination.vue

@@ -0,0 +1,193 @@
+<template>
+  <div>
+    <div class="pagination-container">
+      <ul class="pagination">
+        <li class="pagination-item">
+          <a
+              class="pagination-link"
+              :class="{ disabled: currentPage === 1 }"
+              @click="goToPage(currentPage - 1)"
+          >
+            <i class="fas fa-chevron-left"></i>
+          </a>
+        </li>
+
+        <li
+            v-for="page in displayedPages"
+            :key="page"
+            class="pagination-item"
+        >
+          <a
+              class="pagination-link"
+              :class="{ active: page === currentPage }"
+              @click="goToPage(page)"
+          >
+            {{ page }}
+          </a>
+        </li>
+
+        <li class="pagination-item">
+          <a
+              class="pagination-link"
+              :class="{ disabled: currentPage === totalPages }"
+              @click="goToPage(currentPage + 1)"
+          >
+            <i class="fas fa-chevron-right"></i>
+          </a>
+        </li>
+      </ul>
+    </div>
+
+    <div v-if="showInfo" class="pagination-info">
+      显示第 {{ startItem }} 到 {{ endItem }} 条记录,共 {{ totalItems }} 条记录
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Pagination',
+  props: {
+    currentPage: {
+      type: Number,
+      default: 1
+    },
+    perPage: {
+      type: Number,
+      default: 10
+    },
+    totalItems: {
+      type: Number,
+      default: 0
+    },
+    maxDisplayedPages: {
+      type: Number,
+      default: 5
+    },
+    showInfo: {
+      type: Boolean,
+      default: true
+    }
+  },
+  computed: {
+    totalPages() {
+      return Math.ceil(this.totalItems / this.perPage);
+    },
+
+    displayedPages() {
+      const half = Math.floor(this.maxDisplayedPages / 2);
+      let start = this.currentPage - half;
+      let end = this.currentPage + half;
+
+      if (start < 1) {
+        start = 1;
+        end = Math.min(this.maxDisplayedPages, this.totalPages);
+      }
+
+      if (end > this.totalPages) {
+        end = this.totalPages;
+        start = Math.max(1, end - this.maxDisplayedPages + 1);
+      }
+
+      const pages = [];
+      for (let i = start; i <= end; i++) {
+        pages.push(i);
+      }
+
+      return pages;
+    },
+
+    startItem() {
+      return (this.currentPage - 1) * this.perPage + 1;
+    },
+
+    endItem() {
+      const end = this.currentPage * this.perPage;
+      return end > this.totalItems ? this.totalItems : end;
+    }
+  },
+  methods: {
+    goToPage(page) {
+      // 修复了变量名错误
+      if (page < 1) page = 1;
+      if (page > this.totalPages) page = this.totalPages;
+
+      this.$emit('page-changed', page);
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+  display: flex;
+  justify-content: center;
+  margin: 24px 0;
+}
+
+.pagination {
+  display: flex;
+  list-style: none;
+  padding: 0;
+  margin: 0;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+.pagination-item {
+  margin: 0;
+}
+
+.pagination-link {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 40px;
+  height: 40px;
+  padding: 0 12px;
+  background: white;
+  color: #4a5568;
+  font-weight: 500;
+  text-decoration: none;
+  border: 1px solid #e2e8f0;
+  transition: all 0.3s;
+  cursor: pointer;
+}
+
+.pagination-item:not(:last-child) .pagination-link {
+  border-right: none;
+}
+
+.pagination-link:hover {
+  background: #f7fafc;
+  color: #4a6bdf;
+}
+
+.pagination-link.active {
+  background: #4a6bdf;
+  color: white;
+  border-color: #4a6bdf;
+}
+
+.pagination-link.disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.pagination-info {
+  text-align: center;
+  color: #718096;
+  margin-top: 16px;
+  font-size: 14px;
+}
+
+@media (max-width: 768px) {
+  .pagination-link {
+    min-width: 36px;
+    height: 36px;
+    padding: 0 8px;
+    font-size: 14px;
+  }
+}
+</style>

+ 12 - 0
vite.config.js

@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  server: {
+    port: 8081,
+    strictPort: true
+  },
+  base: './'
+})

Vissa filer visades inte eftersom för många filer har ändrats