From 90f7af7afa04d39c97f1727c4b6d7ec2708c5aac Mon Sep 17 00:00:00 2001
From: Junsen <951434130@qq.com>
Date: Fri, 8 Aug 2025 13:02:39 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E7=A7=BB?=
=?UTF-8?q?=E5=8A=A8=E7=AB=AF=E9=A1=B9=E7=9B=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
main/xiaozhi-mobile-admin/.commitlintrc.cjs | 3 +
main/xiaozhi-mobile-admin/.editorconfig | 13 +
main/xiaozhi-mobile-admin/.gitignore | 44 +
main/xiaozhi-mobile-admin/.husky/commit-msg | 1 +
main/xiaozhi-mobile-admin/.husky/pre-commit | 1 +
main/xiaozhi-mobile-admin/.npmrc | 8 +
main/xiaozhi-mobile-admin/LICENSE | 21 +
main/xiaozhi-mobile-admin/README.md | 93 +
main/xiaozhi-mobile-admin/eslint.config.mjs | 43 +
main/xiaozhi-mobile-admin/favicon.ico | Bin 0 -> 14575 bytes
main/xiaozhi-mobile-admin/index.html | 26 +
main/xiaozhi-mobile-admin/manifest.config.ts | 155 +
main/xiaozhi-mobile-admin/openapi-spec.yaml | 1086 ++
.../openapi-ts-request.config.ts | 13 +
main/xiaozhi-mobile-admin/pages.config.ts | 23 +
.../patches/@dcloudio__uni-h5.patch | 13 +
main/xiaozhi-mobile-admin/pnpm-lock.yaml | 14871 ++++++++++++++++
main/xiaozhi-mobile-admin/pnpm-workspace.yaml | 6 +
.../scripts/postupgrade.js | 35 +
main/xiaozhi-mobile-admin/src/App.vue | 39 +
.../src/api/agent/agent.ts | 185 +
.../src/api/agent/types.ts | 107 +
main/xiaozhi-mobile-admin/src/api/auth.ts | 127 +
.../src/api/chat-history/chat-history.ts | 57 +
.../src/api/chat-history/index.ts | 2 +
.../src/api/chat-history/types.ts | 38 +
.../src/api/device/device.ts | 55 +
.../src/api/device/index.ts | 2 +
.../src/api/device/types.ts | 21 +
.../src/api/voiceprint/index.ts | 2 +
.../src/api/voiceprint/types.ts | 29 +
.../src/api/voiceprint/voiceprint.ts | 62 +
.../src/components/.gitkeep | 0
main/xiaozhi-mobile-admin/src/env.d.ts | 34 +
main/xiaozhi-mobile-admin/src/hooks/.gitkeep | 0
.../src/hooks/usePageAuth.ts | 50 +
.../src/hooks/useRequest.ts | 51 +
.../src/hooks/useUpload.ts | 160 +
main/xiaozhi-mobile-admin/src/http/README.md | 36 +
.../src/http/request/alova.ts | 119 +
.../src/http/request/enum.ts | 66 +
.../src/http/request/types.ts | 22 +
.../src/layouts/default.vue | 17 +
.../src/layouts/fg-tabbar/fg-tabbar.vue | 68 +
.../src/layouts/fg-tabbar/tabbar.md | 17 +
.../src/layouts/fg-tabbar/tabbar.ts | 11 +
.../src/layouts/fg-tabbar/tabbarList.ts | 97 +
.../src/layouts/tabbar.vue | 19 +
main/xiaozhi-mobile-admin/src/main.ts | 19 +
.../src/pages-sub/demo/index.vue | 27 +
.../src/pages/agent/edit.vue | 1085 ++
.../src/pages/agent/tools.vue | 726 +
.../src/pages/chat-history/detail.vue | 331 +
.../src/pages/chat-history/index.vue | 342 +
.../components/ultrasonic-config.vue | 684 +
.../device-config/components/wifi-config.vue | 230 +
.../components/wifi-selector.vue | 556 +
.../src/pages/device-config/index.vue | 154 +
.../src/pages/device/index.vue | 367 +
.../src/pages/index/index.vue | 599 +
.../src/pages/login/index.vue | 777 +
.../src/pages/register/index.vue | 877 +
.../src/pages/voiceprint/index.vue | 544 +
.../src/router/interceptor.ts | 66 +
.../src/static/app/icons/1024x1024.png | Bin 0 -> 458242 bytes
.../src/static/app/icons/120x120.png | Bin 0 -> 17805 bytes
.../src/static/app/icons/144x144.png | Bin 0 -> 24176 bytes
.../src/static/app/icons/152x152.png | Bin 0 -> 26428 bytes
.../src/static/app/icons/167x167.png | Bin 0 -> 30883 bytes
.../src/static/app/icons/180x180.png | Bin 0 -> 35063 bytes
.../src/static/app/icons/192x192.png | Bin 0 -> 38903 bytes
.../src/static/app/icons/20x20.png | Bin 0 -> 1330 bytes
.../src/static/app/icons/29x29.png | Bin 0 -> 1471 bytes
.../src/static/app/icons/40x40.png | Bin 0 -> 1709 bytes
.../src/static/app/icons/58x58.png | Bin 0 -> 2210 bytes
.../src/static/app/icons/60x60.png | Bin 0 -> 2275 bytes
.../src/static/app/icons/72x72.png | Bin 0 -> 2708 bytes
.../src/static/app/icons/76x76.png | Bin 0 -> 2833 bytes
.../src/static/app/icons/80x80.png | Bin 0 -> 2962 bytes
.../src/static/app/icons/87x87.png | Bin 0 -> 3266 bytes
.../src/static/app/icons/96x96.png | Bin 0 -> 3680 bytes
.../src/static/images/.gitkeep | 0
.../src/static/images/default-avatar.png | Bin 0 -> 560 bytes
.../src/static/images/home/main-top-bg.png | Bin 0 -> 978384 bytes
main/xiaozhi-mobile-admin/src/static/logo.png | Bin 0 -> 13372 bytes
main/xiaozhi-mobile-admin/src/static/logo.svg | 33 +
.../src/static/tabbar/chat.png | Bin 0 -> 2153 bytes
.../src/static/tabbar/chat_activate.png | Bin 0 -> 2155 bytes
.../src/static/tabbar/device.png | Bin 0 -> 4058 bytes
.../src/static/tabbar/device_activate.png | Bin 0 -> 4040 bytes
.../src/static/tabbar/example.png | Bin 0 -> 1371 bytes
.../src/static/tabbar/exampleHL.png | Bin 0 -> 1398 bytes
.../src/static/tabbar/home.png | Bin 0 -> 1346 bytes
.../src/static/tabbar/homeHL.png | Bin 0 -> 1415 bytes
.../src/static/tabbar/microphone.png | Bin 0 -> 3189 bytes
.../src/static/tabbar/microphone_activate.png | Bin 0 -> 3273 bytes
.../src/static/tabbar/personal.png | Bin 0 -> 2457 bytes
.../src/static/tabbar/personalHL.png | Bin 0 -> 2534 bytes
.../src/static/tabbar/robot.png | Bin 0 -> 3471 bytes
.../src/static/tabbar/robot_activate.png | Bin 0 -> 3508 bytes
main/xiaozhi-mobile-admin/src/store/agent.ts | 122 +
main/xiaozhi-mobile-admin/src/store/config.ts | 65 +
main/xiaozhi-mobile-admin/src/store/index.ts | 20 +
main/xiaozhi-mobile-admin/src/store/plugin.ts | 58 +
main/xiaozhi-mobile-admin/src/store/user.ts | 83 +
.../src/style/iconfont.css | 28 +
.../xiaozhi-mobile-admin/src/style/index.scss | 19 +
main/xiaozhi-mobile-admin/src/typings.d.ts | 28 +
main/xiaozhi-mobile-admin/src/typings.ts | 15 +
main/xiaozhi-mobile-admin/src/uni.scss | 77 +
main/xiaozhi-mobile-admin/src/utils/index.ts | 172 +
.../src/utils/platform.ts | 24 +
.../xiaozhi-mobile-admin/src/utils/request.ts | 78 +
main/xiaozhi-mobile-admin/src/utils/toast.ts | 65 +
.../src/utils/uploadFile.ts | 324 +
main/xiaozhi-mobile-admin/uno.config.ts | 65 +
.../unpackage/res/icons/1024x1024.png | Bin 0 -> 458242 bytes
.../unpackage/res/icons/120x120.png | Bin 0 -> 17805 bytes
.../unpackage/res/icons/144x144.png | Bin 0 -> 24176 bytes
.../unpackage/res/icons/152x152.png | Bin 0 -> 26428 bytes
.../unpackage/res/icons/167x167.png | Bin 0 -> 30883 bytes
.../unpackage/res/icons/180x180.png | Bin 0 -> 35063 bytes
.../unpackage/res/icons/192x192.png | Bin 0 -> 38903 bytes
.../unpackage/res/icons/20x20.png | Bin 0 -> 1330 bytes
.../unpackage/res/icons/29x29.png | Bin 0 -> 1471 bytes
.../unpackage/res/icons/40x40.png | Bin 0 -> 1709 bytes
.../unpackage/res/icons/58x58.png | Bin 0 -> 2210 bytes
.../unpackage/res/icons/60x60.png | Bin 0 -> 2275 bytes
.../unpackage/res/icons/72x72.png | Bin 0 -> 2708 bytes
.../unpackage/res/icons/76x76.png | Bin 0 -> 2833 bytes
.../unpackage/res/icons/80x80.png | Bin 0 -> 2962 bytes
.../unpackage/res/icons/87x87.png | Bin 0 -> 3266 bytes
.../unpackage/res/icons/96x96.png | Bin 0 -> 3680 bytes
main/xiaozhi-mobile-admin/vite.config.ts | 182 +
134 files changed, 26790 insertions(+)
create mode 100644 main/xiaozhi-mobile-admin/.commitlintrc.cjs
create mode 100644 main/xiaozhi-mobile-admin/.editorconfig
create mode 100644 main/xiaozhi-mobile-admin/.gitignore
create mode 100644 main/xiaozhi-mobile-admin/.husky/commit-msg
create mode 100644 main/xiaozhi-mobile-admin/.husky/pre-commit
create mode 100644 main/xiaozhi-mobile-admin/.npmrc
create mode 100644 main/xiaozhi-mobile-admin/LICENSE
create mode 100644 main/xiaozhi-mobile-admin/README.md
create mode 100644 main/xiaozhi-mobile-admin/eslint.config.mjs
create mode 100644 main/xiaozhi-mobile-admin/favicon.ico
create mode 100644 main/xiaozhi-mobile-admin/index.html
create mode 100644 main/xiaozhi-mobile-admin/manifest.config.ts
create mode 100644 main/xiaozhi-mobile-admin/openapi-spec.yaml
create mode 100644 main/xiaozhi-mobile-admin/openapi-ts-request.config.ts
create mode 100644 main/xiaozhi-mobile-admin/pages.config.ts
create mode 100644 main/xiaozhi-mobile-admin/patches/@dcloudio__uni-h5.patch
create mode 100644 main/xiaozhi-mobile-admin/pnpm-lock.yaml
create mode 100644 main/xiaozhi-mobile-admin/pnpm-workspace.yaml
create mode 100644 main/xiaozhi-mobile-admin/scripts/postupgrade.js
create mode 100644 main/xiaozhi-mobile-admin/src/App.vue
create mode 100644 main/xiaozhi-mobile-admin/src/api/agent/agent.ts
create mode 100644 main/xiaozhi-mobile-admin/src/api/agent/types.ts
create mode 100644 main/xiaozhi-mobile-admin/src/api/auth.ts
create mode 100644 main/xiaozhi-mobile-admin/src/api/chat-history/chat-history.ts
create mode 100644 main/xiaozhi-mobile-admin/src/api/chat-history/index.ts
create mode 100644 main/xiaozhi-mobile-admin/src/api/chat-history/types.ts
create mode 100644 main/xiaozhi-mobile-admin/src/api/device/device.ts
create mode 100644 main/xiaozhi-mobile-admin/src/api/device/index.ts
create mode 100644 main/xiaozhi-mobile-admin/src/api/device/types.ts
create mode 100644 main/xiaozhi-mobile-admin/src/api/voiceprint/index.ts
create mode 100644 main/xiaozhi-mobile-admin/src/api/voiceprint/types.ts
create mode 100644 main/xiaozhi-mobile-admin/src/api/voiceprint/voiceprint.ts
create mode 100644 main/xiaozhi-mobile-admin/src/components/.gitkeep
create mode 100644 main/xiaozhi-mobile-admin/src/env.d.ts
create mode 100644 main/xiaozhi-mobile-admin/src/hooks/.gitkeep
create mode 100644 main/xiaozhi-mobile-admin/src/hooks/usePageAuth.ts
create mode 100644 main/xiaozhi-mobile-admin/src/hooks/useRequest.ts
create mode 100644 main/xiaozhi-mobile-admin/src/hooks/useUpload.ts
create mode 100644 main/xiaozhi-mobile-admin/src/http/README.md
create mode 100644 main/xiaozhi-mobile-admin/src/http/request/alova.ts
create mode 100644 main/xiaozhi-mobile-admin/src/http/request/enum.ts
create mode 100644 main/xiaozhi-mobile-admin/src/http/request/types.ts
create mode 100644 main/xiaozhi-mobile-admin/src/layouts/default.vue
create mode 100644 main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/fg-tabbar.vue
create mode 100644 main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/tabbar.md
create mode 100644 main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/tabbar.ts
create mode 100644 main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/tabbarList.ts
create mode 100644 main/xiaozhi-mobile-admin/src/layouts/tabbar.vue
create mode 100644 main/xiaozhi-mobile-admin/src/main.ts
create mode 100644 main/xiaozhi-mobile-admin/src/pages-sub/demo/index.vue
create mode 100644 main/xiaozhi-mobile-admin/src/pages/agent/edit.vue
create mode 100644 main/xiaozhi-mobile-admin/src/pages/agent/tools.vue
create mode 100644 main/xiaozhi-mobile-admin/src/pages/chat-history/detail.vue
create mode 100644 main/xiaozhi-mobile-admin/src/pages/chat-history/index.vue
create mode 100644 main/xiaozhi-mobile-admin/src/pages/device-config/components/ultrasonic-config.vue
create mode 100644 main/xiaozhi-mobile-admin/src/pages/device-config/components/wifi-config.vue
create mode 100644 main/xiaozhi-mobile-admin/src/pages/device-config/components/wifi-selector.vue
create mode 100644 main/xiaozhi-mobile-admin/src/pages/device-config/index.vue
create mode 100644 main/xiaozhi-mobile-admin/src/pages/device/index.vue
create mode 100644 main/xiaozhi-mobile-admin/src/pages/index/index.vue
create mode 100644 main/xiaozhi-mobile-admin/src/pages/login/index.vue
create mode 100644 main/xiaozhi-mobile-admin/src/pages/register/index.vue
create mode 100644 main/xiaozhi-mobile-admin/src/pages/voiceprint/index.vue
create mode 100644 main/xiaozhi-mobile-admin/src/router/interceptor.ts
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/1024x1024.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/120x120.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/144x144.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/152x152.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/167x167.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/180x180.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/192x192.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/20x20.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/29x29.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/40x40.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/58x58.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/60x60.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/72x72.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/76x76.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/80x80.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/87x87.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/app/icons/96x96.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/images/.gitkeep
create mode 100644 main/xiaozhi-mobile-admin/src/static/images/default-avatar.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/images/home/main-top-bg.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/logo.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/logo.svg
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/chat.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/chat_activate.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/device.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/device_activate.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/example.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/exampleHL.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/home.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/homeHL.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/microphone.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/microphone_activate.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/personal.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/personalHL.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/robot.png
create mode 100644 main/xiaozhi-mobile-admin/src/static/tabbar/robot_activate.png
create mode 100644 main/xiaozhi-mobile-admin/src/store/agent.ts
create mode 100644 main/xiaozhi-mobile-admin/src/store/config.ts
create mode 100644 main/xiaozhi-mobile-admin/src/store/index.ts
create mode 100644 main/xiaozhi-mobile-admin/src/store/plugin.ts
create mode 100644 main/xiaozhi-mobile-admin/src/store/user.ts
create mode 100644 main/xiaozhi-mobile-admin/src/style/iconfont.css
create mode 100644 main/xiaozhi-mobile-admin/src/style/index.scss
create mode 100644 main/xiaozhi-mobile-admin/src/typings.d.ts
create mode 100644 main/xiaozhi-mobile-admin/src/typings.ts
create mode 100644 main/xiaozhi-mobile-admin/src/uni.scss
create mode 100644 main/xiaozhi-mobile-admin/src/utils/index.ts
create mode 100644 main/xiaozhi-mobile-admin/src/utils/platform.ts
create mode 100644 main/xiaozhi-mobile-admin/src/utils/request.ts
create mode 100644 main/xiaozhi-mobile-admin/src/utils/toast.ts
create mode 100644 main/xiaozhi-mobile-admin/src/utils/uploadFile.ts
create mode 100644 main/xiaozhi-mobile-admin/uno.config.ts
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/1024x1024.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/120x120.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/144x144.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/152x152.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/167x167.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/180x180.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/192x192.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/20x20.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/29x29.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/40x40.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/58x58.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/60x60.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/72x72.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/76x76.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/80x80.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/87x87.png
create mode 100644 main/xiaozhi-mobile-admin/unpackage/res/icons/96x96.png
create mode 100644 main/xiaozhi-mobile-admin/vite.config.ts
diff --git a/main/xiaozhi-mobile-admin/.commitlintrc.cjs b/main/xiaozhi-mobile-admin/.commitlintrc.cjs
new file mode 100644
index 00000000..98ee7dfc
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/.commitlintrc.cjs
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: ['@commitlint/config-conventional'],
+}
diff --git a/main/xiaozhi-mobile-admin/.editorconfig b/main/xiaozhi-mobile-admin/.editorconfig
new file mode 100644
index 00000000..7f098647
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/.editorconfig
@@ -0,0 +1,13 @@
+root = true
+
+[*] # 表示所有文件适用
+charset = utf-8 # 设置文件字符集为 utf-8
+indent_style = space # 缩进风格(tab | space)
+indent_size = 2 # 缩进大小
+end_of_line = lf # 控制换行类型(lf | cr | crlf)
+trim_trailing_whitespace = true # 去除行首的任意空白字符
+insert_final_newline = true # 始终在文件末尾插入一个新行
+
+[*.md] # 表示仅 md 文件适用以下规则
+max_line_length = off # 关闭最大行长度限制
+trim_trailing_whitespace = false # 关闭末尾空格修剪
diff --git a/main/xiaozhi-mobile-admin/.gitignore b/main/xiaozhi-mobile-admin/.gitignore
new file mode 100644
index 00000000..34ddbdf3
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/.gitignore
@@ -0,0 +1,44 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+*.local
+
+# Editor directories and files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+.hbuilderx
+
+.stylelintcache
+.eslintcache
+
+docs/.vitepress/dist
+docs/.vitepress/cache
+
+src/types
+
+# lock 文件还是不要了,我主要的版本写死就好了
+# pnpm-lock.yaml
+# package-lock.json
+
+# TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作
+# `git rm -r --cached .` 然后提交 commit 即可。
+
+# git rm -r --cached file1 file2 ## 针对某些文件
+# git rm -r --cached dir1 dir2 ## 针对某些文件夹
+# git rm -r --cached . ## 针对所有文件
+
+# 更新 uni-app 官方版本
+# npx @dcloudio/uvm@latest
diff --git a/main/xiaozhi-mobile-admin/.husky/commit-msg b/main/xiaozhi-mobile-admin/.husky/commit-msg
new file mode 100644
index 00000000..36158d92
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/.husky/commit-msg
@@ -0,0 +1 @@
+npx --no-install commitlint --edit "$1"
\ No newline at end of file
diff --git a/main/xiaozhi-mobile-admin/.husky/pre-commit b/main/xiaozhi-mobile-admin/.husky/pre-commit
new file mode 100644
index 00000000..c3ec64be
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/.husky/pre-commit
@@ -0,0 +1 @@
+npx lint-staged --allow-empty
\ No newline at end of file
diff --git a/main/xiaozhi-mobile-admin/.npmrc b/main/xiaozhi-mobile-admin/.npmrc
new file mode 100644
index 00000000..10ecfe22
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/.npmrc
@@ -0,0 +1,8 @@
+# registry = https://registry.npmjs.org
+registry = https://registry.npmmirror.com
+
+strict-peer-dependencies=false
+auto-install-peers=true
+shamefully-hoist=true
+ignore-workspace-root-check=true
+install-workspace-root=true
diff --git a/main/xiaozhi-mobile-admin/LICENSE b/main/xiaozhi-mobile-admin/LICENSE
new file mode 100644
index 00000000..9e91d10b
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 菲鸽
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/main/xiaozhi-mobile-admin/README.md b/main/xiaozhi-mobile-admin/README.md
new file mode 100644
index 00000000..c088199e
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/README.md
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+旧仓库 codercup 进不去了,star 也拿不回来,这里也展示一下那个地址的 star.
+
+[](https://github.com/codercup/unibest)
+[](https://github.com/codercup/unibest)
+
+
+
+
+
+[](https://github.com/feige996/unibest)
+[](https://github.com/feige996/unibest)
+[](https://gitee.com/feige996/unibest/stargazers)
+[](https://gitee.com/feige996/unibest/members)
+
+
+
+
+
+
+
+`unibest` —— 最好的 `uniapp` 开发模板,由 `uniapp` + `Vue3` + `Ts` + `Vite5` + `UnoCss` + `wot-ui` + `z-paging` 构成,使用了最新的前端技术栈,无需依靠 `HBuilderX`,通过命令行方式运行 `web`、`小程序` 和 `App`(编辑器推荐 `VSCode`,可选 `webstorm`)。
+
+`unibest` 内置了 `约定式路由`、`layout布局`、`请求封装`、`请求拦截`、`登录拦截`、`UnoCSS`、`i18n多语言` 等基础功能,提供了 `代码提示`、`自动格式化`、`统一配置`、`代码片段` 等辅助功能,让你编写 `uniapp` 拥有 `best` 体验 ( `unibest 的由来`)。
+
+
+
+
+ 📖 文档地址(new)
+ |
+ 📱 DEMO 地址
+
+
+---
+
+注意旧的地址 [codercup](https://github.com/codercup/unibest) 我进不去了,使用新的 [feige996](https://github.com/feige996/unibest)。PR和 issue 也请使用新地址,否则无法合并。
+
+## 平台兼容性
+
+| H5 | IOS | 安卓 | 微信小程序 | 字节小程序 | 快手小程序 | 支付宝小程序 | 钉钉小程序 | 百度小程序 |
+| --- | --- | ---- | ---------- | ---------- | ---------- | ------------ | ---------- | ---------- |
+| √ | √ | √ | √ | √ | √ | √ | √ | √ |
+
+注意每种 `UI框架` 支持的平台有所不同,详情请看各 `UI框架` 的官网,也可以看 `unibest` 文档。
+
+## ⚙️ 环境
+
+- node>=18
+- pnpm>=7.30
+- Vue Official>=2.1.10
+- TypeScript>=5.0
+
+## 📂 快速开始
+
+执行 `pnpm create unibest` 创建项目
+执行 `pnpm i` 安装依赖
+执行 `pnpm dev` 运行 `H5`
+执行 `pnpm dev:mp` 运行 `微信小程序`
+
+## 📦 运行(支持热更新)
+
+- web平台: `pnpm dev:h5`, 然后打开 [http://localhost:9000/](http://localhost:9000/)。
+- weixin平台:`pnpm dev:mp` 然后打开微信开发者工具,导入本地文件夹,选择本项目的`dist/dev/mp-weixin` 文件。
+- APP平台:`pnpm dev:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/dev/app` 文件夹,选择运行到模拟器(开发时优先使用),或者运行的安卓/ios基座。
+
+## 🔗 发布
+
+- web平台: `pnpm build:h5`,打包后的文件在 `dist/build/h5`,可以放到web服务器,如nginx运行。如果最终不是放在根目录,可以在 `manifest.config.ts` 文件的 `h5.router.base` 属性进行修改。
+- weixin平台:`pnpm build:mp`, 打包后的文件在 `dist/build/mp-weixin`,然后通过微信开发者工具导入,并点击右上角的“上传”按钮进行上传。
+- APP平台:`pnpm build:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/build/app` 文件夹,选择发行 - APP云打包。
+
+## 📄 License
+
+[MIT](https://opensource.org/license/mit/)
+
+Copyright (c) 2025 菲鸽
+
+## 捐赠
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/eslint.config.mjs b/main/xiaozhi-mobile-admin/eslint.config.mjs
new file mode 100644
index 00000000..54ce246d
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/eslint.config.mjs
@@ -0,0 +1,43 @@
+import uniHelper from '@uni-helper/eslint-config'
+
+export default uniHelper({
+ unocss: true,
+ vue: true,
+ markdown: false,
+ ignores: [
+ 'src/uni_modules/',
+ 'dist',
+ // unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用
+ 'auto-import.d.ts',
+ // vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore
+ 'uni-pages.d.ts',
+ // 插件生成的文件
+ 'src/pages.json',
+ 'src/manifest.json',
+ // 忽略自动生成文件
+ 'src/service/app/**',
+ ],
+ rules: {
+ 'no-console': 'off',
+ 'no-unused-vars': 'off',
+ 'vue/no-unused-refs': 'off',
+ 'unused-imports/no-unused-vars': 'off',
+ 'eslint-comments/no-unlimited-disable': 'off',
+ 'jsdoc/check-param-names': 'off',
+ 'jsdoc/require-returns-description': 'off',
+ 'ts/no-empty-object-type': 'off',
+ 'no-extend-native': 'off',
+ },
+ formatters: {
+ /**
+ * Format CSS, LESS, SCSS files, also the `
diff --git a/main/xiaozhi-mobile-admin/src/api/agent/agent.ts b/main/xiaozhi-mobile-admin/src/api/agent/agent.ts
new file mode 100644
index 00000000..09f352f2
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/api/agent/agent.ts
@@ -0,0 +1,185 @@
+import type {
+ Agent,
+ AgentCreateData,
+ AgentDetail,
+ ModelOption,
+ RoleTemplate,
+} from './types'
+import { http } from '@/http/request/alova'
+
+// 获取智能体详情
+export function getAgentDetail(id: string) {
+ return http.Get(`/agent/${id}`, {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+// 获取角色模板列表
+export function getRoleTemplates() {
+ return http.Get('/agent/template', {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+// 获取模型选项
+export function getModelOptions(modelType: string, modelName: string = '') {
+ return http.Get('/models/names', {
+ params: {
+ modelType,
+ modelName,
+ },
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+// 获取智能体列表
+export function getAgentList() {
+ return http.Get('/agent/list', {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+// 创建智能体
+export function createAgent(data: AgentCreateData) {
+ return http.Post('/agent', data, {
+ meta: {
+ ignoreAuth: false,
+ toast: true,
+ },
+ })
+}
+
+// 删除智能体
+export function deleteAgent(id: string) {
+ return http.Delete(`/agent/${id}`, {
+ meta: {
+ ignoreAuth: false,
+ toast: true,
+ },
+ })
+}
+
+// 获取TTS音色列表
+export function getTTSVoices(ttsModelId: string, voiceName: string = '') {
+ return http.Get<{ id: string, name: string }[]>(`/models/${ttsModelId}/voices`, {
+ params: {
+ voiceName,
+ },
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+// 更新智能体
+export function updateAgent(id: string, data: Partial) {
+ return http.Put(`/agent/${id}`, data, {
+ meta: {
+ ignoreAuth: false,
+ toast: true,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+// 获取插件列表
+export function getPluginFunctions() {
+ return http.Get(`/models/provider/plugin/names`, {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+// 获取mcp接入点
+export function getMcpAddress(agentId: string) {
+ return http.Get(`/agent/mcp/address/${agentId}`, {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ })
+}
+
+// 获取mcp工具
+export function getMcpTools(agentId: string) {
+ return http.Get(`/agent/mcp/tools/${agentId}`, {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+// 获取声纹列表
+export function getVoicePrintList(agentId: string) {
+ return http.Get(`/agent/voice-print/list/${agentId}`, {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+// 获取语音对话记录
+export function getChatHistoryUser(agentId: string) {
+ return http.Get(`/agent/${agentId}/chat-history/user`, {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+// 新增声纹说话人
+export function createVoicePrint(data: { agentId: string, audioId: string, sourceName: string, introduce: string }) {
+ return http.Post('/agent/voice-print', data, {
+ meta: {
+ ignoreAuth: false,
+ toast: true,
+ },
+ })
+}
diff --git a/main/xiaozhi-mobile-admin/src/api/agent/types.ts b/main/xiaozhi-mobile-admin/src/api/agent/types.ts
new file mode 100644
index 00000000..66559a23
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/api/agent/types.ts
@@ -0,0 +1,107 @@
+// 智能体列表数据类型
+export interface Agent {
+ id: string
+ agentName: string
+ ttsModelName: string
+ ttsVoiceName: string
+ llmModelName: string
+ vllmModelName: string
+ memModelId: string
+ systemPrompt: string
+ summaryMemory: string | null
+ lastConnectedAt: string | null
+ deviceCount: number
+}
+
+// 智能体创建数据类型
+export interface AgentCreateData {
+ agentName: string
+}
+
+// 智能体详情数据类型
+export interface AgentDetail {
+ id: string
+ userId: string
+ agentCode: string
+ agentName: string
+ asrModelId: string
+ vadModelId: string
+ llmModelId: string
+ vllmModelId: string
+ ttsModelId: string
+ ttsVoiceId: string
+ memModelId: string
+ intentModelId: string
+ chatHistoryConf: number
+ systemPrompt: string
+ summaryMemory: string
+ langCode: string
+ language: string
+ sort: number
+ creator: string
+ createdAt: string
+ updater: string
+ updatedAt: string
+ functions: AgentFunction[]
+}
+
+export interface AgentFunction {
+ id?: string
+ agentId?: string
+ pluginId: string
+ paramInfo: Record | null
+}
+
+// 角色模板数据类型
+export interface RoleTemplate {
+ id: string
+ agentCode: string
+ agentName: string
+ asrModelId: string
+ vadModelId: string
+ llmModelId: string
+ vllmModelId: string
+ ttsModelId: string
+ ttsVoiceId: string
+ memModelId: string
+ intentModelId: string
+ chatHistoryConf: number
+ systemPrompt: string
+ summaryMemory: string
+ langCode: string
+ language: string
+ sort: number
+ creator: string
+ createdAt: string
+ updater: string
+ updatedAt: string
+}
+
+// 模型选项数据类型
+export interface ModelOption {
+ id: string
+ modelName: string
+}
+
+export interface PluginField {
+ key: string
+ type: string
+ label: string
+ default: string
+ selected?: boolean
+ editing?: boolean
+}
+
+export interface PluginDefinition {
+ id: string
+ modelType: string
+ providerCode: string
+ name: string
+ fields: PluginField[] // 注意:原始是字符串,需要先 JSON.parse
+ sort: number
+ updater: string
+ updateDate: string
+ creator: string
+ createDate: string
+ [key: string]: any
+}
diff --git a/main/xiaozhi-mobile-admin/src/api/auth.ts b/main/xiaozhi-mobile-admin/src/api/auth.ts
new file mode 100644
index 00000000..07cc3fa8
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/api/auth.ts
@@ -0,0 +1,127 @@
+import { http } from '@/http/request/alova'
+
+// 登录接口数据类型
+export interface LoginData {
+ username: string
+ password: string
+ captcha: string
+ captchaId: string
+ areaCode?: string
+ mobile?: string
+}
+
+// 登录响应数据类型
+export interface LoginResponse {
+ token: string
+ expire: number
+ clientHash: string
+}
+
+// 验证码响应数据类型
+export interface CaptchaResponse {
+ captchaId: string
+ captchaImage: string
+}
+
+// 获取验证码
+export function getCaptcha(uuid: string) {
+ return http.Get('/user/captcha', {
+ params: { uuid },
+ meta: {
+ ignoreAuth: true,
+ toast: false,
+ },
+ })
+}
+
+// 用户登录
+export function login(data: LoginData) {
+ return http.Post('/user/login', data, {
+ meta: {
+ ignoreAuth: true,
+ toast: true,
+ },
+ })
+}
+
+// 用户信息响应数据类型
+export interface UserInfo {
+ id: number
+ username: string
+ realName: string
+ email: string
+ mobile: string
+ status: number
+ superAdmin: number
+}
+
+// 公共配置响应数据类型
+export interface PublicConfig {
+ enableMobileRegister: boolean
+ version: string
+ year: string
+ allowUserRegister: boolean
+ mobileAreaList: Array<{
+ name: string
+ key: string
+ }>
+ beianIcpNum: string
+ beianGaNum: string
+ name: string
+}
+
+// 获取用户信息
+export function getUserInfo() {
+ return http.Get('/user/info', {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ })
+}
+
+// 获取公共配置
+export function getPublicConfig() {
+ return http.Get('/user/pub-config', {
+ meta: {
+ ignoreAuth: true,
+ toast: false,
+ },
+ })
+}
+
+// 注册数据类型
+export interface RegisterData {
+ username: string
+ password: string
+ confirmPassword: string
+ captcha: string
+ captchaId: string
+ areaCode: string
+ mobile: string
+ mobileCaptcha: string
+}
+
+// 发送短信验证码
+export function sendSmsCode(data: {
+ phone: string
+ captcha: string
+ captchaId: string
+}) {
+ return http.Post('/user/smsVerification', data, {
+ meta: {
+ ignoreAuth: true,
+ toast: false,
+ },
+ })
+}
+
+// 用户注册
+export function register(data: RegisterData) {
+ return http.Post('/user/register', data, {
+ meta: {
+ ignoreAuth: true,
+ toast: true,
+ },
+ })
+}
diff --git a/main/xiaozhi-mobile-admin/src/api/chat-history/chat-history.ts b/main/xiaozhi-mobile-admin/src/api/chat-history/chat-history.ts
new file mode 100644
index 00000000..8d6e396b
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/api/chat-history/chat-history.ts
@@ -0,0 +1,57 @@
+import type {
+ ChatMessage,
+ ChatSessionsResponse,
+ GetSessionsParams,
+} from './types'
+import { http } from '@/http/request/alova'
+
+/**
+ * 获取聊天会话列表
+ * @param agentId 智能体ID
+ * @param params 分页参数
+ */
+export function getChatSessions(agentId: string, params: GetSessionsParams) {
+ return http.Get(`/agent/${agentId}/sessions`, {
+ params,
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ })
+}
+
+/**
+ * 获取聊天记录详情
+ * @param agentId 智能体ID
+ * @param sessionId 会话ID
+ */
+export function getChatHistory(agentId: string, sessionId: string) {
+ return http.Get(`/agent/${agentId}/chat-history/${sessionId}`, {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ })
+}
+
+/**
+ * 获取音频下载ID
+ * @param audioId 音频ID
+ */
+export function getAudioId(audioId: string) {
+ return http.Post(`/agent/audio/${audioId}`, {}, {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ })
+}
+
+/**
+ * 获取音频播放地址
+ * @param downloadId 下载ID
+ */
+export function getAudioPlayUrl(downloadId: string) {
+ // 根据需求文档,这个是直接返回二进制的,所以我们直接构造URL
+ return `/agent/play/${downloadId}`
+}
diff --git a/main/xiaozhi-mobile-admin/src/api/chat-history/index.ts b/main/xiaozhi-mobile-admin/src/api/chat-history/index.ts
new file mode 100644
index 00000000..ecf782a4
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/api/chat-history/index.ts
@@ -0,0 +1,2 @@
+export * from './chat-history'
+export * from './types'
diff --git a/main/xiaozhi-mobile-admin/src/api/chat-history/types.ts b/main/xiaozhi-mobile-admin/src/api/chat-history/types.ts
new file mode 100644
index 00000000..b895ed47
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/api/chat-history/types.ts
@@ -0,0 +1,38 @@
+// 聊天会话列表项
+export interface ChatSession {
+ sessionId: string
+ createdAt: string
+ chatCount: number
+}
+
+// 聊天会话列表响应
+export interface ChatSessionsResponse {
+ total: number
+ list: ChatSession[]
+}
+
+// 聊天消息
+export interface ChatMessage {
+ createdAt: string
+ chatType: 1 | 2 // 1是用户,2是AI
+ content: string
+ audioId: string | null
+ macAddress: string
+}
+
+// 用户消息内容(需要解析JSON)
+export interface UserMessageContent {
+ speaker: string
+ content: string
+}
+
+// 获取聊天会话列表参数
+export interface GetSessionsParams {
+ page: number
+ limit: number
+}
+
+// 音频播放相关
+export interface AudioResponse {
+ data: string // 音频下载ID
+}
diff --git a/main/xiaozhi-mobile-admin/src/api/device/device.ts b/main/xiaozhi-mobile-admin/src/api/device/device.ts
new file mode 100644
index 00000000..fc9d5f69
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/api/device/device.ts
@@ -0,0 +1,55 @@
+import type { Device, FirmwareType } from './types'
+import { http } from '@/http/request/alova'
+
+/**
+ * 获取设备类型列表
+ */
+export function getFirmwareTypes() {
+ return http.Get('/admin/dict/data/type/FIRMWARE_TYPE')
+}
+
+/**
+ * 获取绑定设备列表
+ * @param agentId 智能体ID
+ */
+export function getBindDevices(agentId: string) {
+ return http.Get(`/device/bind/${agentId}`, {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+/**
+ * 添加设备
+ * @param agentId 智能体ID
+ * @param code 验证码
+ */
+export function bindDevice(agentId: string, code: string) {
+ return http.Post(`/device/bind/${agentId}/${code}`, null)
+}
+
+/**
+ * 设置设备OTA升级开关
+ * @param deviceId 设备ID (MAC地址)
+ * @param autoUpdate 是否自动升级 0|1
+ */
+export function updateDeviceAutoUpdate(deviceId: string, autoUpdate: number) {
+ return http.Put(`/device/update/${deviceId}`, {
+ autoUpdate,
+ })
+}
+
+/**
+ * 解绑设备
+ * @param deviceId 设备ID (MAC地址)
+ */
+export function unbindDevice(deviceId: string) {
+ return http.Post('/device/unbind', {
+ deviceId,
+ })
+}
diff --git a/main/xiaozhi-mobile-admin/src/api/device/index.ts b/main/xiaozhi-mobile-admin/src/api/device/index.ts
new file mode 100644
index 00000000..aa5f6179
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/api/device/index.ts
@@ -0,0 +1,2 @@
+export * from './device'
+export * from './types'
diff --git a/main/xiaozhi-mobile-admin/src/api/device/types.ts b/main/xiaozhi-mobile-admin/src/api/device/types.ts
new file mode 100644
index 00000000..4b6165c5
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/api/device/types.ts
@@ -0,0 +1,21 @@
+export interface FirmwareType {
+ name: string
+ key: string
+}
+
+export interface Device {
+ id: string
+ userId: string
+ macAddress: string
+ lastConnectedAt: string
+ autoUpdate: number
+ board: string
+ alias?: string
+ agentId: string
+ appVersion: string
+ sort: number
+ updater?: string
+ updateDate: string
+ creator: string
+ createDate: string
+}
diff --git a/main/xiaozhi-mobile-admin/src/api/voiceprint/index.ts b/main/xiaozhi-mobile-admin/src/api/voiceprint/index.ts
new file mode 100644
index 00000000..af56e69a
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/api/voiceprint/index.ts
@@ -0,0 +1,2 @@
+export * from './types'
+export * from './voiceprint'
diff --git a/main/xiaozhi-mobile-admin/src/api/voiceprint/types.ts b/main/xiaozhi-mobile-admin/src/api/voiceprint/types.ts
new file mode 100644
index 00000000..726b1783
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/api/voiceprint/types.ts
@@ -0,0 +1,29 @@
+// 声纹信息响应类型
+export interface VoicePrint {
+ id: string
+ audioId: string
+ sourceName: string
+ introduce: string
+ createDate: string
+}
+
+// 语音对话记录类型
+export interface ChatHistory {
+ content: string
+ audioId: string
+}
+
+// 创建说话人数据类型
+export interface CreateSpeakerData {
+ agentId: string
+ audioId: string
+ sourceName: string
+ introduce: string
+}
+
+// 通用响应类型
+export interface ApiResponse {
+ code: number
+ msg: string
+ data: T
+}
diff --git a/main/xiaozhi-mobile-admin/src/api/voiceprint/voiceprint.ts b/main/xiaozhi-mobile-admin/src/api/voiceprint/voiceprint.ts
new file mode 100644
index 00000000..600696dd
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/api/voiceprint/voiceprint.ts
@@ -0,0 +1,62 @@
+import type {
+ ChatHistory,
+ CreateSpeakerData,
+ VoicePrint,
+} from './types'
+import { http } from '@/http/request/alova'
+
+// 获取声纹列表
+export function getVoicePrintList(agentId: string) {
+ return http.Get(`/agent/voice-print/list/${agentId}`, {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+// 获取语音对话记录(用于选择声纹向量)
+export function getChatHistory(agentId: string) {
+ return http.Get(`/agent/${agentId}/chat-history/user`, {
+ meta: {
+ ignoreAuth: false,
+ toast: false,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+// 新增说话人
+export function createVoicePrint(data: CreateSpeakerData) {
+ return http.Post('/agent/voice-print', data, {
+ meta: {
+ ignoreAuth: false,
+ toast: true,
+ },
+ })
+}
+
+// 删除声纹
+export function deleteVoicePrint(id: string) {
+ return http.Delete(`/agent/voice-print/${id}`, {
+ meta: {
+ ignoreAuth: false,
+ toast: true,
+ },
+ })
+}
+
+// 更新声纹信息
+export function updateVoicePrint(data: VoicePrint) {
+ return http.Put('/agent/voice-print', data, {
+ meta: {
+ ignoreAuth: false,
+ toast: true,
+ },
+ })
+}
diff --git a/main/xiaozhi-mobile-admin/src/components/.gitkeep b/main/xiaozhi-mobile-admin/src/components/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/main/xiaozhi-mobile-admin/src/env.d.ts b/main/xiaozhi-mobile-admin/src/env.d.ts
new file mode 100644
index 00000000..b4a2c97b
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/env.d.ts
@@ -0,0 +1,34 @@
+///
+///
+
+declare module '*.vue' {
+ import type { DefineComponent } from 'vue'
+
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
+
+interface ImportMetaEnv {
+ /** 网站标题,应用名称 */
+ readonly VITE_APP_TITLE: string
+ /** 服务端口号 */
+ readonly VITE_SERVER_PORT: string
+ /** 后台接口地址 */
+ readonly VITE_SERVER_BASEURL: string
+ /** H5是否需要代理 */
+ readonly VITE_APP_PROXY: 'true' | 'false'
+ /** H5是否需要代理,需要的话有个前缀 */
+ readonly VITE_APP_PROXY_PREFIX: string // 一般是/api
+ /** 上传图片地址 */
+ readonly VITE_UPLOAD_BASEURL: string
+ /** 是否清除console */
+ readonly VITE_DELETE_CONSOLE: string
+ // 更多环境变量...
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}
+
+declare const __VITE_APP_PROXY__: 'true' | 'false'
+declare const __UNI_PLATFORM__: 'app' | 'h5' | 'mp-alipay' | 'mp-baidu' | 'mp-kuaishou' | 'mp-lark' | 'mp-qq' | 'mp-tiktok' | 'mp-weixin' | 'mp-xiaochengxu'
diff --git a/main/xiaozhi-mobile-admin/src/hooks/.gitkeep b/main/xiaozhi-mobile-admin/src/hooks/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/main/xiaozhi-mobile-admin/src/hooks/usePageAuth.ts b/main/xiaozhi-mobile-admin/src/hooks/usePageAuth.ts
new file mode 100644
index 00000000..fd006c81
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/hooks/usePageAuth.ts
@@ -0,0 +1,50 @@
+import { onLoad } from '@dcloudio/uni-app'
+import { useUserStore } from '@/store'
+import { needLoginPages as _needLoginPages, getNeedLoginPages } from '@/utils'
+
+const loginRoute = import.meta.env.VITE_LOGIN_URL
+const isDev = import.meta.env.DEV
+function isLogined() {
+ const userStore = useUserStore()
+ return !!userStore.userInfo.username
+}
+// 检查当前页面是否需要登录
+export function usePageAuth() {
+ onLoad((options) => {
+ // 获取当前页面路径
+ const pages = getCurrentPages()
+ const currentPage = pages[pages.length - 1]
+ const currentPath = `/${currentPage.route}`
+
+ // 获取需要登录的页面列表
+ let needLoginPages: string[] = []
+ if (isDev) {
+ needLoginPages = getNeedLoginPages()
+ }
+ else {
+ needLoginPages = _needLoginPages
+ }
+
+ // 检查当前页面是否需要登录
+ const isNeedLogin = needLoginPages.includes(currentPath)
+ if (!isNeedLogin) {
+ return
+ }
+
+ const hasLogin = isLogined()
+ if (hasLogin) {
+ return true
+ }
+
+ // 构建重定向URL
+ const queryString = Object.entries(options || {})
+ .map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`)
+ .join('&')
+
+ const currentFullPath = queryString ? `${currentPath}?${queryString}` : currentPath
+ const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(currentFullPath)}`
+
+ // 重定向到登录页
+ uni.redirectTo({ url: redirectRoute })
+ })
+}
diff --git a/main/xiaozhi-mobile-admin/src/hooks/useRequest.ts b/main/xiaozhi-mobile-admin/src/hooks/useRequest.ts
new file mode 100644
index 00000000..017a710a
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/hooks/useRequest.ts
@@ -0,0 +1,51 @@
+import type { Ref } from 'vue'
+
+interface IUseRequestOptions {
+ /** 是否立即执行 */
+ immediate?: boolean
+ /** 初始化数据 */
+ initialData?: T
+}
+
+interface IUseRequestReturn {
+ loading: Ref
+ error: Ref
+ data: Ref
+ run: () => Promise
+}
+
+/**
+ * useRequest是一个定制化的请求钩子,用于处理异步请求和响应。
+ * @param func 一个执行异步请求的函数,返回一个包含响应数据的Promise。
+ * @param options 包含请求选项的对象 {immediate, initialData}。
+ * @param options.immediate 是否立即执行请求,默认为false。
+ * @param options.initialData 初始化数据,默认为undefined。
+ * @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
+ */
+export default function useRequest(
+ func: () => Promise>,
+ options: IUseRequestOptions = { immediate: false },
+): IUseRequestReturn {
+ const loading = ref(false)
+ const error = ref(false)
+ const data = ref(options.initialData) as Ref
+ const run = async () => {
+ loading.value = true
+ return func()
+ .then((res) => {
+ data.value = res.data
+ error.value = false
+ return data.value
+ })
+ .catch((err) => {
+ error.value = err
+ throw err
+ })
+ .finally(() => {
+ loading.value = false
+ })
+ }
+
+ options.immediate && run()
+ return { loading, error, data, run }
+}
diff --git a/main/xiaozhi-mobile-admin/src/hooks/useUpload.ts b/main/xiaozhi-mobile-admin/src/hooks/useUpload.ts
new file mode 100644
index 00000000..3080d5a7
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/hooks/useUpload.ts
@@ -0,0 +1,160 @@
+import { ref } from 'vue'
+import { getEnvBaseUploadUrl } from '@/utils'
+
+const VITE_UPLOAD_BASEURL = `${getEnvBaseUploadUrl()}`
+
+type TfileType = 'image' | 'file'
+type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*'
+type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage
+
+interface TOptions {
+ formData?: Record
+ maxSize?: number
+ accept?: T extends 'image' ? TImage[] : TFile[]
+ fileType?: T
+ success?: (params: any) => void
+ error?: (err: any) => void
+}
+
+export default function useUpload(options: TOptions = {} as TOptions) {
+ const {
+ formData = {},
+ maxSize = 5 * 1024 * 1024,
+ accept = ['*'],
+ fileType = 'image',
+ success,
+ error: onError,
+ } = options
+
+ const loading = ref(false)
+ const error = ref(null)
+ const data = ref(null)
+
+ const handleFileChoose = ({ tempFilePath, size }: { tempFilePath: string, size: number }) => {
+ if (size > maxSize) {
+ uni.showToast({
+ title: `文件大小不能超过 ${maxSize / 1024 / 1024}MB`,
+ icon: 'none',
+ })
+ return
+ }
+
+ // const fileExtension = file?.tempFiles?.name?.split('.').pop()?.toLowerCase()
+ // const isTypeValid = accept.some((type) => type === '*' || type.toLowerCase() === fileExtension)
+
+ // if (!isTypeValid) {
+ // uni.showToast({
+ // title: `仅支持 ${accept.join(', ')} 格式的文件`,
+ // icon: 'none',
+ // })
+ // return
+ // }
+
+ loading.value = true
+ uploadFile({
+ tempFilePath,
+ formData,
+ onSuccess: (res) => {
+ const { data: _data } = JSON.parse(res)
+ data.value = _data
+ // console.log('上传成功', res)
+ success?.(_data)
+ },
+ onError: (err) => {
+ error.value = err
+ onError?.(err)
+ },
+ onComplete: () => {
+ loading.value = false
+ },
+ })
+ }
+
+ const run = () => {
+ // 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
+ // 微信小程序在2023年10月17日之后,使用本API需要配置隐私协议
+ const chooseFileOptions = {
+ count: 1,
+ success: (res: any) => {
+ console.log('File selected successfully:', res)
+ // 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]}
+ // h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]}
+ // h5的File有以下字段:{name: "girl.jpeg", size: 48976, type: "image/jpeg"}
+ // App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]}
+ // App的File有以下字段:{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976}
+ let tempFilePath = ''
+ let size = 0
+ // #ifdef MP-WEIXIN
+ tempFilePath = res.tempFiles[0].tempFilePath
+ size = res.tempFiles[0].size
+ // #endif
+ // #ifndef MP-WEIXIN
+ tempFilePath = res.tempFilePaths[0]
+ size = res.tempFiles[0].size
+ // #endif
+ handleFileChoose({ tempFilePath, size })
+ },
+ fail: (err: any) => {
+ console.error('File selection failed:', err)
+ error.value = err
+ onError?.(err)
+ },
+ }
+
+ if (fileType === 'image') {
+ // #ifdef MP-WEIXIN
+ uni.chooseMedia({
+ ...chooseFileOptions,
+ mediaType: ['image'],
+ })
+ // #endif
+
+ // #ifndef MP-WEIXIN
+ uni.chooseImage(chooseFileOptions)
+ // #endif
+ }
+ else {
+ uni.chooseFile({
+ ...chooseFileOptions,
+ type: 'all',
+ })
+ }
+ }
+
+ return { loading, error, data, run }
+}
+
+async function uploadFile({
+ tempFilePath,
+ formData,
+ onSuccess,
+ onError,
+ onComplete,
+}: {
+ tempFilePath: string
+ formData: Record
+ onSuccess: (data: any) => void
+ onError: (err: any) => void
+ onComplete: () => void
+}) {
+ uni.uploadFile({
+ url: VITE_UPLOAD_BASEURL,
+ filePath: tempFilePath,
+ name: 'file',
+ formData,
+ success: (uploadFileRes) => {
+ try {
+ const data = uploadFileRes.data
+ onSuccess(data)
+ }
+ catch (err) {
+ onError(err)
+ }
+ },
+ fail: (err) => {
+ console.error('Upload failed:', err)
+ onError(err)
+ },
+ complete: onComplete,
+ })
+}
diff --git a/main/xiaozhi-mobile-admin/src/http/README.md b/main/xiaozhi-mobile-admin/src/http/README.md
new file mode 100644
index 00000000..fd34fd52
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/http/README.md
@@ -0,0 +1,36 @@
+# 请求库
+
+当前项目使用 Alova 作为唯一的 HTTP 请求库:
+
+## 使用方式
+
+- **Alova HTTP**:路径(src/http/request/alova.ts)
+- **示例代码**:src/api/foo-alova.ts 和 src/api/foo.ts
+- **API文档**:https://alova.js.org/
+
+## 配置说明
+
+Alova 实例已配置:
+- 自动 Token 认证和刷新
+- 统一错误处理和提示
+- 支持动态域名切换
+- 内置请求/响应拦截器
+
+## 使用示例
+
+```typescript
+import { http } from '@/http/request/alova'
+
+// GET 请求
+http.Get('/api/path', {
+ params: { id: 1 },
+ headers: { 'Custom-Header': 'value' },
+ meta: { toast: false } // 关闭错误提示
+})
+
+// POST 请求
+http.Post('/api/path', data, {
+ params: { query: 'param' },
+ headers: { 'Content-Type': 'application/json' }
+})
+```
\ No newline at end of file
diff --git a/main/xiaozhi-mobile-admin/src/http/request/alova.ts b/main/xiaozhi-mobile-admin/src/http/request/alova.ts
new file mode 100644
index 00000000..d970a4c3
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/http/request/alova.ts
@@ -0,0 +1,119 @@
+import type { uniappRequestAdapter } from '@alova/adapter-uniapp'
+import type { IResponse } from './types'
+import AdapterUniapp from '@alova/adapter-uniapp'
+import { createAlova } from 'alova'
+import { createServerTokenAuthentication } from 'alova/client'
+import VueHook from 'alova/vue'
+import { toast } from '@/utils/toast'
+import { ContentTypeEnum, ResultEnum, ShowMessage } from './enum'
+
+/**
+ * 创建请求实例
+ */
+const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<
+ typeof VueHook,
+ typeof uniappRequestAdapter
+>({
+ refreshTokenOnError: {
+ isExpired: (error) => {
+ return error.response?.status === ResultEnum.Unauthorized
+ },
+ handler: async () => {
+ try {
+ // await authLogin();
+ }
+ catch (error) {
+ // 切换到登录页
+ await uni.reLaunch({ url: '/pages/login/index' })
+ throw error
+ }
+ },
+ },
+})
+
+/**
+ * alova 请求实例
+ */
+const alovaInstance = createAlova({
+ baseURL: import.meta.env.VITE_SERVER_BASEURL,
+ ...AdapterUniapp(),
+ timeout: 5000,
+ statesHook: VueHook,
+
+ beforeRequest: onAuthRequired((method) => {
+ // 设置默认 Content-Type
+ method.config.headers = {
+ 'Content-Type': ContentTypeEnum.JSON,
+ 'Accept': 'application/json, text/plain, */*',
+ ...method.config.headers,
+ }
+
+ const { config } = method
+ const ignoreAuth = config.meta?.ignoreAuth
+ console.log('ignoreAuth===>', ignoreAuth)
+
+ // 处理认证信息
+ if (!ignoreAuth) {
+ const token = uni.getStorageSync('token')
+ if (!token) {
+ // 跳转到登录页
+ uni.reLaunch({ url: '/pages/login/index' })
+ throw new Error('[请求错误]:未登录')
+ }
+ // 添加 Authorization 头
+ method.config.headers.Authorization = `Bearer ${token}`
+ }
+
+ // 处理动态域名
+ if (config.meta?.domain) {
+ method.baseURL = config.meta.domain
+ console.log('当前域名', method.baseURL)
+ }
+ }),
+
+ responded: onResponseRefreshToken((response, method) => {
+ const { config } = method
+ const { requestType } = config
+ const {
+ statusCode,
+ data: rawData,
+ errMsg,
+ } = response as UniNamespace.RequestSuccessCallbackResult
+
+ console.log(response)
+
+ // 处理特殊请求类型(上传/下载)
+ if (requestType === 'upload' || requestType === 'download') {
+ return response
+ }
+
+ // 处理 HTTP 状态码错误
+ if (statusCode !== 200) {
+ const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
+ console.error('errorMessage===>', errorMessage)
+ toast.error(errorMessage)
+ throw new Error(`${errorMessage}:${errMsg}`)
+ }
+
+ // 处理业务逻辑错误
+ const { code, msg, data } = rawData as IResponse
+ if (code !== ResultEnum.Success) {
+ // 检查是否为token失效
+ if (code === ResultEnum.Unauthorized) {
+ // 清除token并跳转到登录页
+ uni.removeStorageSync('token')
+ uni.reLaunch({ url: '/pages/login/index' })
+ throw new Error(`请求错误[${code}]:${msg}`)
+ }
+
+ if (config.meta?.toast !== false) {
+ toast.warning(msg)
+ }
+ throw new Error(`请求错误[${code}]:${msg}`)
+ }
+ // 处理成功响应,返回业务数据
+ return data
+ }),
+})
+
+export const http = alovaInstance
diff --git a/main/xiaozhi-mobile-admin/src/http/request/enum.ts b/main/xiaozhi-mobile-admin/src/http/request/enum.ts
new file mode 100644
index 00000000..1868fe08
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/http/request/enum.ts
@@ -0,0 +1,66 @@
+export enum ResultEnum {
+ Success = 0, // 成功
+ Error = 400, // 错误
+ Unauthorized = 401, // 未授权
+ Forbidden = 403, // 禁止访问(原为forbidden)
+ NotFound = 404, // 未找到(原为notFound)
+ MethodNotAllowed = 405, // 方法不允许(原为methodNotAllowed)
+ RequestTimeout = 408, // 请求超时(原为requestTimeout)
+ InternalServerError = 500, // 服务器错误(原为internalServerError)
+ NotImplemented = 501, // 未实现(原为notImplemented)
+ BadGateway = 502, // 网关错误(原为badGateway)
+ ServiceUnavailable = 503, // 服务不可用(原为serviceUnavailable)
+ GatewayTimeout = 504, // 网关超时(原为gatewayTimeout)
+ HttpVersionNotSupported = 505, // HTTP版本不支持(原为httpVersionNotSupported)
+}
+export enum ContentTypeEnum {
+ JSON = 'application/json;charset=UTF-8',
+ FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
+ FORM_DATA = 'multipart/form-data;charset=UTF-8',
+}
+/**
+ * 根据状态码,生成对应的错误信息
+ * @param {number|string} status 状态码
+ * @returns {string} 错误信息
+ */
+export function ShowMessage(status: number | string): string {
+ let message: string
+ switch (status) {
+ case 400:
+ message = '请求错误(400)'
+ break
+ case 401:
+ message = '未授权,请重新登录(401)'
+ break
+ case 403:
+ message = '拒绝访问(403)'
+ break
+ case 404:
+ message = '请求出错(404)'
+ break
+ case 408:
+ message = '请求超时(408)'
+ break
+ case 500:
+ message = '服务器错误(500)'
+ break
+ case 501:
+ message = '服务未实现(501)'
+ break
+ case 502:
+ message = '网络错误(502)'
+ break
+ case 503:
+ message = '服务不可用(503)'
+ break
+ case 504:
+ message = '网络超时(504)'
+ break
+ case 505:
+ message = 'HTTP版本不受支持(505)'
+ break
+ default:
+ message = `连接出错(${status})!`
+ }
+ return `${message},请检查网络或联系管理员!`
+}
diff --git a/main/xiaozhi-mobile-admin/src/http/request/types.ts b/main/xiaozhi-mobile-admin/src/http/request/types.ts
new file mode 100644
index 00000000..824aa4f4
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/http/request/types.ts
@@ -0,0 +1,22 @@
+// 通用响应格式
+export interface IResponse {
+ code: number | string
+ data: T
+ msg: string
+ status: string | number
+}
+
+// 分页请求参数
+export interface PageParams {
+ page: number
+ pageSize: number
+ [key: string]: any
+}
+
+// 分页响应数据
+export interface PageResult {
+ list: T[]
+ total: number
+ page: number
+ pageSize: number
+}
diff --git a/main/xiaozhi-mobile-admin/src/layouts/default.vue b/main/xiaozhi-mobile-admin/src/layouts/default.vue
new file mode 100644
index 00000000..360c6b21
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/layouts/default.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/fg-tabbar.vue b/main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/fg-tabbar.vue
new file mode 100644
index 00000000..819aa6fb
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/fg-tabbar.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/tabbar.md b/main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/tabbar.md
new file mode 100644
index 00000000..2485b06d
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/tabbar.md
@@ -0,0 +1,17 @@
+# tabbar 说明
+
+`tabbar` 分为 `4 种` 情况:
+
+- 0 `无 tabbar`,只有一个页面入口,底部无 `tabbar` 显示;常用语临时活动页。
+- 1 `原生 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。
+ - 优势:原生自带的 tabbar,最先渲染,有缓存。
+ - 劣势:只能使用 2 组图片来切换选中和非选中状态,修改颜色只能重新换图片(或者用 iconfont)。
+- 2 `有缓存自定义 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。使用了第三方 UI 库的 `tabbar` 组件,并隐藏了原生 `tabbar` 的显示。
+ - 优势:可以随意配置自己想要的 `svg icon`,切换字体颜色方便。有缓存。可以实现各种花里胡哨的动效等。
+ - 劣势:首次点击 tababr 会闪烁。
+- 3 `无缓存自定义 tabbar`,使用 `navigateTo` 切换 `tabbar`,`tabbar` 页面无缓存。使用了第三方 UI 库的 `tabbar` 组件。
+ - 优势:可以随意配置自己想要的 svg icon,切换字体颜色方便。可以实现各种花里胡哨的动效等。
+ - 劣势:首次点击 `tababr` 会闪烁,无缓存。
+
+
+> 注意:花里胡哨的效果需要自己实现,本模版不提供。
diff --git a/main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/tabbar.ts b/main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/tabbar.ts
new file mode 100644
index 00000000..03be03fb
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/tabbar.ts
@@ -0,0 +1,11 @@
+/**
+ * tabbar 状态,增加 storageSync 保证刷新浏览器时在正确的 tabbar 页面
+ * 使用reactive简单状态,而不是 pinia 全局状态
+ */
+export const tabbarStore = reactive({
+ curIdx: uni.getStorageSync('app-tabbar-index') || 0,
+ setCurIdx(idx: number) {
+ this.curIdx = idx
+ uni.setStorageSync('app-tabbar-index', idx)
+ },
+})
diff --git a/main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/tabbarList.ts b/main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/tabbarList.ts
new file mode 100644
index 00000000..69fbfec0
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/layouts/fg-tabbar/tabbarList.ts
@@ -0,0 +1,97 @@
+import type { TabBar } from '@uni-helper/vite-plugin-uni-pages'
+
+type FgTabBarItem = TabBar['list'][0] & {
+ icon: string
+ iconType: 'uiLib' | 'unocss' | 'iconfont'
+}
+
+/**
+ * tabbar 选择的策略,更详细的介绍见 tabbar.md 文件
+ * 0: 'NO_TABBAR' `无 tabbar`
+ * 1: 'NATIVE_TABBAR' `完全原生 tabbar`
+ * 2: 'CUSTOM_TABBAR_WITH_CACHE' `有缓存自定义 tabbar`
+ * 3: 'CUSTOM_TABBAR_WITHOUT_CACHE' `无缓存自定义 tabbar`
+ *
+ * 温馨提示:本文件的任何代码更改了之后,都需要重新运行,否则 pages.json 不会更新导致错误
+ */
+export const TABBAR_MAP = {
+ NO_TABBAR: 0,
+ NATIVE_TABBAR: 1,
+ CUSTOM_TABBAR_WITH_CACHE: 2,
+ CUSTOM_TABBAR_WITHOUT_CACHE: 3,
+}
+// TODO:通过这里切换使用tabbar的策略
+export const selectedTabbarStrategy = TABBAR_MAP.NATIVE_TABBAR
+
+// selectedTabbarStrategy==NATIVE_TABBAR(1) 时,需要填 iconPath 和 selectedIconPath
+// selectedTabbarStrategy==CUSTOM_TABBAR(2,3) 时,需要填 icon 和 iconType
+// selectedTabbarStrategy==NO_TABBAR(0) 时,tabbarList 不生效
+export const tabbarList: FgTabBarItem[] = [
+ {
+ iconPath: 'static/tabbar/robot.png',
+ selectedIconPath: 'static/tabbar/robot_activate.png',
+ pagePath: 'pages/index/index',
+ text: '首页',
+ icon: 'home',
+ // 选用 UI 框架自带的 icon 时,iconType 为 uiLib
+ iconType: 'uiLib',
+ },
+ {
+ iconPath: 'static/tabbar/device.png',
+ selectedIconPath: 'static/tabbar/device_activate.png',
+ pagePath: 'pages/device/index',
+ text: '设备管理',
+ icon: 'i-carbon-laptop',
+ // 注意 unocss 的图标需要在 页面上引入一下,或者配置到 unocss.config.ts 的 safelist 中
+ iconType: 'uiLib',
+ },
+ {
+ iconPath: 'static/tabbar/microphone.png',
+ selectedIconPath: 'static/tabbar/microphone_activate.png',
+ pagePath: 'pages/voiceprint/index',
+ text: '声纹识别',
+ icon: 'i-carbon-microphone-filled',
+ iconType: 'uiLib',
+ },
+ {
+ iconPath: 'static/tabbar/chat.png',
+ selectedIconPath: 'static/tabbar/chat_activate.png',
+ pagePath: 'pages/chat-history/index',
+ text: '聊天记录',
+ icon: 'i-carbon-chat-bot',
+ iconType: 'uiLib',
+ },
+ // {
+ // pagePath: 'pages/my/index',
+ // text: '我的',
+ // icon: '/static/logo.svg',
+ // iconType: 'local',
+ // },
+ // {
+ // pagePath: 'pages/mine/index',
+ // text: '我的',
+ // icon: 'iconfont icon-my',
+ // iconType: 'iconfont',
+ // },
+]
+
+// NATIVE_TABBAR(1) 和 CUSTOM_TABBAR_WITH_CACHE(2) 时,需要tabbar缓存
+export const cacheTabbarEnable = selectedTabbarStrategy === TABBAR_MAP.NATIVE_TABBAR
+ || selectedTabbarStrategy === TABBAR_MAP.CUSTOM_TABBAR_WITH_CACHE
+
+const _tabbar: TabBar = {
+ // 只有微信小程序支持 custom。App 和 H5 不生效
+ custom: selectedTabbarStrategy === TABBAR_MAP.CUSTOM_TABBAR_WITH_CACHE,
+ color: '#e6e6e6',
+ selectedColor: '#667dea',
+ backgroundColor: '#fff',
+ borderStyle: 'black',
+ height: '50px',
+ fontSize: '10px',
+ iconWidth: '24px',
+ spacing: '3px',
+ list: tabbarList as unknown as TabBar['list'],
+}
+
+// 0和1 需要显示底部的tabbar的各种配置,以利用缓存
+export const tabBar = cacheTabbarEnable ? _tabbar : undefined
diff --git a/main/xiaozhi-mobile-admin/src/layouts/tabbar.vue b/main/xiaozhi-mobile-admin/src/layouts/tabbar.vue
new file mode 100644
index 00000000..0c1bb1c6
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/layouts/tabbar.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/main.ts b/main/xiaozhi-mobile-admin/src/main.ts
new file mode 100644
index 00000000..ab429500
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/main.ts
@@ -0,0 +1,19 @@
+import { VueQueryPlugin } from '@tanstack/vue-query'
+import { createSSRApp } from 'vue'
+import App from './App.vue'
+import { routeInterceptor } from './router/interceptor'
+
+import store from './store'
+import '@/style/index.scss'
+import 'virtual:uno.css'
+
+export function createApp() {
+ const app = createSSRApp(App)
+ app.use(store)
+ app.use(routeInterceptor)
+ app.use(VueQueryPlugin)
+
+ return {
+ app,
+ }
+}
diff --git a/main/xiaozhi-mobile-admin/src/pages-sub/demo/index.vue b/main/xiaozhi-mobile-admin/src/pages-sub/demo/index.vue
new file mode 100644
index 00000000..4231cff4
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages-sub/demo/index.vue
@@ -0,0 +1,27 @@
+
+{
+ "layout": "default",
+ "style": {
+ "navigationBarTitleText": "分包页面"
+ }
+}
+
+
+
+
+
+
+
+ http://localhost:9000/#/pages-sub/demo/index
+
+
+ 分包页面demo
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/pages/agent/edit.vue b/main/xiaozhi-mobile-admin/src/pages/agent/edit.vue
new file mode 100644
index 00000000..98526bb3
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages/agent/edit.vue
@@ -0,0 +1,1085 @@
+
+{
+ "layout": "default",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "编辑"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 基础信息
+
+
+
+
+
+
+
+ 助手昵称
+
+
+
+
+
+
+ 角色模式
+
+
+
+ {{ template.agentName }}
+
+
+
+
+
+
+ 角色介绍
+
+
+
+ {{ (formData.systemPrompt || '').length }}/2000
+
+
+
+
+
+
+
+ 模型配置
+
+
+
+
+
+
+
+
+ 语音活动检测
+
+
+ {{ displayNames.vad }}
+
+
+
+
+
+
+ 语音识别
+
+
+ {{ displayNames.asr }}
+
+
+
+
+
+
+ 大语言模型
+
+
+ {{ displayNames.llm }}
+
+
+
+
+
+
+ 视觉大模型
+
+
+ {{ displayNames.vllm }}
+
+
+
+
+
+
+ 意图识别
+
+
+ {{ displayNames.intent }}
+
+
+
+
+
+
+ 记忆
+
+
+ {{ displayNames.memory }}
+
+
+
+
+
+
+
+
+
+ 语音设置
+
+
+
+
+
+
+
+
+ 语音合成
+
+
+ {{ displayNames.tts }}
+
+
+
+
+
+
+ 角色音色
+
+
+ {{ displayNames.voiceprint }}
+
+
+
+
+
+
+ 插件
+
+
+ 编辑功能
+
+
+
+
+
+
+
+
+ 历史记忆
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ saving ? '保存中...' : '保存' }}
+
+
+
+
+ onPickerConfirm('vad', item.value, item.name)"
+ />
+
+ onPickerConfirm('asr', item.value, item.name)"
+ />
+
+ onPickerConfirm('llm', item.value, item.name)"
+ />
+
+ onPickerConfirm('vllm', item.value, item.name)"
+ />
+
+ onPickerConfirm('intent', item.value, item.name)"
+ />
+
+ onPickerConfirm('memory', item.value, item.name)"
+ />
+
+ onPickerConfirm('tts', item.value, item.name)"
+ />
+
+ onPickerConfirm('voiceprint', item.value, item.name)"
+ />
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/pages/agent/tools.vue b/main/xiaozhi-mobile-admin/src/pages/agent/tools.vue
new file mode 100644
index 00000000..1e355333
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages/agent/tools.vue
@@ -0,0 +1,726 @@
+
+{
+ "layout": "default",
+ "style": {
+ "navigationBarTitleText": "编辑功能",
+ "navigationStyle": "custom",
+ },
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 内置插件
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ func.name }}
+
+
+ {{ func.providerCode }}
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ func.name }}
+
+
+ 点击配置参数
+
+
+
+
+
+
+ ⚙
+
+
+
+
+
+ ×
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mcp接入点
+
+
+
+
+
+ 复制
+
+
+
+
+
+
+
+
+
+
+
+ {{ tool }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentFunction?.name }} 无需配置参数
+
+
+
+
+
+
+
+
+
+ {{ field.label }}
+
+
+ {{ getFieldRemark(field) }}
+
+
+
+
+
+
+
+
+
+
+
+ 每行输入一个项目
+
+
+
+
+
+
+
+ 请输入有效的JSON格式
+
+
+
+
+
+
+
+
+
+
+
+ 启用功能
+
+
+ 开启或关闭此功能
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/pages/chat-history/detail.vue b/main/xiaozhi-mobile-admin/src/pages/chat-history/detail.vue
new file mode 100644
index 00000000..88d91402
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages/chat-history/detail.vue
@@ -0,0 +1,331 @@
+
+{
+ "layout": "default",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "聊天详情"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getMessageContent(message) }}
+
+
+
+
+
+
+
+ {{ formatTime(message.createdAt) }}
+
+
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/pages/chat-history/index.vue b/main/xiaozhi-mobile-admin/src/pages/chat-history/index.vue
new file mode 100644
index 00000000..60ffbda8
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages/chat-history/index.vue
@@ -0,0 +1,342 @@
+
+{
+ "layout": "default",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "聊天记录"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 共 {{ session.chatCount }} 条对话
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无聊天记录
+
+
+ 与智能体的对话记录会显示在这里
+
+
+
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/pages/device-config/components/ultrasonic-config.vue b/main/xiaozhi-mobile-admin/src/pages/device-config/components/ultrasonic-config.vue
new file mode 100644
index 00000000..8f1ae7e1
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages/device-config/components/ultrasonic-config.vue
@@ -0,0 +1,684 @@
+
+
+
+
+
+
+
+
+ 选中网络: {{ props.selectedNetwork.ssid }}
+
+
+
+ 信号: {{ props.selectedNetwork.rssi }}dBm
+
+
+ {{ props.selectedNetwork.authmode === 0 ? '开放网络' : '加密网络' }}
+
+
+
+ 密码: {{ '*'.repeat(props.password.length) }}
+
+
+
+
+
+
+
+ {{ generating ? '生成中...' : '🎵 生成并播放声波' }}
+
+
+
+ {{ playing ? '播放中...' : '🔊 播放声波' }}
+
+
+
+ ⏹️ 停止播放
+
+
+
+
+
+
+
+ 自动循环播放声波
+
+
+
+
+
+
+
+
+ 配网音频文件
+
+
+ 时长: {{ audioLengthText }}
+
+
+
+
+
+
+
+ 超声波配网说明
+
+
+
+ 1. 确保已选择WiFi网络并输入密码
+
+
+ 2. 点击生成并播放声波,系统会将配网信息编码为音频
+
+
+ 3. 将手机靠近xiaozhi设备(距离1-2米)
+
+
+ 4. 音频播放时,xiaozhi会接收并解码配网信息
+
+
+ 5. 配网成功后设备会自动连接WiFi网络
+
+
+ 使用AFSK调制技术,通过1800Hz和1500Hz频率传输数据
+
+
+ 请确保手机音量适中,避免环境噪音干扰
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/pages/device-config/components/wifi-config.vue b/main/xiaozhi-mobile-admin/src/pages/device-config/components/wifi-config.vue
new file mode 100644
index 00000000..ac54b6b6
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages/device-config/components/wifi-config.vue
@@ -0,0 +1,230 @@
+
+
+
+
+
+
+
+
+ 选中网络: {{ props.selectedNetwork.ssid }}
+
+
+
+ 信号: {{ props.selectedNetwork.rssi }}dBm
+
+
+ {{ props.selectedNetwork.authmode === 0 ? '开放网络' : '加密网络' }}
+
+
+
+
+
+
+
+
+ {{ configuring ? '配网中...' : '开始WiFi配网' }}
+
+
+
+
+
+
+ WiFi配网说明
+
+
+
+ 1. 手机连接xiaozhi热点 (xiaozhi-XXXXXX)
+
+
+ 2. 选择要配网的目标WiFi网络
+
+
+ 3. 输入WiFi密码(如果需要)
+
+
+ 4. 点击开始配网,等待设备连接
+
+
+ 配网成功后设备会自动重启并连接目标WiFi
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/pages/device-config/components/wifi-selector.vue b/main/xiaozhi-mobile-admin/src/pages/device-config/components/wifi-selector.vue
new file mode 100644
index 00000000..a91d734b
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages/device-config/components/wifi-selector.vue
@@ -0,0 +1,556 @@
+
+
+
+
+
+
+
+
+
+ 请先连接xiaozhi热点 (xiaozhi-XXXXXX)
+
+
+ {{ checkingConnection ? '检测中...' : '重新检测' }}
+
+
+
+
+
+
+ 已连接xiaozhi热点
+
+
+ {{ checkingConnection ? '检测中...' : '刷新状态' }}
+
+
+
+
+
+
+
+
+
+ WiFi网络
+
+
+ {{ networkDisplayText }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无WiFi网络
+
+
+ 请点击刷新扫描
+
+
+
+
+
+
+
+ {{ network.ssid }}
+
+
+
+ 信号: {{ network.rssi }}dBm
+
+
+ 频道: {{ network.channel }}
+
+
+
+
+
+ {{ network.authmode === 0 ? '开放' : '加密' }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 网络密码
+
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/pages/device-config/index.vue b/main/xiaozhi-mobile-admin/src/pages/device-config/index.vue
new file mode 100644
index 00000000..9c8ec402
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages/device-config/index.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 配网方式
+
+
+
+
+
+
+ 配网方式
+
+
+ {{ configType === 'wifi' ? 'WiFi配网' : '超声波配网' }}
+
+
+
+
+
+
+
+
+ 网络配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ onConfigTypeConfirm(item)"
+ />
+
+
+
+
+{
+ "style": {
+ "navigationBarTitleText": "设备配网",
+ "navigationStyle": "custom"
+ }
+}
+
diff --git a/main/xiaozhi-mobile-admin/src/pages/device/index.vue b/main/xiaozhi-mobile-admin/src/pages/device/index.vue
new file mode 100644
index 00000000..5ef91d90
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages/device/index.vue
@@ -0,0 +1,367 @@
+
+{
+ "layout": "default",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "设备管理"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getDeviceTypeName(device.board) }}
+
+
+
+
+
+ MAC地址:{{ device.macAddress }}
+
+
+ 固件版本:{{ device.appVersion }}
+
+
+ 最近对话:{{ formatTime(device.lastConnectedAt) }}
+
+
+
+
+
+ OTA升级
+
+
+
+
+
+
+
+
+
+
+
+ 解绑
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无设备
+
+
+ 点击右下角 + 号绑定您的第一个设备
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/pages/index/index.vue b/main/xiaozhi-mobile-admin/src/pages/index/index.vue
new file mode 100644
index 00000000..eced9e2f
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages/index/index.vue
@@ -0,0 +1,599 @@
+
+
+{
+ "layout": "tabbar",
+ "style": {
+ // 'custom' 表示开启自定义导航栏,默认 'default'
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "首页"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 你好,小智
+
+
+ 让我们度过
+ 美好的一天!
+
+
+
+ Hello, Let's have a wonderful day!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ agent.agentName }}
+
+
+
+
+
+ 语言模型: {{ agent.llmModelName }}
+
+
+ 音色模型: {{ agent.ttsModelName }} ({{ agent.ttsVoiceName }})
+
+
+
+
+
+
+
+ 设备管理({{ agent.deviceCount }})
+
+
+
+
+
+ 最近对话:{{ formatTime(agent.lastConnectedAt) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无智能体
+
+
+ 点击右下角 + 号创建您的第一个智能体
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/pages/login/index.vue b/main/xiaozhi-mobile-admin/src/pages/login/index.vue
new file mode 100644
index 00000000..d0f1e1c7
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages/login/index.vue
@@ -0,0 +1,777 @@
+
+{
+ "layout": "default",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "登陆"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ selectedAreaCode }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 忘记密码?
+
+
+
+
+ {{ loading ? '登录中...' : '登录' }}
+
+
+
+
+ 还没有账户?
+
+
+ 立即注册
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+ {{ item.key }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/pages/register/index.vue b/main/xiaozhi-mobile-admin/src/pages/register/index.vue
new file mode 100644
index 00000000..68691d1d
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages/register/index.vue
@@ -0,0 +1,877 @@
+
+{
+ "layout": "default",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "注册"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ selectedAreaCode }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
+
+
+
+
+
+ {{ loading ? '注册中...' : '注册' }}
+
+
+
+
+ 已有账户?
+
+
+ 立即登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+ {{ item.key }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/pages/voiceprint/index.vue b/main/xiaozhi-mobile-admin/src/pages/voiceprint/index.vue
new file mode 100644
index 00000000..824ebf8c
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/pages/voiceprint/index.vue
@@ -0,0 +1,544 @@
+
+{
+ "layout": "default",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "声纹管理"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.sourceName }}
+
+
+ {{ item.introduce || '暂无描述' }}
+
+
+ {{ item.createDate }}
+
+
+
+
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无声纹数据
+
+
+ 点击右下角 + 号添加您的第一个说话人
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加说话人
+
+
+
+
+
+
+
+ * 声纹向量
+
+
+
+ {{ getSelectedAudioContent(addForm.audioId) }}
+
+
+
+
+
+
+
+
+ * 姓名
+
+
+
+
+
+
+
+ * 描述
+
+
+
+ {{ (addForm.introduce || '').length }}/100
+
+
+
+
+
+
+ 取消
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+
+ 编辑说话人
+
+
+
+
+
+
+
+ * 声纹向量
+
+
+
+ {{ getSelectedAudioContent(editForm.audioId) }}
+
+
+
+
+
+
+
+
+ * 姓名
+
+
+
+
+
+
+
+ * 描述
+
+
+
+ {{ (editForm.introduce || '').length }}/100
+
+
+
+
+
+
+ 取消
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/router/interceptor.ts b/main/xiaozhi-mobile-admin/src/router/interceptor.ts
new file mode 100644
index 00000000..74905a64
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/router/interceptor.ts
@@ -0,0 +1,66 @@
+/**
+ * by 菲鸽 on 2024-03-06
+ * 路由拦截,通常也是登录拦截
+ * 可以设置路由白名单,或者黑名单,看业务需要选哪一个
+ * 我这里应为大部分都可以随便进入,所以使用黑名单
+ */
+import { useUserStore } from '@/store'
+import { needLoginPages as _needLoginPages, getLastPage, getNeedLoginPages } from '@/utils'
+
+// TODO Check
+const loginRoute = import.meta.env.VITE_LOGIN_URL
+
+function isLogined() {
+ const userStore = useUserStore()
+ return !!userStore.userInfo.username
+}
+
+const isDev = import.meta.env.DEV
+
+// 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录)
+const navigateToInterceptor = {
+ // 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
+ // 增加对相对路径的处理,BY 网友 @ideal
+ invoke({ url }: { url: string }) {
+ // console.log(url) // /pages/route-interceptor/index?name=feige&age=30
+ let path = url.split('?')[0]
+ console.log('页面变动')
+
+ // 处理相对路径
+ if (!path.startsWith('/')) {
+ const currentPath = getLastPage().route
+ const normalizedCurrentPath = currentPath.startsWith('/') ? currentPath : `/${currentPath}`
+ const baseDir = normalizedCurrentPath.substring(0, normalizedCurrentPath.lastIndexOf('/'))
+ path = `${baseDir}/${path}`
+ }
+
+ let needLoginPages: string[] = []
+ // 为了防止开发时出现BUG,这里每次都获取一下。生产环境可以移到函数外,性能更好
+ if (isDev) {
+ needLoginPages = getNeedLoginPages()
+ }
+ else {
+ needLoginPages = _needLoginPages
+ }
+ const isNeedLogin = needLoginPages.includes(path)
+ if (!isNeedLogin) {
+ return true
+ }
+ const hasLogin = isLogined()
+ if (hasLogin) {
+ return true
+ }
+ const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(url)}`
+ uni.navigateTo({ url: redirectRoute })
+ return false
+ },
+}
+
+export const routeInterceptor = {
+ install() {
+ uni.addInterceptor('navigateTo', navigateToInterceptor)
+ uni.addInterceptor('reLaunch', navigateToInterceptor)
+ uni.addInterceptor('redirectTo', navigateToInterceptor)
+ uni.addInterceptor('switchTab', navigateToInterceptor)
+ },
+}
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/1024x1024.png b/main/xiaozhi-mobile-admin/src/static/app/icons/1024x1024.png
new file mode 100644
index 0000000000000000000000000000000000000000..2ceae9c25cec36e68a686d54662123edd35a3c51
GIT binary patch
literal 458242
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
z