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 00000000..2ceae9c2
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/1024x1024.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/120x120.png b/main/xiaozhi-mobile-admin/src/static/app/icons/120x120.png
new file mode 100644
index 00000000..3eb78f64
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/120x120.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/144x144.png b/main/xiaozhi-mobile-admin/src/static/app/icons/144x144.png
new file mode 100644
index 00000000..a1036734
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/144x144.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/152x152.png b/main/xiaozhi-mobile-admin/src/static/app/icons/152x152.png
new file mode 100644
index 00000000..6829c936
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/152x152.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/167x167.png b/main/xiaozhi-mobile-admin/src/static/app/icons/167x167.png
new file mode 100644
index 00000000..5e9289b4
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/167x167.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/180x180.png b/main/xiaozhi-mobile-admin/src/static/app/icons/180x180.png
new file mode 100644
index 00000000..607fc4ac
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/180x180.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/192x192.png b/main/xiaozhi-mobile-admin/src/static/app/icons/192x192.png
new file mode 100644
index 00000000..4a912736
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/192x192.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/20x20.png b/main/xiaozhi-mobile-admin/src/static/app/icons/20x20.png
new file mode 100644
index 00000000..f4b0e598
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/20x20.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/29x29.png b/main/xiaozhi-mobile-admin/src/static/app/icons/29x29.png
new file mode 100644
index 00000000..403fda9f
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/29x29.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/40x40.png b/main/xiaozhi-mobile-admin/src/static/app/icons/40x40.png
new file mode 100644
index 00000000..88ae47c5
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/40x40.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/58x58.png b/main/xiaozhi-mobile-admin/src/static/app/icons/58x58.png
new file mode 100644
index 00000000..889b9fff
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/58x58.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/60x60.png b/main/xiaozhi-mobile-admin/src/static/app/icons/60x60.png
new file mode 100644
index 00000000..6d0b41a5
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/60x60.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/72x72.png b/main/xiaozhi-mobile-admin/src/static/app/icons/72x72.png
new file mode 100644
index 00000000..6cf11e53
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/72x72.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/76x76.png b/main/xiaozhi-mobile-admin/src/static/app/icons/76x76.png
new file mode 100644
index 00000000..a1c11df8
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/76x76.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/80x80.png b/main/xiaozhi-mobile-admin/src/static/app/icons/80x80.png
new file mode 100644
index 00000000..68b1891b
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/80x80.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/87x87.png b/main/xiaozhi-mobile-admin/src/static/app/icons/87x87.png
new file mode 100644
index 00000000..6ac4e62f
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/87x87.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/app/icons/96x96.png b/main/xiaozhi-mobile-admin/src/static/app/icons/96x96.png
new file mode 100644
index 00000000..2644d325
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/app/icons/96x96.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/images/.gitkeep b/main/xiaozhi-mobile-admin/src/static/images/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/main/xiaozhi-mobile-admin/src/static/images/default-avatar.png b/main/xiaozhi-mobile-admin/src/static/images/default-avatar.png
new file mode 100644
index 00000000..4eb58795
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/images/default-avatar.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/images/home/main-top-bg.png b/main/xiaozhi-mobile-admin/src/static/images/home/main-top-bg.png
new file mode 100644
index 00000000..e56ba941
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/images/home/main-top-bg.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/logo.png b/main/xiaozhi-mobile-admin/src/static/logo.png
new file mode 100644
index 00000000..9e664970
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/logo.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/logo.svg b/main/xiaozhi-mobile-admin/src/static/logo.svg
new file mode 100644
index 00000000..eaee6698
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/static/logo.svg
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/chat.png b/main/xiaozhi-mobile-admin/src/static/tabbar/chat.png
new file mode 100644
index 00000000..90c4338c
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/chat.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/chat_activate.png b/main/xiaozhi-mobile-admin/src/static/tabbar/chat_activate.png
new file mode 100644
index 00000000..78d38e7c
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/chat_activate.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/device.png b/main/xiaozhi-mobile-admin/src/static/tabbar/device.png
new file mode 100644
index 00000000..2d624f47
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/device.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/device_activate.png b/main/xiaozhi-mobile-admin/src/static/tabbar/device_activate.png
new file mode 100644
index 00000000..1b8f96f8
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/device_activate.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/example.png b/main/xiaozhi-mobile-admin/src/static/tabbar/example.png
new file mode 100644
index 00000000..fd1e942b
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/example.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/exampleHL.png b/main/xiaozhi-mobile-admin/src/static/tabbar/exampleHL.png
new file mode 100644
index 00000000..7501011a
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/exampleHL.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/home.png b/main/xiaozhi-mobile-admin/src/static/tabbar/home.png
new file mode 100644
index 00000000..8f82e213
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/home.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/homeHL.png b/main/xiaozhi-mobile-admin/src/static/tabbar/homeHL.png
new file mode 100644
index 00000000..26d37613
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/homeHL.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/microphone.png b/main/xiaozhi-mobile-admin/src/static/tabbar/microphone.png
new file mode 100644
index 00000000..bde9d13b
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/microphone.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/microphone_activate.png b/main/xiaozhi-mobile-admin/src/static/tabbar/microphone_activate.png
new file mode 100644
index 00000000..c6b6c416
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/microphone_activate.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/personal.png b/main/xiaozhi-mobile-admin/src/static/tabbar/personal.png
new file mode 100644
index 00000000..0a569a2f
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/personal.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/personalHL.png b/main/xiaozhi-mobile-admin/src/static/tabbar/personalHL.png
new file mode 100644
index 00000000..8c3e66e8
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/personalHL.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/robot.png b/main/xiaozhi-mobile-admin/src/static/tabbar/robot.png
new file mode 100644
index 00000000..ffe1db27
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/robot.png differ
diff --git a/main/xiaozhi-mobile-admin/src/static/tabbar/robot_activate.png b/main/xiaozhi-mobile-admin/src/static/tabbar/robot_activate.png
new file mode 100644
index 00000000..aa5c3055
Binary files /dev/null and b/main/xiaozhi-mobile-admin/src/static/tabbar/robot_activate.png differ
diff --git a/main/xiaozhi-mobile-admin/src/store/agent.ts b/main/xiaozhi-mobile-admin/src/store/agent.ts
new file mode 100644
index 00000000..fe99f12f
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/store/agent.ts
@@ -0,0 +1,122 @@
+import type { Agent } from '@/api/agent/types'
+import { defineStore } from 'pinia'
+import { getAgentList } from '@/api/agent/agent'
+
+export const useAgentStore = defineStore('agent', {
+ state: () => ({
+ // 智能体列表
+ agentList: [] as Agent[],
+ // 当前选中的智能体ID
+ currentAgentId: '',
+ // 当前选中的智能体信息
+ currentAgent: null as Agent | null,
+ // 是否已加载智能体列表
+ isLoaded: false,
+ }),
+
+ getters: {
+ // 获取当前智能体
+ getCurrentAgent: (state) => {
+ return state.agentList.find(agent => agent.id === state.currentAgentId) || null
+ },
+
+ // 获取智能体列表用于选择器
+ getAgentOptions: (state) => {
+ return state.agentList.map(agent => ({
+ name: agent.agentName,
+ value: agent.id,
+ agent,
+ }))
+ },
+
+ // 是否有智能体列表
+ hasAgents: (state) => {
+ return state.agentList.length > 0
+ },
+ },
+
+ actions: {
+ // 加载智能体列表
+ async loadAgentList() {
+ try {
+ const list = await getAgentList()
+ this.agentList = list
+ this.isLoaded = true
+
+ // 如果没有当前选中的智能体,且列表不为空,选择第一个
+ if (!this.currentAgentId && list.length > 0) {
+ this.setCurrentAgent(list[0].id)
+ }
+
+ return list
+ }
+ catch (error) {
+ console.error('加载智能体列表失败:', error)
+ throw error
+ }
+ },
+
+ // 设置当前智能体
+ setCurrentAgent(agentId: string) {
+ this.currentAgentId = agentId
+ console.log(this.agentList.find(agent => agent.id === agentId), '执行')
+
+ this.currentAgent = this.agentList.find(agent => agent.id === agentId) || null
+ },
+
+ // 添加智能体到列表
+ addAgent(agent: Agent) {
+ this.agentList.push(agent)
+
+ // 如果是第一个智能体,设置为当前智能体
+ if (this.agentList.length === 1) {
+ this.setCurrentAgent(agent.id)
+ }
+ },
+
+ // 从列表中移除智能体
+ removeAgent(agentId: string) {
+ const index = this.agentList.findIndex(agent => agent.id === agentId)
+ if (index > -1) {
+ this.agentList.splice(index, 1)
+
+ // 如果删除的是当前选中的智能体,重新选择
+ if (this.currentAgentId === agentId) {
+ if (this.agentList.length > 0) {
+ this.setCurrentAgent(this.agentList[0].id)
+ }
+ else {
+ this.currentAgentId = ''
+ this.currentAgent = null
+ }
+ }
+ }
+ },
+
+ // 更新智能体信息
+ updateAgent(agent: Agent) {
+ const index = this.agentList.findIndex(item => item.id === agent.id)
+ if (index > -1) {
+ this.agentList[index] = agent
+
+ // 如果是当前智能体,更新当前智能体信息
+ if (this.currentAgentId === agent.id) {
+ this.currentAgent = agent
+ }
+ }
+ },
+
+ // 清空状态
+ clearState() {
+ this.agentList = []
+ this.currentAgentId = ''
+ this.currentAgent = null
+ this.isLoaded = false
+ },
+ },
+
+ persist: {
+ // 持久化当前选中的智能体ID
+ // paths: ['currentAgentId'],
+ },
+})
diff --git a/main/xiaozhi-mobile-admin/src/store/config.ts b/main/xiaozhi-mobile-admin/src/store/config.ts
new file mode 100644
index 00000000..9609ec6e
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/store/config.ts
@@ -0,0 +1,65 @@
+import type { PublicConfig } from '@/api/auth'
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import { getPublicConfig } from '@/api/auth'
+
+// 初始化状态
+const initialConfigState: PublicConfig = {
+ enableMobileRegister: false,
+ version: '',
+ year: '',
+ allowUserRegister: false,
+ mobileAreaList: [],
+ beianIcpNum: '',
+ beianGaNum: '',
+ name: '小智智能助手',
+}
+
+export const useConfigStore = defineStore(
+ 'config',
+ () => {
+ // 定义全局配置
+ const config = ref({ ...initialConfigState })
+
+ // 设置配置信息
+ const setConfig = (val: PublicConfig) => {
+ config.value = val
+ }
+
+ // 获取公共配置
+ const fetchPublicConfig = async () => {
+ try {
+ const configData = await getPublicConfig()
+ console.log(configData)
+
+ setConfig(configData)
+ return configData
+ }
+ catch (error) {
+ console.error('获取公共配置失败:', error)
+ throw error
+ }
+ }
+
+ // 重置配置
+ const resetConfig = () => {
+ config.value = { ...initialConfigState }
+ }
+
+ return {
+ config,
+ setConfig,
+ fetchPublicConfig,
+ resetConfig,
+ }
+ },
+ {
+ persist: {
+ key: 'config',
+ serializer: {
+ serialize: state => JSON.stringify(state.config),
+ deserialize: value => ({ config: JSON.parse(value) }),
+ },
+ },
+ },
+)
diff --git a/main/xiaozhi-mobile-admin/src/store/index.ts b/main/xiaozhi-mobile-admin/src/store/index.ts
new file mode 100644
index 00000000..e9f960d7
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/store/index.ts
@@ -0,0 +1,20 @@
+import { createPinia } from 'pinia'
+import { createPersistedState } from 'pinia-plugin-persistedstate' // 数据持久化
+
+const store = createPinia()
+store.use(
+ createPersistedState({
+ storage: {
+ getItem: uni.getStorageSync,
+ setItem: uni.setStorageSync,
+ },
+ }),
+)
+
+export default store
+
+export * from './agent'
+export * from './config'
+export * from './plugin'
+// 模块统一导出
+export * from './user'
diff --git a/main/xiaozhi-mobile-admin/src/store/plugin.ts b/main/xiaozhi-mobile-admin/src/store/plugin.ts
new file mode 100644
index 00000000..200b0850
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/store/plugin.ts
@@ -0,0 +1,58 @@
+import type { AgentFunction, PluginDefinition } from '@/api/agent/types'
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const usePluginStore = defineStore(
+ 'plugin',
+ () => {
+ // 所有可用插件
+ const allFunctions = ref([])
+
+ // 当前智能体的插件配置
+ const currentFunctions = ref([])
+
+ // 当前编辑的智能体ID
+ const currentAgentId = ref('')
+
+ // 设置所有可用插件
+ const setAllFunctions = (functions: PluginDefinition[]) => {
+ allFunctions.value = functions
+ }
+
+ // 设置当前智能体的插件配置
+ const setCurrentFunctions = (functions: AgentFunction[]) => {
+ currentFunctions.value = functions
+ }
+
+ // 设置当前智能体ID
+ const setCurrentAgentId = (agentId: string) => {
+ currentAgentId.value = agentId
+ }
+
+ // 更新插件配置(用于保存时调用)
+ const updateFunctions = (functions: AgentFunction[]) => {
+ currentFunctions.value = functions
+ }
+
+ // 清空数据
+ const clear = () => {
+ allFunctions.value = []
+ currentFunctions.value = []
+ currentAgentId.value = ''
+ }
+
+ return {
+ allFunctions,
+ currentFunctions,
+ currentAgentId,
+ setAllFunctions,
+ setCurrentFunctions,
+ setCurrentAgentId,
+ updateFunctions,
+ clear,
+ }
+ },
+ {
+ persist: false, // 不持久化,每次进入页面重新加载
+ },
+)
diff --git a/main/xiaozhi-mobile-admin/src/store/user.ts b/main/xiaozhi-mobile-admin/src/store/user.ts
new file mode 100644
index 00000000..47f6d80c
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/store/user.ts
@@ -0,0 +1,83 @@
+import type { UserInfo } from '@/api/auth'
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import {
+ getUserInfo as _getUserInfo,
+} from '@/api/auth'
+
+// 初始化状态
+const userInfoState: UserInfo & { avatar?: string, token?: string } = {
+ id: 0,
+ username: '',
+ realName: '',
+ email: '',
+ mobile: '',
+ status: 0,
+ superAdmin: 0,
+ avatar: '/static/images/default-avatar.png',
+ token: '',
+}
+
+export const useUserStore = defineStore(
+ 'user',
+ () => {
+ // 定义用户信息
+ const userInfo = ref({ ...userInfoState })
+ // 设置用户信息
+ const setUserInfo = (val: UserInfo & { avatar?: string, token?: string }) => {
+ console.log('设置用户信息', val)
+ // 若头像为空 则使用默认头像
+ if (!val.avatar) {
+ val.avatar = userInfoState.avatar
+ }
+ else {
+ val.avatar = 'https://oss.laf.run/ukw0y1-site/avatar.jpg?feige'
+ }
+ userInfo.value = val
+ }
+ const setUserAvatar = (avatar: string) => {
+ userInfo.value.avatar = avatar
+ console.log('设置用户头像', avatar)
+ console.log('userInfo', userInfo.value)
+ }
+ // 删除用户信息
+ const removeUserInfo = () => {
+ userInfo.value = { ...userInfoState }
+ uni.removeStorageSync('userInfo')
+ uni.removeStorageSync('token')
+ }
+ /**
+ * 获取用户信息
+ */
+ const getUserInfo = async () => {
+ const userData = await _getUserInfo()
+ const userInfoWithExtras = {
+ ...userData,
+ avatar: userInfoState.avatar,
+ token: uni.getStorageSync('token') || '',
+ }
+ setUserInfo(userInfoWithExtras)
+ uni.setStorageSync('userInfo', userInfoWithExtras)
+ // TODO 这里可以增加获取用户路由的方法 根据用户的角色动态生成路由
+ return userInfoWithExtras
+ }
+ /**
+ * 退出登录 并 删除用户信息
+ */
+ const logout = async () => {
+ removeUserInfo()
+ }
+
+ return {
+ userInfo,
+ getUserInfo,
+ setUserInfo,
+ setUserAvatar,
+ logout,
+ removeUserInfo,
+ }
+ },
+ {
+ persist: true,
+ },
+)
diff --git a/main/xiaozhi-mobile-admin/src/style/iconfont.css b/main/xiaozhi-mobile-admin/src/style/iconfont.css
new file mode 100644
index 00000000..35da86c6
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/style/iconfont.css
@@ -0,0 +1,28 @@
+@font-face {
+ font-family: 'iconfont'; /* Project id 4543091 */
+ src:
+ url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAOwAAsAAAAAB9AAAANjAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACDHAqDBIJqATYCJAMQCwoABCAFhGcHPRvnBsgusG3kMyE15/44PsBX09waBHv0REDt97oHAQDFrOIyPirRiULQ+TJcXV0hCYTuVFcBC915/2vX/32Q80hkZ5PZGZ9snvwruVLloidKqYN6iKC53bOtbKwVLSIi3W6zCWZbs3VbER3j9JpGX3ySYcc94IQRTK5s4epS/jSqIgvg37qlY2/jwQN7D9ADpfRCmIknQByTscVZPTBr+hnnCKg2o4bjakvXEPjuY65DJGeJNtBUhn1JxOBuB2UZmUpBOXdsFp4oxOv4GHgs3h/+wRDcicqSZJG1q9kK1z/Af9NpqxjpC2QaAdpHlCFh4spcYXs5sMWpSk5wUj31G2dLQKVKkZ/w7f/8/i/A3JVUSZK9f7xIKJeU14IFpBI/Qfkkz46GT/CuaGREfCtKJUougWeQWHvVC5Lcz2BGS+SePR99vj3yjJx7h574tp7uWcOh4yfaTjS/245TT/vkQrN+a7RLkK8+Vd+bz+FSGh+9srDQKPeJ2s29z7ah4+efdoxefRbbGwfy7ht+SuIWukzsu1b6ePP+6kN1aamb47qsPim1Ia3xdEpDcl1dckPKGYnneI23+57r2W1Mmkqs6ajrChRCs5qyQ66rTVWhgZaG7toOeHm5cxn0sSQuNDEgcUTdNTSupKI1JRZih/JssAUKezPeOJJzbNozF6zWJuuVavVU5Tgtkop/SDzHa7ytvnCTq0PhkEfi4xLLtb0PuwyOAYqmrYQApFJyoJjTnfz+ve94vvv2f/yWgxl8Jd8Di2DRDPuob59mU/+VfDCROQyR8xSnmP9fXm7liagmN39OlmbvjqG0sMsJKrU0EFXogaRSH5bNY1CmxhyUq7QC1cY1T67RwuQk5CoM2RUQNLoEUb03kDS6h2XzcyjT7iOUa/QXqq1Hn6/GUBAaGcGcWJFlGUmCoVOp8kLvABHnVczGYiOE2SVEUH5OXj/TSnTCDjHAviAWcE4RZYaGWszNiKoayGSGTASeY+PcrMjNpVMvyREMDRoxBMYRVojFMkQiMOhohubdzxtAiOapMMbERpKMnQT9SL4ceQysVdJZVa9kEbsFogIcRyEUE2kN0mL7CDVIGhBzupWMEHA5bDvipgq5hKJcKef8ivbx1kC15KgcYkghhzLxYNntxoKCReJ82jAHAAA=')
+ format('woff2'),
+ url('//at.alicdn.com/t/c/font_4543091_njpo5b95nl.woff?t=1715485842402') format('woff'),
+ url('//at.alicdn.com/t/c/font_4543091_njpo5b95nl.ttf?t=1715485842402') format('truetype');
+}
+
+.iconfont {
+ font-family: 'iconfont' !important;
+ font-size: 16px;
+ font-style: normal;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-my:before {
+ content: '\e78c';
+}
+
+.icon-package:before {
+ content: '\e9c2';
+}
+
+.icon-chat:before {
+ content: '\e600';
+}
diff --git a/main/xiaozhi-mobile-admin/src/style/index.scss b/main/xiaozhi-mobile-admin/src/style/index.scss
new file mode 100644
index 00000000..e1d33c59
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/style/index.scss
@@ -0,0 +1,19 @@
+// 测试用的 iconfont,可生效
+// @import './iconfont.css';
+
+.test {
+ // 可以通过 @apply 多个样式封装整体样式
+ @apply mt-4 ml-4;
+
+ padding-top: 4px;
+ color: red;
+}
+
+:root,
+page {
+ // 修改按主题色
+ // --wot-color-theme: #37c2bc;
+
+ // 修改按钮背景色
+ // --wot-button-primary-bg-color: green;
+}
diff --git a/main/xiaozhi-mobile-admin/src/typings.d.ts b/main/xiaozhi-mobile-admin/src/typings.d.ts
new file mode 100644
index 00000000..9ead3fb5
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/typings.d.ts
@@ -0,0 +1,28 @@
+// 全局要用的类型放到这里
+
+declare global {
+ interface IResData {
+ code: number
+ msg: string
+ data: T
+ }
+
+ // uni.uploadFile文件上传参数
+ interface IUniUploadFileOptions {
+ file?: File
+ files?: UniApp.UploadFileOptionFiles[]
+ filePath?: string
+ name?: string
+ formData?: any
+ }
+
+ interface IUserInfo {
+ nickname?: string
+ avatar?: string
+ /** 微信的 openid,非微信没有这个字段 */
+ openid?: string
+ token?: string
+ }
+}
+
+export {} // 防止模块污染
diff --git a/main/xiaozhi-mobile-admin/src/typings.ts b/main/xiaozhi-mobile-admin/src/typings.ts
new file mode 100644
index 00000000..b48b6304
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/typings.ts
@@ -0,0 +1,15 @@
+// 枚举定义
+
+export enum TestEnum {
+ A = '1',
+ B = '2',
+}
+
+// uni.uploadFile文件上传参数
+export interface IUniUploadFileOptions {
+ file?: File
+ files?: UniApp.UploadFileOptionFiles[]
+ filePath?: string
+ name?: string
+ formData?: any
+}
diff --git a/main/xiaozhi-mobile-admin/src/uni.scss b/main/xiaozhi-mobile-admin/src/uni.scss
new file mode 100644
index 00000000..21b9e5fc
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/uni.scss
@@ -0,0 +1,77 @@
+/* stylelint-disable comment-empty-line-before */
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+
+/* 颜色变量 */
+
+/* 行为相关颜色 */
+$uni-color-primary: #007aff;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+
+/* 文字基本颜色 */
+$uni-text-color: #333; // 基本色
+$uni-text-color-inverse: #fff; // 反色
+$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable: #c0c0c0;
+
+/* 背景颜色 */
+$uni-bg-color: #fff;
+$uni-bg-color-grey: #f8f8f8;
+$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
+$uni-bg-color-mask: rgb(0 0 0 / 40%); // 遮罩颜色
+
+/* 边框颜色 */
+$uni-border-color: #c8c7cc;
+
+/* 尺寸变量 */
+
+/* 文字尺寸 */
+$uni-font-size-sm: 12px;
+$uni-font-size-base: 14px;
+$uni-font-size-lg: 16;
+
+/* 图片尺寸 */
+$uni-img-size-sm: 20px;
+$uni-img-size-base: 26px;
+$uni-img-size-lg: 40px;
+
+/* Border Radius */
+$uni-border-radius-sm: 2px;
+$uni-border-radius-base: 3px;
+$uni-border-radius-lg: 6px;
+$uni-border-radius-circle: 50%;
+
+/* 水平间距 */
+$uni-spacing-row-sm: 5px;
+$uni-spacing-row-base: 10px;
+$uni-spacing-row-lg: 15px;
+
+/* 垂直间距 */
+$uni-spacing-col-sm: 4px;
+$uni-spacing-col-base: 8px;
+$uni-spacing-col-lg: 12px;
+
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+
+/* 文章场景相关 */
+$uni-color-title: #2c405a; // 文章标题颜色
+$uni-font-size-title: 20px;
+$uni-color-subtitle: #555; // 二级标题颜色
+$uni-font-size-subtitle: 18px;
+$uni-color-paragraph: #3f536e; // 文章段落颜色
+$uni-font-size-paragraph: 15px;
diff --git a/main/xiaozhi-mobile-admin/src/utils/index.ts b/main/xiaozhi-mobile-admin/src/utils/index.ts
new file mode 100644
index 00000000..fa814133
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/utils/index.ts
@@ -0,0 +1,172 @@
+import { pages, subPackages } from '@/pages.json'
+import { isMpWeixin } from './platform'
+
+export function getLastPage() {
+ // getCurrentPages() 至少有1个元素,所以不再额外判断
+ // const lastPage = getCurrentPages().at(-1)
+ // 上面那个在低版本安卓中打包会报错,所以改用下面这个【虽然我加了 src/interceptions/prototype.ts,但依然报错】
+ const pages = getCurrentPages()
+ return pages[pages.length - 1]
+}
+
+/**
+ * 获取当前页面路由的 path 路径和 redirectPath 路径
+ * path 如 '/pages/login/index'
+ * redirectPath 如 '/pages/demo/base/route-interceptor'
+ */
+export function currRoute() {
+ const lastPage = getLastPage()
+ const currRoute = (lastPage as any).$page
+ // console.log('lastPage.$page:', currRoute)
+ // console.log('lastPage.$page.fullpath:', currRoute.fullPath)
+ // console.log('lastPage.$page.options:', currRoute.options)
+ // console.log('lastPage.options:', (lastPage as any).options)
+ // 经过多端测试,只有 fullPath 靠谱,其他都不靠谱
+ const { fullPath } = currRoute as { fullPath: string }
+ // console.log(fullPath)
+ // eg: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序)
+ // eg: /pages/login/index?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5)
+ return getUrlObj(fullPath)
+}
+
+function ensureDecodeURIComponent(url: string) {
+ if (url.startsWith('%')) {
+ return ensureDecodeURIComponent(decodeURIComponent(url))
+ }
+ return url
+}
+/**
+ * 解析 url 得到 path 和 query
+ * 比如输入url: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor
+ * 输出: {path: /pages/login/index, query: {redirect: /pages/demo/base/route-interceptor}}
+ */
+export function getUrlObj(url: string) {
+ const [path, queryStr] = url.split('?')
+ // console.log(path, queryStr)
+
+ if (!queryStr) {
+ return {
+ path,
+ query: {},
+ }
+ }
+ const query: Record = {}
+ queryStr.split('&').forEach((item) => {
+ const [key, value] = item.split('=')
+ // console.log(key, value)
+ query[key] = ensureDecodeURIComponent(value) // 这里需要统一 decodeURIComponent 一下,可以兼容h5和微信y
+ })
+ return { path, query }
+}
+/**
+ * 得到所有的需要登录的 pages,包括主包和分包的
+ * 这里设计得通用一点,可以传递 key 作为判断依据,默认是 needLogin, 与 route-block 配对使用
+ * 如果没有传 key,则表示所有的 pages,如果传递了 key, 则表示通过 key 过滤
+ */
+export function getAllPages(key = 'needLogin') {
+ // 这里处理主包
+ const mainPages = pages
+ .filter(page => !key || page[key])
+ .map(page => ({
+ ...page,
+ path: `/${page.path}`,
+ }))
+
+ // 这里处理分包
+ const subPages: any[] = []
+ subPackages.forEach((subPageObj) => {
+ // console.log(subPageObj)
+ const { root } = subPageObj
+
+ subPageObj.pages
+ .filter(page => !key || page[key])
+ .forEach((page: { path: string } & Record) => {
+ subPages.push({
+ ...page,
+ path: `/${root}/${page.path}`,
+ })
+ })
+ })
+ const result = [...mainPages, ...subPages]
+ // console.log(`getAllPages by ${key} result: `, result)
+ return result
+}
+
+/**
+ * 得到所有的需要登录的 pages,包括主包和分包的
+ * 只得到 path 数组
+ */
+export const getNeedLoginPages = (): string[] => getAllPages('needLogin').map(page => page.path)
+
+/**
+ * 得到所有的需要登录的 pages,包括主包和分包的
+ * 只得到 path 数组
+ */
+export const needLoginPages: string[] = getAllPages('needLogin').map(page => page.path)
+
+/**
+ * 根据微信小程序当前环境,判断应该获取的 baseUrl
+ */
+export function getEnvBaseUrl() {
+ // 请求基准地址
+ let baseUrl = import.meta.env.VITE_SERVER_BASEURL
+
+ // # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
+ const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run'
+ const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run'
+ const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run'
+
+ // 微信小程序端环境区分
+ if (isMpWeixin) {
+ const {
+ miniProgram: { envVersion },
+ } = uni.getAccountInfoSync()
+
+ switch (envVersion) {
+ case 'develop':
+ baseUrl = VITE_SERVER_BASEURL__WEIXIN_DEVELOP || baseUrl
+ break
+ case 'trial':
+ baseUrl = VITE_SERVER_BASEURL__WEIXIN_TRIAL || baseUrl
+ break
+ case 'release':
+ baseUrl = VITE_SERVER_BASEURL__WEIXIN_RELEASE || baseUrl
+ break
+ }
+ }
+
+ return baseUrl
+}
+
+/**
+ * 根据微信小程序当前环境,判断应该获取的 UPLOAD_BASEURL
+ */
+export function getEnvBaseUploadUrl() {
+ // 请求基准地址
+ let baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL
+
+ const VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run/upload'
+ const VITE_UPLOAD_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run/upload'
+ const VITE_UPLOAD_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run/upload'
+
+ // 微信小程序端环境区分
+ if (isMpWeixin) {
+ const {
+ miniProgram: { envVersion },
+ } = uni.getAccountInfoSync()
+
+ switch (envVersion) {
+ case 'develop':
+ baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP || baseUploadUrl
+ break
+ case 'trial':
+ baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_TRIAL || baseUploadUrl
+ break
+ case 'release':
+ baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_RELEASE || baseUploadUrl
+ break
+ }
+ }
+
+ return baseUploadUrl
+}
diff --git a/main/xiaozhi-mobile-admin/src/utils/platform.ts b/main/xiaozhi-mobile-admin/src/utils/platform.ts
new file mode 100644
index 00000000..c714199e
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/utils/platform.ts
@@ -0,0 +1,24 @@
+/*
+ * @Author: 菲鸽
+ * @Date: 2024-03-28 19:13:55
+ * @Last Modified by: 菲鸽
+ * @Last Modified time: 2024-03-28 19:24:55
+ */
+export const platform = __UNI_PLATFORM__
+export const isH5 = __UNI_PLATFORM__ === 'h5'
+export const isApp = __UNI_PLATFORM__ === 'app'
+export const isMp = __UNI_PLATFORM__.startsWith('mp-')
+export const isMpWeixin = __UNI_PLATFORM__.startsWith('mp-weixin')
+export const isMpAplipay = __UNI_PLATFORM__.startsWith('mp-alipay')
+export const isMpToutiao = __UNI_PLATFORM__.startsWith('mp-toutiao')
+
+const PLATFORM = {
+ platform,
+ isH5,
+ isApp,
+ isMp,
+ isMpWeixin,
+ isMpAplipay,
+ isMpToutiao,
+}
+export default PLATFORM
diff --git a/main/xiaozhi-mobile-admin/src/utils/request.ts b/main/xiaozhi-mobile-admin/src/utils/request.ts
new file mode 100644
index 00000000..8b169257
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/utils/request.ts
@@ -0,0 +1,78 @@
+import type { CustomRequestOptions } from '@/http/interceptor'
+
+/**
+ * 请求方法: 主要是对 uni.request 的封装,去适配 openapi-ts-request 的 request 方法
+ * @param options 请求参数
+ * @returns 返回 Promise 对象
+ */
+function http(options: CustomRequestOptions) {
+ // 1. 返回 Promise 对象
+ return new Promise((resolve, reject) => {
+ uni.request({
+ ...options,
+ dataType: 'json',
+ // #ifndef MP-WEIXIN
+ responseType: 'json',
+ // #endif
+ // 响应成功
+ success(res) {
+ // 状态码 2xx,参考 axios 的设计
+ if (res.statusCode >= 200 && res.statusCode < 300) {
+ // 2.1 提取核心数据 res.data
+ resolve(res.data as T)
+ }
+ else if (res.statusCode === 401) {
+ // 401错误 -> 清理用户信息,跳转到登录页
+ // userStore.clearUserInfo()
+ // uni.navigateTo({ url: '/pages/login/login' })
+ reject(res)
+ }
+ else {
+ // 其他错误 -> 根据后端错误信息轻提示
+ !options.hideErrorToast
+ && uni.showToast({
+ icon: 'none',
+ title: (res.data as T & { msg?: string })?.msg || '请求错误',
+ })
+ reject(res)
+ }
+ },
+ // 响应失败
+ fail(err) {
+ uni.showToast({
+ icon: 'none',
+ title: '网络错误,换个网络试试',
+ })
+ reject(err)
+ },
+ })
+ })
+}
+
+/*
+ * openapi-ts-request 工具的 request 跨客户端适配方法
+ */
+export default function request(
+ url: string,
+ options: Omit & {
+ params?: Record
+ headers?: Record
+ },
+) {
+ const requestOptions = {
+ url,
+ ...options,
+ }
+
+ if (options.params) {
+ requestOptions.query = requestOptions.params
+ delete requestOptions.params
+ }
+
+ if (options.headers) {
+ requestOptions.header = options.headers
+ delete requestOptions.headers
+ }
+
+ return http(requestOptions)
+}
diff --git a/main/xiaozhi-mobile-admin/src/utils/toast.ts b/main/xiaozhi-mobile-admin/src/utils/toast.ts
new file mode 100644
index 00000000..e524b000
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/utils/toast.ts
@@ -0,0 +1,65 @@
+/**
+ * toast 弹窗组件
+ * 支持 success/error/warning/info 四种状态
+ * 可配置 duration, position 等参数
+ */
+
+type ToastType = 'success' | 'error' | 'warning' | 'info'
+
+interface ToastOptions {
+ type?: ToastType
+ duration?: number
+ position?: 'top' | 'middle' | 'bottom'
+ icon?: 'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception'
+ message: string
+}
+
+export function showToast(options: ToastOptions | string) {
+ const defaultOptions: ToastOptions = {
+ type: 'info',
+ duration: 2000,
+ position: 'middle',
+ message: '',
+ }
+ const mergedOptions
+ = typeof options === 'string'
+ ? { ...defaultOptions, message: options }
+ : { ...defaultOptions, ...options }
+ // 映射position到uniapp支持的格式
+ const positionMap: Record = {
+ top: 'top',
+ middle: 'center',
+ bottom: 'bottom',
+ }
+
+ // 映射图标类型
+ const iconMap: Record<
+ ToastType,
+ 'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception'
+ > = {
+ success: 'success',
+ error: 'error',
+ warning: 'fail',
+ info: 'none',
+ }
+
+ // 调用uni.showToast显示提示
+ uni.showToast({
+ title: mergedOptions.message,
+ duration: mergedOptions.duration,
+ position: positionMap[mergedOptions.position],
+ icon: mergedOptions.icon || iconMap[mergedOptions.type],
+ mask: true,
+ })
+}
+
+export const toast = {
+ success: (message: string, options?: Omit) =>
+ showToast({ ...options, type: 'success', message }),
+ error: (message: string, options?: Omit) =>
+ showToast({ ...options, type: 'error', message }),
+ warning: (message: string, options?: Omit) =>
+ showToast({ ...options, type: 'warning', message }),
+ info: (message: string, options?: Omit) =>
+ showToast({ ...options, type: 'info', message }),
+}
diff --git a/main/xiaozhi-mobile-admin/src/utils/uploadFile.ts b/main/xiaozhi-mobile-admin/src/utils/uploadFile.ts
new file mode 100644
index 00000000..416d39cb
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/src/utils/uploadFile.ts
@@ -0,0 +1,324 @@
+import { toast } from './toast'
+
+/**
+ * 文件上传钩子函数使用示例
+ * @example
+ * const { loading, error, data, progress, run } = useUpload(
+ * uploadUrl,
+ * {},
+ * {
+ * maxSize: 5, // 最大5MB
+ * sourceType: ['album'], // 仅支持从相册选择
+ * onProgress: (p) => console.log(`上传进度:${p}%`),
+ * onSuccess: (res) => console.log('上传成功', res),
+ * onError: (err) => console.error('上传失败', err),
+ * },
+ * )
+ */
+
+/**
+ * 上传文件的URL配置
+ */
+export const uploadFileUrl = {
+ /** 用户头像上传地址 */
+ USER_AVATAR: `${import.meta.env.VITE_SERVER_BASEURL}/user/avatar`,
+}
+
+/**
+ * 通用文件上传函数(支持直接传入文件路径)
+ * @param url 上传地址
+ * @param filePath 本地文件路径
+ * @param formData 额外表单数据
+ * @param options 上传选项
+ */
+export function useFileUpload(url: string, filePath: string, formData: Record = {}, options: Omit = {}) {
+ return useUpload(
+ url,
+ formData,
+ {
+ ...options,
+ sourceType: ['album'],
+ sizeType: ['original'],
+ },
+ filePath,
+ )
+}
+
+export interface UploadOptions {
+ /** 最大可选择的图片数量,默认为1 */
+ count?: number
+ /** 所选的图片的尺寸,original-原图,compressed-压缩图 */
+ sizeType?: Array<'original' | 'compressed'>
+ /** 选择图片的来源,album-相册,camera-相机 */
+ sourceType?: Array<'album' | 'camera'>
+ /** 文件大小限制,单位:MB */
+ maxSize?: number //
+ /** 上传进度回调函数 */
+ onProgress?: (progress: number) => void
+ /** 上传成功回调函数 */
+ onSuccess?: (res: Record) => void
+ /** 上传失败回调函数 */
+ onError?: (err: Error | UniApp.GeneralCallbackResult) => void
+ /** 上传完成回调函数(无论成功失败) */
+ onComplete?: () => void
+}
+
+/**
+ * 文件上传钩子函数
+ * @template T 上传成功后返回的数据类型
+ * @param url 上传地址
+ * @param formData 额外的表单数据
+ * @param options 上传选项
+ * @returns 上传状态和控制对象
+ */
+export function useUpload(url: string, formData: Record = {}, options: UploadOptions = {},
+ /** 直接传入文件路径,跳过选择器 */
+ directFilePath?: string) {
+ /** 上传中状态 */
+ const loading = ref(false)
+ /** 上传错误状态 */
+ const error = ref(false)
+ /** 上传成功后的响应数据 */
+ const data = ref()
+ /** 上传进度(0-100) */
+ const progress = ref(0)
+
+ /** 解构上传选项,设置默认值 */
+ const {
+ /** 最大可选择的图片数量 */
+ count = 1,
+ /** 所选的图片的尺寸 */
+ sizeType = ['original', 'compressed'],
+ /** 选择图片的来源 */
+ sourceType = ['album', 'camera'],
+ /** 文件大小限制(MB) */
+ maxSize = 10,
+ /** 进度回调 */
+ onProgress,
+ /** 成功回调 */
+ onSuccess,
+ /** 失败回调 */
+ onError,
+ /** 完成回调 */
+ onComplete,
+ } = options
+
+ /**
+ * 检查文件大小是否超过限制
+ * @param size 文件大小(字节)
+ * @returns 是否通过检查
+ */
+ const checkFileSize = (size: number) => {
+ const sizeInMB = size / 1024 / 1024
+ if (sizeInMB > maxSize) {
+ toast.warning(`文件大小不能超过${maxSize}MB`)
+ return false
+ }
+ return true
+ }
+ /**
+ * 触发文件选择和上传
+ * 根据平台使用不同的选择器:
+ * - 微信小程序使用 chooseMedia
+ * - 其他平台使用 chooseImage
+ */
+ const run = () => {
+ if (directFilePath) {
+ // 直接使用传入的文件路径
+ loading.value = true
+ progress.value = 0
+ uploadFile({
+ url,
+ tempFilePath: directFilePath,
+ formData,
+ data,
+ error,
+ loading,
+ progress,
+ onProgress,
+ onSuccess,
+ onError,
+ onComplete,
+ })
+ return
+ }
+
+ // #ifdef MP-WEIXIN
+ // 微信小程序环境下使用 chooseMedia API
+ uni.chooseMedia({
+ count,
+ mediaType: ['image'], // 仅支持图片类型
+ sourceType,
+ success: (res) => {
+ const file = res.tempFiles[0]
+ // 检查文件大小是否符合限制
+ if (!checkFileSize(file.size))
+ return
+
+ // 开始上传
+ loading.value = true
+ progress.value = 0
+ uploadFile({
+ url,
+ tempFilePath: file.tempFilePath,
+ formData,
+ data,
+ error,
+ loading,
+ progress,
+ onProgress,
+ onSuccess,
+ onError,
+ onComplete,
+ })
+ },
+ fail: (err) => {
+ console.error('选择媒体文件失败:', err)
+ error.value = true
+ onError?.(err)
+ },
+ })
+ // #endif
+
+ // #ifndef MP-WEIXIN
+ // 非微信小程序环境下使用 chooseImage API
+ uni.chooseImage({
+ count,
+ sizeType,
+ sourceType,
+ success: (res) => {
+ console.log('选择图片成功:', res)
+
+ // 开始上传
+ loading.value = true
+ progress.value = 0
+ uploadFile({
+ url,
+ tempFilePath: res.tempFilePaths[0],
+ formData,
+ data,
+ error,
+ loading,
+ progress,
+ onProgress,
+ onSuccess,
+ onError,
+ onComplete,
+ })
+ },
+ fail: (err) => {
+ console.error('选择图片失败:', err)
+ error.value = true
+ onError?.(err)
+ },
+ })
+ // #endif
+ }
+
+ return { loading, error, data, progress, run }
+}
+
+/**
+ * 文件上传选项接口
+ * @template T 上传成功后返回的数据类型
+ */
+interface UploadFileOptions {
+ /** 上传地址 */
+ url: string
+ /** 临时文件路径 */
+ tempFilePath: string
+ /** 额外的表单数据 */
+ formData: Record
+ /** 上传成功后的响应数据 */
+ data: Ref
+ /** 上传错误状态 */
+ error: Ref
+ /** 上传中状态 */
+ loading: Ref
+ /** 上传进度(0-100) */
+ progress: Ref
+ /** 上传进度回调 */
+ onProgress?: (progress: number) => void
+ /** 上传成功回调 */
+ onSuccess?: (res: Record) => void
+ /** 上传失败回调 */
+ onError?: (err: Error | UniApp.GeneralCallbackResult) => void
+ /** 上传完成回调 */
+ onComplete?: () => void
+}
+
+/**
+ * 执行文件上传
+ * @template T 上传成功后返回的数据类型
+ * @param options 上传选项
+ */
+function uploadFile({
+ url,
+ tempFilePath,
+ formData,
+ data,
+ error,
+ loading,
+ progress,
+ onProgress,
+ onSuccess,
+ onError,
+ onComplete,
+}: UploadFileOptions) {
+ try {
+ // 创建上传任务
+ const uploadTask = uni.uploadFile({
+ url,
+ filePath: tempFilePath,
+ name: 'file', // 文件对应的 key
+ formData,
+ header: {
+ // H5环境下不需要手动设置Content-Type,让浏览器自动处理multipart格式
+ // #ifndef H5
+ 'Content-Type': 'multipart/form-data',
+ // #endif
+ },
+ // 确保文件名称合法
+ success: (uploadFileRes) => {
+ console.log('上传文件成功:', uploadFileRes)
+ try {
+ // 解析响应数据
+ const { data: _data } = JSON.parse(uploadFileRes.data)
+ // 上传成功
+ data.value = _data as T
+ onSuccess?.(_data)
+ }
+ catch (err) {
+ // 响应解析错误
+ console.error('解析上传响应失败:', err)
+ error.value = true
+ onError?.(new Error('上传响应解析失败'))
+ }
+ },
+ fail: (err) => {
+ // 上传请求失败
+ console.error('上传文件失败:', err)
+ error.value = true
+ onError?.(err)
+ },
+ complete: () => {
+ // 无论成功失败都执行
+ loading.value = false
+ onComplete?.()
+ },
+ })
+
+ // 监听上传进度
+ uploadTask.onProgressUpdate((res) => {
+ progress.value = res.progress
+ onProgress?.(res.progress)
+ })
+ }
+ catch (err) {
+ // 创建上传任务失败
+ console.error('创建上传任务失败:', err)
+ error.value = true
+ loading.value = false
+ onError?.(new Error('创建上传任务失败'))
+ }
+}
diff --git a/main/xiaozhi-mobile-admin/uno.config.ts b/main/xiaozhi-mobile-admin/uno.config.ts
new file mode 100644
index 00000000..2ddbd835
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/uno.config.ts
@@ -0,0 +1,65 @@
+// https://www.npmjs.com/package/@uni-helper/unocss-preset-uni
+import { presetUni } from '@uni-helper/unocss-preset-uni'
+import {
+ defineConfig,
+ presetAttributify,
+ presetIcons,
+ transformerDirectives,
+ transformerVariantGroup,
+} from 'unocss'
+
+export default defineConfig({
+ presets: [
+ presetUni({
+ attributify: {
+ // prefix: 'fg-', // 如果加前缀,则需要在代码里面使用 `fg-` 前缀,如:
+ prefixedOnly: true,
+ },
+ }),
+ presetIcons({
+ scale: 1.2,
+ warn: true,
+ extraProperties: {
+ 'display': 'inline-block',
+ 'vertical-align': 'middle',
+ },
+ }),
+ // 支持css class属性化
+ presetAttributify(),
+ ],
+ transformers: [
+ // 启用指令功能:主要用于支持 @apply、@screen 和 theme() 等 CSS 指令
+ transformerDirectives(),
+ // 启用 () 分组功能
+ // 支持css class组合,eg: `测试 unocss
`
+ transformerVariantGroup(),
+ ],
+ shortcuts: [
+ {
+ center: 'flex justify-center items-center',
+ },
+ ],
+ safelist: [],
+ rules: [
+ [
+ 'p-safe',
+ {
+ padding:
+ 'env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)',
+ },
+ ],
+ ['pt-safe', { 'padding-top': 'env(safe-area-inset-top)' }],
+ ['pb-safe', { 'padding-bottom': 'env(safe-area-inset-bottom)' }],
+ ],
+ theme: {
+ colors: {
+ /** 主题色,用法如: text-primary */
+ primary: 'var(--wot-color-theme,#0957DE)',
+ },
+ fontSize: {
+ /** 提供更小号的字体,用法如:text-2xs */
+ '2xs': ['20rpx', '28rpx'],
+ '3xs': ['18rpx', '26rpx'],
+ },
+ },
+})
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/1024x1024.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/1024x1024.png
new file mode 100644
index 00000000..2ceae9c2
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/1024x1024.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/120x120.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/120x120.png
new file mode 100644
index 00000000..3eb78f64
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/120x120.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/144x144.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/144x144.png
new file mode 100644
index 00000000..a1036734
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/144x144.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/152x152.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/152x152.png
new file mode 100644
index 00000000..6829c936
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/152x152.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/167x167.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/167x167.png
new file mode 100644
index 00000000..5e9289b4
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/167x167.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/180x180.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/180x180.png
new file mode 100644
index 00000000..607fc4ac
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/180x180.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/192x192.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/192x192.png
new file mode 100644
index 00000000..4a912736
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/192x192.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/20x20.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/20x20.png
new file mode 100644
index 00000000..f4b0e598
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/20x20.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/29x29.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/29x29.png
new file mode 100644
index 00000000..403fda9f
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/29x29.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/40x40.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/40x40.png
new file mode 100644
index 00000000..88ae47c5
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/40x40.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/58x58.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/58x58.png
new file mode 100644
index 00000000..889b9fff
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/58x58.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/60x60.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/60x60.png
new file mode 100644
index 00000000..6d0b41a5
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/60x60.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/72x72.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/72x72.png
new file mode 100644
index 00000000..6cf11e53
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/72x72.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/76x76.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/76x76.png
new file mode 100644
index 00000000..a1c11df8
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/76x76.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/80x80.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/80x80.png
new file mode 100644
index 00000000..68b1891b
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/80x80.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/87x87.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/87x87.png
new file mode 100644
index 00000000..6ac4e62f
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/87x87.png differ
diff --git a/main/xiaozhi-mobile-admin/unpackage/res/icons/96x96.png b/main/xiaozhi-mobile-admin/unpackage/res/icons/96x96.png
new file mode 100644
index 00000000..2644d325
Binary files /dev/null and b/main/xiaozhi-mobile-admin/unpackage/res/icons/96x96.png differ
diff --git a/main/xiaozhi-mobile-admin/vite.config.ts b/main/xiaozhi-mobile-admin/vite.config.ts
new file mode 100644
index 00000000..b939d205
--- /dev/null
+++ b/main/xiaozhi-mobile-admin/vite.config.ts
@@ -0,0 +1,182 @@
+import path from 'node:path'
+import process from 'node:process'
+import Uni from '@dcloudio/vite-plugin-uni'
+import Components from '@uni-helper/vite-plugin-uni-components'
+// @see https://uni-helper.js.org/vite-plugin-uni-layouts
+import UniLayouts from '@uni-helper/vite-plugin-uni-layouts'
+// @see https://github.com/uni-helper/vite-plugin-uni-manifest
+import UniManifest from '@uni-helper/vite-plugin-uni-manifest'
+// @see https://uni-helper.js.org/vite-plugin-uni-pages
+import UniPages from '@uni-helper/vite-plugin-uni-pages'
+// @see https://github.com/uni-helper/vite-plugin-uni-platform
+// 需要与 @uni-helper/vite-plugin-uni-pages 插件一起使用
+import UniPlatform from '@uni-helper/vite-plugin-uni-platform'
+/**
+ * 分包优化、模块异步跨包调用、组件异步跨包引用
+ * @see https://github.com/uni-ku/bundle-optimizer
+ */
+import Optimization from '@uni-ku/bundle-optimizer'
+import dayjs from 'dayjs'
+import { visualizer } from 'rollup-plugin-visualizer'
+import AutoImport from 'unplugin-auto-import/vite'
+import { defineConfig, loadEnv } from 'vite'
+import ViteRestart from 'vite-plugin-restart'
+
+// https://vitejs.dev/config/
+export default async ({ command, mode }) => {
+ // @see https://unocss.dev/
+ const UnoCSS = (await import('unocss/vite')).default
+ // console.log(mode === process.env.NODE_ENV) // true
+
+ // mode: 区分生产环境还是开发环境
+ console.log('command, mode -> ', command, mode)
+ // pnpm dev:h5 时得到 => serve development
+ // pnpm build:h5 时得到 => build production
+ // pnpm dev:mp-weixin 时得到 => build development (注意区别,command为build)
+ // pnpm build:mp-weixin 时得到 => build production
+ // pnpm dev:app 时得到 => build development (注意区别,command为build)
+ // pnpm build:app 时得到 => build production
+ // dev 和 build 命令可以分别使用 .env.development 和 .env.production 的环境变量
+
+ const { UNI_PLATFORM } = process.env
+ console.log('UNI_PLATFORM -> ', UNI_PLATFORM) // 得到 mp-weixin, h5, app 等
+
+ const env = loadEnv(mode, path.resolve(process.cwd(), 'env'))
+ const {
+ VITE_APP_PORT,
+ VITE_SERVER_BASEURL,
+ VITE_DELETE_CONSOLE,
+ VITE_SHOW_SOURCEMAP,
+ VITE_APP_PUBLIC_BASE,
+ VITE_APP_PROXY,
+ VITE_APP_PROXY_PREFIX,
+ } = env
+ console.log('环境变量 env -> ', env)
+
+ return defineConfig({
+ envDir: './env', // 自定义env目录
+ base: VITE_APP_PUBLIC_BASE,
+ plugins: [
+ UniPages({
+ exclude: ['**/components/**/**.*'],
+ // homePage 通过 vue 文件的 route-block 的type="home"来设定
+ // pages 目录为 src/pages,分包目录不能配置在pages目录下
+ subPackages: ['src/pages-sub'], // 是个数组,可以配置多个,但是不能为pages里面的目录
+ dts: 'src/types/uni-pages.d.ts',
+ }),
+ UniLayouts(),
+ UniPlatform(),
+ UniManifest(),
+ // UniXXX 需要在 Uni 之前引入
+ {
+ // 临时解决 dcloudio 官方的 @dcloudio/uni-mp-compiler 出现的编译 BUG
+ // 参考 github issue: https://github.com/dcloudio/uni-app/issues/4952
+ // 自定义插件禁用 vite:vue 插件的 devToolsEnabled,强制编译 vue 模板时 inline 为 true
+ name: 'fix-vite-plugin-vue',
+ configResolved(config) {
+ const plugin = config.plugins.find(p => p.name === 'vite:vue')
+ if (plugin && plugin.api && plugin.api.options) {
+ plugin.api.options.devToolsEnabled = false
+ }
+ },
+ },
+ UnoCSS(),
+ AutoImport({
+ imports: ['vue', 'uni-app'],
+ dts: 'src/types/auto-import.d.ts',
+ dirs: ['src/hooks'], // 自动导入 hooks
+ vueTemplate: true, // default false
+ }),
+ // Optimization 插件需要 page.json 文件,故应在 UniPages 插件之后执行
+ Optimization({
+ enable: {
+ 'optimization': true,
+ 'async-import': true,
+ 'async-component': true,
+ },
+ dts: {
+ base: 'src/types',
+ },
+ logger: false,
+ }),
+
+ ViteRestart({
+ // 通过这个插件,在修改vite.config.js文件则不需要重新运行也生效配置
+ restart: ['vite.config.js'],
+ }),
+ // h5环境增加 BUILD_TIME 和 BUILD_BRANCH
+ UNI_PLATFORM === 'h5' && {
+ name: 'html-transform',
+ transformIndexHtml(html) {
+ return html.replace('%BUILD_TIME%', dayjs().format('YYYY-MM-DD HH:mm:ss'))
+ },
+ },
+ // 打包分析插件,h5 + 生产环境才弹出
+ UNI_PLATFORM === 'h5'
+ && mode === 'production'
+ && visualizer({
+ filename: './node_modules/.cache/visualizer/stats.html',
+ open: true,
+ gzipSize: true,
+ brotliSize: true,
+ }),
+ // 只有在 app 平台时才启用 copyNativeRes 插件
+ // UNI_PLATFORM === 'app' && copyNativeRes(),
+ Components({
+ extensions: ['vue'],
+ deep: true, // 是否递归扫描子目录,
+ directoryAsNamespace: false, // 是否把目录名作为命名空间前缀,true 时组件名为 目录名+组件名,
+ dts: 'src/types/components.d.ts', // 自动生成的组件类型声明文件路径(用于 TypeScript 支持)
+ }),
+ Uni(),
+ ],
+ define: {
+ __UNI_PLATFORM__: JSON.stringify(UNI_PLATFORM),
+ __VITE_APP_PROXY__: JSON.stringify(VITE_APP_PROXY),
+ },
+ css: {
+ postcss: {
+ plugins: [
+ // autoprefixer({
+ // // 指定目标浏览器
+ // overrideBrowserslist: ['> 1%', 'last 2 versions'],
+ // }),
+ ],
+ },
+ },
+
+ resolve: {
+ alias: {
+ '@': path.join(process.cwd(), './src'),
+ '@img': path.join(process.cwd(), './src/static/images'),
+ },
+ },
+ server: {
+ host: '0.0.0.0',
+ hmr: true,
+ port: Number.parseInt(VITE_APP_PORT, 10),
+ // 仅 H5 端生效,其他端不生效(其他端走build,不走devServer)
+ proxy: JSON.parse(VITE_APP_PROXY)
+ ? {
+ [VITE_APP_PROXY_PREFIX]: {
+ target: VITE_SERVER_BASEURL,
+ changeOrigin: true,
+ rewrite: path => path.replace(new RegExp(`^${VITE_APP_PROXY_PREFIX}`), ''),
+ },
+ }
+ : undefined,
+ },
+ esbuild: {
+ drop: VITE_DELETE_CONSOLE === 'true' ? ['console', 'debugger'] : ['debugger'],
+ },
+ build: {
+ sourcemap: false,
+ // 方便非h5端调试
+ // sourcemap: VITE_SHOW_SOURCEMAP === 'true', // 默认是false
+ target: 'es6',
+ // 开发环境不用压缩
+ minify: mode === 'development' ? false : 'esbuild',
+
+ },
+ })
+}