diff --git a/.gitignore b/.gitignore
index b43a8de3..6cc16b40 100644
--- a/.gitignore
+++ b/.gitignore
@@ -175,3 +175,11 @@ uploadfile
*.json
.vscode
.cursor
+
+!package.json
+!**/package.json
+
+# Do not ignore env and json files inside manager-mobile
+!main/manager-mobile/**/env/
+!main/manager-mobile/**/.env*
+!main/manager-mobile/**/*.json
\ No newline at end of file
diff --git a/docs/images/manager-mobile/打包发行步骤1.png b/docs/images/manager-mobile/打包发行步骤1.png
new file mode 100644
index 00000000..88cabf48
Binary files /dev/null and b/docs/images/manager-mobile/打包发行步骤1.png differ
diff --git a/docs/images/manager-mobile/打包发行步骤2.png b/docs/images/manager-mobile/打包发行步骤2.png
new file mode 100644
index 00000000..747b5ffb
Binary files /dev/null and b/docs/images/manager-mobile/打包发行步骤2.png differ
diff --git a/docs/images/manager-mobile/本地运行.png b/docs/images/manager-mobile/本地运行.png
new file mode 100644
index 00000000..c3c5b6ea
Binary files /dev/null and b/docs/images/manager-mobile/本地运行.png differ
diff --git a/docs/images/manager-mobile/生成appid.png b/docs/images/manager-mobile/生成appid.png
new file mode 100644
index 00000000..73e6357e
Binary files /dev/null and b/docs/images/manager-mobile/生成appid.png differ
diff --git a/docs/images/manager-mobile/重新识别项目.png b/docs/images/manager-mobile/重新识别项目.png
new file mode 100644
index 00000000..0e9c1c9f
Binary files /dev/null and b/docs/images/manager-mobile/重新识别项目.png differ
diff --git a/main/manager-mobile/.commitlintrc.cjs b/main/manager-mobile/.commitlintrc.cjs
new file mode 100644
index 00000000..98ee7dfc
--- /dev/null
+++ b/main/manager-mobile/.commitlintrc.cjs
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: ['@commitlint/config-conventional'],
+}
diff --git a/main/manager-mobile/.editorconfig b/main/manager-mobile/.editorconfig
new file mode 100644
index 00000000..7f098647
--- /dev/null
+++ b/main/manager-mobile/.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/manager-mobile/.gitignore b/main/manager-mobile/.gitignore
new file mode 100644
index 00000000..34ddbdf3
--- /dev/null
+++ b/main/manager-mobile/.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/manager-mobile/.npmrc b/main/manager-mobile/.npmrc
new file mode 100644
index 00000000..10ecfe22
--- /dev/null
+++ b/main/manager-mobile/.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/manager-mobile/LICENSE b/main/manager-mobile/LICENSE
new file mode 100644
index 00000000..8738d3b5
--- /dev/null
+++ b/main/manager-mobile/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Junsen
+
+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/manager-mobile/README.md b/main/manager-mobile/README.md
new file mode 100644
index 00000000..6c767a51
--- /dev/null
+++ b/main/manager-mobile/README.md
@@ -0,0 +1,170 @@
+## 智控台移动版(manager-mobile)
+基于 uni-app v3 + Vue 3 + Vite 的跨端移动管理端,支持 App(Android & iOS)和微信小程序。
+
+### 平台兼容性
+
+| H5 | iOS | Android | 微信小程序 | 字节小程序 | 快手小程序 | 支付宝小程序 | 钉钉小程序 | 百度小程序 |
+| -- | --- | ------- | ---------- | ---------- | ---------- | ------------ | ---------- | ---------- |
+| × | √ | √ | √ | × | × | × | × | × |
+
+提示:不同 UI 组件在不同平台的适配度略有差异,请以对应组件库文档为准。
+
+### 开发环境要求
+- Node >= 18
+- pnpm >= 7.30(建议使用项目中声明的 `pnpm@10.x`)
+- 可选:HBuilderX(App 调试/打包)、微信开发者工具(微信小程序)
+
+### 快速开始
+1) 配置环境变量
+ - 复制 `env/.env.example` 为 `env/.env.development`
+ - 根据实际情况修改配置项(特别是 `VITE_SERVER_BASEURL`、`VITE_UNI_APPID`、`VITE_WX_APPID`)
+
+2) 安装依赖
+
+```bash
+pnpm i
+```
+
+3) 本地开发(热更新)
+- h5: `pnpm dev:h5`,然后观察启动日志显示的ip端口号
+- 微信小程序:`pnpm dev:mp` 或 `pnpm dev:mp-weixin`,然后用微信开发者工具导入 `dist/dev/mp-weixin`
+- App:用 HBuilderX 导入 `manager-mobile`,然后参考下面的教程就能运行了
+
+### 环境变量与配置
+项目使用自定义 `env` 目录存放环境文件,按 Vite 规范命名:`.env.development`、`.env.production` 等。
+
+关键变量(部分):
+- VITE_APP_TITLE:应用名称(写入 `manifest.config.ts`)
+- VITE_UNI_APPID:uni-app 应用 appid(App)
+- VITE_WX_APPID:微信小程序 appid(mp-weixin)
+- VITE_FALLBACK_LOCALE:默认语言,如 `zh-Hans`
+- VITE_SERVER_BASEURL:服务端基础地址(HTTP 请求 baseURL)
+- VITE_DELETE_CONSOLE:构建时是否移除 console(`true`/`false`)
+- VITE_SHOW_SOURCEMAP:是否生成 sourcemap(默认关闭)
+- VITE_LOGIN_URL:未登录跳转的登录页路径(路由拦截器使用)
+
+示例(`env/.env.development`):
+```env
+VITE_APP_TITLE=小智
+VITE_FALLBACK_LOCALE=zh-Hans
+VITE_UNI_APPID=
+VITE_WX_APPID=
+
+VITE_SERVER_BASEURL=http://localhost:8080
+
+VITE_DELETE_CONSOLE=false
+VITE_SHOW_SOURCEMAP=false
+VITE_LOGIN_URL=/pages/login/index
+```
+
+说明:
+- `manifest.config.ts` 会从 `env` 读取标题、appid、语言等配置。
+
+### 重要注意事项
+⚠️ **部署前必须修改的配置项:**
+
+1. **应用 ID 配置**
+ - `VITE_UNI_APPID`:需要在 [DCloud 开发者中心](https://dev.dcloud.net.cn/) 创建应用并获取 AppID
+ - `VITE_WX_APPID`:需要在 [微信公众平台](https://mp.weixin.qq.com/) 注册小程序并获取 AppID
+
+2. **服务端地址**
+ - `VITE_SERVER_BASEURL`:修改为您的实际服务端地址
+
+3. **应用信息**
+ - `VITE_APP_TITLE`:修改为您的应用名称
+ - 更新 `src/static/logo.png` 等图标资源
+
+4. **其他配置**
+ - 检查 `manifest.config.ts` 中的应用配置信息
+ - 根据需要修改 `src/layouts/fg-tabbar/tabbarList.ts` 中的 tabbar 配置
+
+### 详细操作指南
+
+#### 1. 获取 uni-app AppID
+
+- 复制生成的 AppID 到环境变量 `VITE_UNI_APPID` 中
+
+#### 2. 本地运行步骤
+
+
+**App 本地调试:**
+1. 用 HBuilderX 导入 `manager-mobile` 目录
+2. 重新识别项目
+3. 连接手机或使用模拟器进行真机调试
+
+**项目识别问题解决:**
+
+
+如果 HBuilderX 无法正确识别项目类型:
+- 在项目根目录右键选择"重新识别项目类型"
+- 确保项目被识别为 "uni-app" 项目
+
+### 路由与鉴权
+- 在 `src/main.ts` 中注册了路由拦截插件 `routeInterceptor`。
+- 黑名单拦截:仅对配置为需要登录的页面进行校验(来源 `@/utils` 的 `getNeedLoginPages`)。
+- 登录判断:基于用户信息(`pinia` 的 `useUserStore`),未登录将跳转到 `VITE_LOGIN_URL`,并附带重定向回原页面的参数。
+
+### 网络请求
+- 基于 `alova` + `@alova/adapter-uniapp`,统一在 `src/http/request/alova.ts` 创建实例。
+- `baseURL` 读取环境配置(`getEnvBaseUrl`),可通过 `method.config.meta.domain` 动态切换域名。
+- 认证:默认从本地 `token`(`uni.getStorageSync('token')`)注入 `Authorization` 头,缺失则重定向登录。
+- 响应:统一处理 `statusCode !== 200` 的 HTTP 错误与业务 `code !== 0` 的错误;`401` 会清除 token 并跳转登录。
+
+### 构建与发布
+
+**微信小程序:**
+1. 确保已配置正确的 `VITE_WX_APPID`
+2. 运行 `pnpm build:mp`,产物在 `dist/build/mp-weixin`
+3. 用微信开发者工具导入项目目录,并上传代码
+4. 在微信公众平台提交审核
+
+**Android & iOS App:**
+
+#### 3. App 打包发行步骤
+
+**步骤一:准备打包**
+
+
+1. 确保已配置正确的 `VITE_UNI_APPID`
+2. 运行 `pnpm build:app`,产物在 `dist/build/app`
+3. 用 HBuilderX 导入项目目录
+4. 在 HBuilderX 中点击"发行" → "原生App-云打包"
+
+**步骤二:配置打包参数**
+
+
+1. **应用图标和启动图**:上传应用图标和启动页面图片
+2. **应用版本号**:设置版本号和版本名称
+3. **签名证书**:
+ - Android:上传 keystore 证书文件
+ - iOS:配置开发者证书和描述文件
+4. **包名配置**:设置应用的包名(Bundle ID)
+5. **打包类型**:选择测试包或正式包
+6. 点击"打包"开始云打包流程
+
+**发布到应用商店:**
+- **Android**:将生成的 APK 文件上传到各大 Android 应用市场
+- **iOS**:将生成的 IPA 文件通过 App Store Connect 上传到 App Store(需要 Apple 开发者账号)
+
+### 约定与工程化
+- 页面与分包:由 `@uni-helper/vite-plugin-uni-pages` 与 `pages.config.ts` 统一生成;tabbar 配置在 `src/layouts/fg-tabbar/tabbarList.ts`。
+- 组件与 hooks 自动导入:见 `vite.config.ts` 中 `unplugin-auto-import` 与 `@uni-helper/vite-plugin-uni-components`。
+- 样式:使用 UnoCSS 与 `src/style/index.scss`。
+- 状态管理:`pinia` + `pinia-plugin-persistedstate`。
+- 代码规范:内置 `eslint`、`husky`、`lint-staged`,提交前自动格式化(`lint-staged`)。
+
+### 常用脚本
+```bash
+# 开发
+pnpm dev:mp # 等价 dev:mp-weixin
+
+# 构建
+pnpm build:mp # 等价 build:mp-weixin
+
+# 其他
+pnpm type-check
+pnpm lint && pnpm lint:fix
+```
+
+### License
+MIT
diff --git a/main/manager-mobile/env/.env b/main/manager-mobile/env/.env
new file mode 100644
index 00000000..e0f4eea6
--- /dev/null
+++ b/main/manager-mobile/env/.env
@@ -0,0 +1,19 @@
+VITE_APP_TITLE = '小智'
+VITE_APP_PORT = 9000
+
+VITE_UNI_APPID = '__UNI__36A515E'
+VITE_WX_APPID = 'wxa2abb91f64032a2b'
+
+# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
+VITE_APP_PUBLIC_BASE=/
+
+# 登录页面
+VITE_LOGIN_URL = '/pages/login/index'
+# 第一个请求地址
+VITE_SERVER_BASEURL = 'https://2662r3426b.vicp.fun/xiaozhi'
+
+VITE_UPLOAD_BASEURL = '/otaMag/upload'
+
+# h5是否需要配置代理
+VITE_APP_PROXY=true
+VITE_APP_PROXY_PREFIX = '/xiaozhi'
\ No newline at end of file
diff --git a/main/manager-mobile/env/.env.development b/main/manager-mobile/env/.env.development
new file mode 100644
index 00000000..04fa2739
--- /dev/null
+++ b/main/manager-mobile/env/.env.development
@@ -0,0 +1,6 @@
+# 变量必须以 VITE_ 为前缀才能暴露给外部读取
+NODE_ENV = 'development'
+# 是否去除console 和 debugger
+VITE_DELETE_CONSOLE = false
+# 是否开启sourcemap
+VITE_SHOW_SOURCEMAP = true
diff --git a/main/manager-mobile/env/.env.production b/main/manager-mobile/env/.env.production
new file mode 100644
index 00000000..8a1b50ca
--- /dev/null
+++ b/main/manager-mobile/env/.env.production
@@ -0,0 +1,6 @@
+# 变量必须以 VITE_ 为前缀才能暴露给外部读取
+NODE_ENV = 'development'
+# 是否去除console 和 debugger
+VITE_DELETE_CONSOLE = true
+# 是否开启sourcemap
+VITE_SHOW_SOURCEMAP = false
diff --git a/main/manager-mobile/env/.env.test b/main/manager-mobile/env/.env.test
new file mode 100644
index 00000000..e22f765a
--- /dev/null
+++ b/main/manager-mobile/env/.env.test
@@ -0,0 +1,4 @@
+# 变量必须以 VITE_ 为前缀才能暴露给外部读取
+NODE_ENV = 'development'
+# 是否去除console 和 debugger
+VITE_DELETE_CONSOLE = false
diff --git a/main/manager-mobile/eslint.config.mjs b/main/manager-mobile/eslint.config.mjs
new file mode 100644
index 00000000..54ce246d
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/api/agent/agent.ts b/main/manager-mobile/src/api/agent/agent.ts
new file mode 100644
index 00000000..09f352f2
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/api/agent/types.ts b/main/manager-mobile/src/api/agent/types.ts
new file mode 100644
index 00000000..66559a23
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/api/auth.ts b/main/manager-mobile/src/api/auth.ts
new file mode 100644
index 00000000..07cc3fa8
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/api/chat-history/chat-history.ts b/main/manager-mobile/src/api/chat-history/chat-history.ts
new file mode 100644
index 00000000..89c3f1ef
--- /dev/null
+++ b/main/manager-mobile/src/api/chat-history/chat-history.ts
@@ -0,0 +1,60 @@
+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,
+ },
+ cacheFor: {
+ expire: 0,
+ },
+ })
+}
+
+/**
+ * 获取聊天记录详情
+ * @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/manager-mobile/src/api/chat-history/index.ts b/main/manager-mobile/src/api/chat-history/index.ts
new file mode 100644
index 00000000..ecf782a4
--- /dev/null
+++ b/main/manager-mobile/src/api/chat-history/index.ts
@@ -0,0 +1,2 @@
+export * from './chat-history'
+export * from './types'
diff --git a/main/manager-mobile/src/api/chat-history/types.ts b/main/manager-mobile/src/api/chat-history/types.ts
new file mode 100644
index 00000000..b895ed47
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/api/device/device.ts b/main/manager-mobile/src/api/device/device.ts
new file mode 100644
index 00000000..fc9d5f69
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/api/device/index.ts b/main/manager-mobile/src/api/device/index.ts
new file mode 100644
index 00000000..aa5f6179
--- /dev/null
+++ b/main/manager-mobile/src/api/device/index.ts
@@ -0,0 +1,2 @@
+export * from './device'
+export * from './types'
diff --git a/main/manager-mobile/src/api/device/types.ts b/main/manager-mobile/src/api/device/types.ts
new file mode 100644
index 00000000..4b6165c5
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/api/voiceprint/index.ts b/main/manager-mobile/src/api/voiceprint/index.ts
new file mode 100644
index 00000000..af56e69a
--- /dev/null
+++ b/main/manager-mobile/src/api/voiceprint/index.ts
@@ -0,0 +1,2 @@
+export * from './types'
+export * from './voiceprint'
diff --git a/main/manager-mobile/src/api/voiceprint/types.ts b/main/manager-mobile/src/api/voiceprint/types.ts
new file mode 100644
index 00000000..726b1783
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/api/voiceprint/voiceprint.ts b/main/manager-mobile/src/api/voiceprint/voiceprint.ts
new file mode 100644
index 00000000..600696dd
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/components/.gitkeep b/main/manager-mobile/src/components/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/main/manager-mobile/src/components/custom-tabs/index.vue b/main/manager-mobile/src/components/custom-tabs/index.vue
new file mode 100644
index 00000000..f5ef0705
--- /dev/null
+++ b/main/manager-mobile/src/components/custom-tabs/index.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
diff --git a/main/manager-mobile/src/env.d.ts b/main/manager-mobile/src/env.d.ts
new file mode 100644
index 00000000..b4a2c97b
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/hooks/.gitkeep b/main/manager-mobile/src/hooks/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/main/manager-mobile/src/hooks/usePageAuth.ts b/main/manager-mobile/src/hooks/usePageAuth.ts
new file mode 100644
index 00000000..fd006c81
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/hooks/useRequest.ts b/main/manager-mobile/src/hooks/useRequest.ts
new file mode 100644
index 00000000..017a710a
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/hooks/useUpload.ts b/main/manager-mobile/src/hooks/useUpload.ts
new file mode 100644
index 00000000..3080d5a7
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/http/README.md b/main/manager-mobile/src/http/README.md
new file mode 100644
index 00000000..fd34fd52
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/http/request/alova.ts b/main/manager-mobile/src/http/request/alova.ts
new file mode 100644
index 00000000..288e2542
--- /dev/null
+++ b/main/manager-mobile/src/http/request/alova.ts
@@ -0,0 +1,120 @@
+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 { getEnvBaseUrl } from '@/utils'
+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: getEnvBaseUrl(),
+ ...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/manager-mobile/src/http/request/enum.ts b/main/manager-mobile/src/http/request/enum.ts
new file mode 100644
index 00000000..1868fe08
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/http/request/types.ts b/main/manager-mobile/src/http/request/types.ts
new file mode 100644
index 00000000..824aa4f4
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/layouts/default.vue b/main/manager-mobile/src/layouts/default.vue
new file mode 100644
index 00000000..360c6b21
--- /dev/null
+++ b/main/manager-mobile/src/layouts/default.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/main/manager-mobile/src/layouts/fg-tabbar/fg-tabbar.vue b/main/manager-mobile/src/layouts/fg-tabbar/fg-tabbar.vue
new file mode 100644
index 00000000..819aa6fb
--- /dev/null
+++ b/main/manager-mobile/src/layouts/fg-tabbar/fg-tabbar.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/manager-mobile/src/layouts/fg-tabbar/tabbar.md b/main/manager-mobile/src/layouts/fg-tabbar/tabbar.md
new file mode 100644
index 00000000..2485b06d
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/layouts/fg-tabbar/tabbar.ts b/main/manager-mobile/src/layouts/fg-tabbar/tabbar.ts
new file mode 100644
index 00000000..03be03fb
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/layouts/fg-tabbar/tabbarList.ts b/main/manager-mobile/src/layouts/fg-tabbar/tabbarList.ts
new file mode 100644
index 00000000..5273136c
--- /dev/null
+++ b/main/manager-mobile/src/layouts/fg-tabbar/tabbarList.ts
@@ -0,0 +1,76 @@
+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/network.png',
+ selectedIconPath: 'static/tabbar/network_activate.png',
+ pagePath: 'pages/device-config/index',
+ text: '配网',
+ icon: 'i-carbon-network-3',
+ iconType: 'uiLib',
+ },
+ {
+ iconPath: 'static/tabbar/system.png',
+ selectedIconPath: 'static/tabbar/system_activate.png',
+ pagePath: 'pages/settings/index',
+ text: '系统',
+ icon: 'i-carbon-settings',
+ iconType: 'uiLib',
+ },
+]
+
+// 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/manager-mobile/src/layouts/tabbar.vue b/main/manager-mobile/src/layouts/tabbar.vue
new file mode 100644
index 00000000..0c1bb1c6
--- /dev/null
+++ b/main/manager-mobile/src/layouts/tabbar.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/main/manager-mobile/src/main.ts b/main/manager-mobile/src/main.ts
new file mode 100644
index 00000000..ab429500
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/manifest.json b/main/manager-mobile/src/manifest.json
new file mode 100644
index 00000000..67225dc8
--- /dev/null
+++ b/main/manager-mobile/src/manifest.json
@@ -0,0 +1,124 @@
+{
+ "name": "小智",
+ "appid": "__UNI__36A515E",
+ "description": "",
+ "versionName": "1.0.0",
+ "versionCode": "100",
+ "transformPx": false,
+ "app-plus": {
+ "usingComponents": true,
+ "nvueStyleCompiler": "uni-app",
+ "compilerVersion": 3,
+ "splashscreen": {
+ "alwaysShowBeforeRender": true,
+ "waiting": true,
+ "autoclose": true,
+ "delay": 0
+ },
+ "modules": {},
+ "distribute": {
+ "android": {
+ "permissions": [
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ ],
+ "minSdkVersion": 30,
+ "targetSdkVersion": 30,
+ "abiFilters": [
+ "armeabi-v7a",
+ "arm64-v8a"
+ ]
+ },
+ "ios": {},
+ "sdkConfigs": {},
+ "icons": {
+ "android": {
+ "hdpi": "unpackage/res/icons/72x72.png",
+ "xhdpi": "unpackage/res/icons/96x96.png",
+ "xxhdpi": "unpackage/res/icons/144x144.png",
+ "xxxhdpi": "unpackage/res/icons/192x192.png"
+ },
+ "ios": {
+ "appstore": "unpackage/res/icons/1024x1024.png",
+ "ipad": {
+ "app": "unpackage/res/icons/76x76.png",
+ "app@2x": "unpackage/res/icons/152x152.png",
+ "notification": "unpackage/res/icons/20x20.png",
+ "notification@2x": "unpackage/res/icons/40x40.png",
+ "proapp@2x": "unpackage/res/icons/167x167.png",
+ "settings": "unpackage/res/icons/29x29.png",
+ "settings@2x": "unpackage/res/icons/58x58.png",
+ "spotlight": "unpackage/res/icons/40x40.png",
+ "spotlight@2x": "unpackage/res/icons/80x80.png"
+ },
+ "iphone": {
+ "app@2x": "unpackage/res/icons/120x120.png",
+ "app@3x": "unpackage/res/icons/180x180.png",
+ "notification@2x": "unpackage/res/icons/40x40.png",
+ "notification@3x": "unpackage/res/icons/60x60.png",
+ "settings@2x": "unpackage/res/icons/58x58.png",
+ "settings@3x": "unpackage/res/icons/87x87.png",
+ "spotlight@2x": "unpackage/res/icons/80x80.png",
+ "spotlight@3x": "unpackage/res/icons/120x120.png"
+ }
+ }
+ }
+ },
+ "compatible": {
+ "ignoreVersion": true
+ }
+ },
+ "quickapp": {},
+ "mp-weixin": {
+ "appid": "wxa2abb91f64032a2b",
+ "setting": {
+ "urlCheck": false,
+ "es6": true,
+ "minified": true
+ },
+ "usingComponents": true,
+ "optimization": {
+ "subPackages": true
+ },
+ "permission": {
+ "scope.userLocation": {
+ "desc": "WiFi配网功能需要获取位置权限"
+ }
+ },
+ "requiredPrivateInfos": [
+ "getLocation"
+ ]
+ },
+ "mp-alipay": {
+ "usingComponents": true,
+ "styleIsolation": "shared"
+ },
+ "mp-baidu": {
+ "usingComponents": true
+ },
+ "mp-toutiao": {
+ "usingComponents": true
+ },
+ "uniStatistics": {
+ "enable": false
+ },
+ "vueVersion": "3",
+ "h5": {
+ "router": {}
+ }
+}
\ No newline at end of file
diff --git a/main/manager-mobile/src/pages-sub/demo/index.vue b/main/manager-mobile/src/pages-sub/demo/index.vue
new file mode 100644
index 00000000..4231cff4
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/pages.json b/main/manager-mobile/src/pages.json
new file mode 100644
index 00000000..5ddd6f53
--- /dev/null
+++ b/main/manager-mobile/src/pages.json
@@ -0,0 +1,158 @@
+{
+ "globalStyle": {
+ "navigationStyle": "default",
+ "navigationBarTitleText": "小智",
+ "navigationBarBackgroundColor": "#f8f8f8",
+ "navigationBarTextStyle": "black",
+ "backgroundColor": "#FFFFFF"
+ },
+ "easycom": {
+ "autoscan": true,
+ "custom": {
+ "^fg-(.*)": "@/components/fg-$1/fg-$1.vue",
+ "^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue",
+ "^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue"
+ }
+ },
+ "tabBar": {
+ "custom": false,
+ "color": "#e6e6e6",
+ "selectedColor": "#667dea",
+ "backgroundColor": "#fff",
+ "borderStyle": "black",
+ "height": "50px",
+ "fontSize": "10px",
+ "iconWidth": "24px",
+ "spacing": "3px",
+ "list": [
+ {
+ "iconPath": "static/tabbar/robot.png",
+ "selectedIconPath": "static/tabbar/robot_activate.png",
+ "pagePath": "pages/index/index",
+ "text": "首页",
+ "icon": "home",
+ "iconType": "uiLib"
+ },
+ {
+ "iconPath": "static/tabbar/network.png",
+ "selectedIconPath": "static/tabbar/network_activate.png",
+ "pagePath": "pages/device-config/index",
+ "text": "配网",
+ "icon": "i-carbon-network-3",
+ "iconType": "uiLib"
+ },
+ {
+ "iconPath": "static/tabbar/system.png",
+ "selectedIconPath": "static/tabbar/system_activate.png",
+ "pagePath": "pages/settings/index",
+ "text": "系统",
+ "icon": "i-carbon-settings",
+ "iconType": "uiLib"
+ }
+ ]
+ },
+ "pages": [
+ {
+ "path": "pages/index/index",
+ "type": "home",
+ "layout": "tabbar",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "首页"
+ }
+ },
+ {
+ "path": "pages/agent/edit",
+ "type": "page"
+ },
+ {
+ "path": "pages/agent/index",
+ "type": "page",
+ "layout": "default",
+ "style": {
+ "navigationBarTitleText": "智能体",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/agent/tools",
+ "type": "page",
+ "layout": "default",
+ "style": {
+ "navigationBarTitleText": "编辑功能",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/chat-history/detail",
+ "type": "page",
+ "layout": "default",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "聊天详情"
+ }
+ },
+ {
+ "path": "pages/chat-history/index",
+ "type": "page"
+ },
+ {
+ "path": "pages/device/index",
+ "type": "page"
+ },
+ {
+ "path": "pages/device-config/index",
+ "type": "page",
+ "style": {
+ "navigationBarTitleText": "设备配网",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/login/index",
+ "type": "page",
+ "layout": "default",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "登陆"
+ }
+ },
+ {
+ "path": "pages/register/index",
+ "type": "page",
+ "layout": "default",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "注册"
+ }
+ },
+ {
+ "path": "pages/settings/index",
+ "type": "page",
+ "layout": "default",
+ "style": {
+ "navigationBarTitleText": "设置",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/voiceprint/index",
+ "type": "page"
+ }
+ ],
+ "subPackages": [
+ {
+ "root": "pages-sub",
+ "pages": [
+ {
+ "path": "demo/index",
+ "type": "page",
+ "layout": "default",
+ "style": {
+ "navigationBarTitleText": "分包页面"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/main/manager-mobile/src/pages/agent/edit.vue b/main/manager-mobile/src/pages/agent/edit.vue
new file mode 100644
index 00000000..36809fa6
--- /dev/null
+++ b/main/manager-mobile/src/pages/agent/edit.vue
@@ -0,0 +1,701 @@
+
+
+
+
+
+
+
+ 基础信息
+
+
+
+
+
+
+
+ 助手昵称
+
+
+
+
+
+
+ 角色模式
+
+
+
+ {{ 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/manager-mobile/src/pages/agent/index.vue b/main/manager-mobile/src/pages/agent/index.vue
new file mode 100644
index 00000000..9e81b663
--- /dev/null
+++ b/main/manager-mobile/src/pages/agent/index.vue
@@ -0,0 +1,214 @@
+
+{
+ "layout": "default",
+ "style": {
+ "navigationBarTitleText": "智能体",
+ "navigationStyle": "custom"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/manager-mobile/src/pages/agent/tools.vue b/main/manager-mobile/src/pages/agent/tools.vue
new file mode 100644
index 00000000..88ca36fc
--- /dev/null
+++ b/main/manager-mobile/src/pages/agent/tools.vue
@@ -0,0 +1,568 @@
+
+{
+ "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/manager-mobile/src/pages/chat-history/detail.vue b/main/manager-mobile/src/pages/chat-history/detail.vue
new file mode 100644
index 00000000..2658749a
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/pages/chat-history/index.vue b/main/manager-mobile/src/pages/chat-history/index.vue
new file mode 100644
index 00000000..ed71afab
--- /dev/null
+++ b/main/manager-mobile/src/pages/chat-history/index.vue
@@ -0,0 +1,399 @@
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 共 {{ session.chatCount }} 条对话
+
+
+
+
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+ 没有更多数据了
+
+
+
+
+
+
+
+
+ 暂无聊天记录
+
+
+ 与智能体的对话记录会显示在这里
+
+
+
+
+
+
diff --git a/main/manager-mobile/src/pages/device-config/components/ultrasonic-config.vue b/main/manager-mobile/src/pages/device-config/components/ultrasonic-config.vue
new file mode 100644
index 00000000..8f1ae7e1
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/pages/device-config/components/wifi-config.vue b/main/manager-mobile/src/pages/device-config/components/wifi-config.vue
new file mode 100644
index 00000000..ac54b6b6
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/pages/device-config/components/wifi-selector.vue b/main/manager-mobile/src/pages/device-config/components/wifi-selector.vue
new file mode 100644
index 00000000..a91d734b
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/pages/device-config/index.vue b/main/manager-mobile/src/pages/device-config/index.vue
new file mode 100644
index 00000000..8e534d16
--- /dev/null
+++ b/main/manager-mobile/src/pages/device-config/index.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+ 配网方式
+
+
+
+
+
+
+ 配网方式
+
+
+ {{ configType === 'wifi' ? 'WiFi配网' : '超声波配网' }}
+
+
+
+
+
+
+
+
+ 网络配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ onConfigTypeConfirm(item)"
+ />
+
+
+
+
+{
+ "style": {
+ "navigationBarTitleText": "设备配网",
+ "navigationStyle": "custom"
+ }
+}
+
diff --git a/main/manager-mobile/src/pages/device/index.vue b/main/manager-mobile/src/pages/device/index.vue
new file mode 100644
index 00000000..c12c5607
--- /dev/null
+++ b/main/manager-mobile/src/pages/device/index.vue
@@ -0,0 +1,333 @@
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getDeviceTypeName(device.board) }}
+
+
+
+
+
+ MAC地址:{{ device.macAddress }}
+
+
+ 固件版本:{{ device.appVersion }}
+
+
+ 最近对话:{{ formatTime(device.lastConnectedAt) }}
+
+
+
+
+
+ OTA升级
+
+
+
+
+
+
+
+
+
+
+
+ 解绑
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无设备
+
+
+ 点击右下角 + 号绑定您的第一个设备
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/manager-mobile/src/pages/index/index.vue b/main/manager-mobile/src/pages/index/index.vue
new file mode 100644
index 00000000..bb74ab1c
--- /dev/null
+++ b/main/manager-mobile/src/pages/index/index.vue
@@ -0,0 +1,578 @@
+
+
+{
+ "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/manager-mobile/src/pages/login/index.vue b/main/manager-mobile/src/pages/login/index.vue
new file mode 100644
index 00000000..225c5da0
--- /dev/null
+++ b/main/manager-mobile/src/pages/login/index.vue
@@ -0,0 +1,778 @@
+
+{
+ "layout": "default",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "登陆"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ selectedAreaCode }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 忘记密码?
+
+
+
+
+ {{ loading ? '登录中...' : '登录' }}
+
+
+
+
+ 还没有账户?
+
+
+ 立即注册
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+ {{ item.key }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/manager-mobile/src/pages/register/index.vue b/main/manager-mobile/src/pages/register/index.vue
new file mode 100644
index 00000000..91463e1d
--- /dev/null
+++ b/main/manager-mobile/src/pages/register/index.vue
@@ -0,0 +1,878 @@
+
+{
+ "layout": "default",
+ "style": {
+ "navigationStyle": "custom",
+ "navigationBarTitleText": "注册"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ selectedAreaCode }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
+
+
+
+
+
+ {{ loading ? '注册中...' : '注册' }}
+
+
+
+
+ 已有账户?
+
+
+ 立即登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+ {{ item.key }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/manager-mobile/src/pages/settings/index.vue b/main/manager-mobile/src/pages/settings/index.vue
new file mode 100644
index 00000000..bcc3ced1
--- /dev/null
+++ b/main/manager-mobile/src/pages/settings/index.vue
@@ -0,0 +1,307 @@
+{
+ "layout": "default",
+ "style": {
+ "navigationBarTitleText": "设置",
+ "navigationStyle": "custom"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+ 网络设置
+
+
+
+
+
+
+ 服务端接口地址
+
+
+ 修改后将自动清空缓存并重启应用
+
+
+
+
+
+
+
+
+
+ 保存设置
+
+
+ 恢复默认
+
+
+
+
+
+
+
+
+
+ 缓存管理
+
+
+
+
+
+
+
+
+
+ 总缓存大小
+
+
+ 应用数据总大小
+
+
+
+ {{ cacheInfo.storageSize }}
+
+
+
+
+
+
+
+ 缓存清理
+
+
+ 清空所有缓存数据
+
+
+
+ 清除缓存
+
+
+
+
+
+
+
+
+
+
+ 应用信息
+
+
+
+
+
+
+
+ 关于我们
+
+
+ 应用版本与团队信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/manager-mobile/src/pages/voiceprint/index.vue b/main/manager-mobile/src/pages/voiceprint/index.vue
new file mode 100644
index 00000000..5fe8bbad
--- /dev/null
+++ b/main/manager-mobile/src/pages/voiceprint/index.vue
@@ -0,0 +1,512 @@
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.sourceName }}
+
+
+ {{ item.introduce || '暂无描述' }}
+
+
+ {{ item.createDate }}
+
+
+
+
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无声纹数据
+
+
+ 点击右下角 + 号添加您的第一个说话人
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加说话人
+
+
+
+
+
+
+
+ * 声纹向量
+
+
+
+ {{ getSelectedAudioContent(addForm.audioId) }}
+
+
+
+
+
+
+
+
+ * 姓名
+
+
+
+
+
+
+
+ * 描述
+
+
+
+ {{ (addForm.introduce || '').length }}/100
+
+
+
+
+
+
+ 取消
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+
+ 编辑说话人
+
+
+
+
+
+
+
+ * 声纹向量
+
+
+
+ {{ getSelectedAudioContent(editForm.audioId) }}
+
+
+
+
+
+
+
+
+ * 姓名
+
+
+
+
+
+
+
+ * 描述
+
+
+
+ {{ (editForm.introduce || '').length }}/100
+
+
+
+
+
+
+ 取消
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+
diff --git a/main/manager-mobile/src/router/interceptor.ts b/main/manager-mobile/src/router/interceptor.ts
new file mode 100644
index 00000000..74905a64
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/static/app/icons/1024x1024.png b/main/manager-mobile/src/static/app/icons/1024x1024.png
new file mode 100644
index 00000000..2ceae9c2
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/1024x1024.png differ
diff --git a/main/manager-mobile/src/static/app/icons/120x120.png b/main/manager-mobile/src/static/app/icons/120x120.png
new file mode 100644
index 00000000..3eb78f64
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/120x120.png differ
diff --git a/main/manager-mobile/src/static/app/icons/144x144.png b/main/manager-mobile/src/static/app/icons/144x144.png
new file mode 100644
index 00000000..a1036734
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/144x144.png differ
diff --git a/main/manager-mobile/src/static/app/icons/152x152.png b/main/manager-mobile/src/static/app/icons/152x152.png
new file mode 100644
index 00000000..6829c936
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/152x152.png differ
diff --git a/main/manager-mobile/src/static/app/icons/167x167.png b/main/manager-mobile/src/static/app/icons/167x167.png
new file mode 100644
index 00000000..5e9289b4
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/167x167.png differ
diff --git a/main/manager-mobile/src/static/app/icons/180x180.png b/main/manager-mobile/src/static/app/icons/180x180.png
new file mode 100644
index 00000000..607fc4ac
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/180x180.png differ
diff --git a/main/manager-mobile/src/static/app/icons/192x192.png b/main/manager-mobile/src/static/app/icons/192x192.png
new file mode 100644
index 00000000..4a912736
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/192x192.png differ
diff --git a/main/manager-mobile/src/static/app/icons/20x20.png b/main/manager-mobile/src/static/app/icons/20x20.png
new file mode 100644
index 00000000..f4b0e598
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/20x20.png differ
diff --git a/main/manager-mobile/src/static/app/icons/29x29.png b/main/manager-mobile/src/static/app/icons/29x29.png
new file mode 100644
index 00000000..403fda9f
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/29x29.png differ
diff --git a/main/manager-mobile/src/static/app/icons/40x40.png b/main/manager-mobile/src/static/app/icons/40x40.png
new file mode 100644
index 00000000..88ae47c5
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/40x40.png differ
diff --git a/main/manager-mobile/src/static/app/icons/58x58.png b/main/manager-mobile/src/static/app/icons/58x58.png
new file mode 100644
index 00000000..889b9fff
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/58x58.png differ
diff --git a/main/manager-mobile/src/static/app/icons/60x60.png b/main/manager-mobile/src/static/app/icons/60x60.png
new file mode 100644
index 00000000..6d0b41a5
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/60x60.png differ
diff --git a/main/manager-mobile/src/static/app/icons/72x72.png b/main/manager-mobile/src/static/app/icons/72x72.png
new file mode 100644
index 00000000..6cf11e53
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/72x72.png differ
diff --git a/main/manager-mobile/src/static/app/icons/76x76.png b/main/manager-mobile/src/static/app/icons/76x76.png
new file mode 100644
index 00000000..a1c11df8
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/76x76.png differ
diff --git a/main/manager-mobile/src/static/app/icons/80x80.png b/main/manager-mobile/src/static/app/icons/80x80.png
new file mode 100644
index 00000000..68b1891b
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/80x80.png differ
diff --git a/main/manager-mobile/src/static/app/icons/87x87.png b/main/manager-mobile/src/static/app/icons/87x87.png
new file mode 100644
index 00000000..6ac4e62f
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/87x87.png differ
diff --git a/main/manager-mobile/src/static/app/icons/96x96.png b/main/manager-mobile/src/static/app/icons/96x96.png
new file mode 100644
index 00000000..2644d325
Binary files /dev/null and b/main/manager-mobile/src/static/app/icons/96x96.png differ
diff --git a/main/manager-mobile/src/static/images/.gitkeep b/main/manager-mobile/src/static/images/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/main/manager-mobile/src/static/images/default-avatar.png b/main/manager-mobile/src/static/images/default-avatar.png
new file mode 100644
index 00000000..4eb58795
Binary files /dev/null and b/main/manager-mobile/src/static/images/default-avatar.png differ
diff --git a/main/manager-mobile/src/static/images/home/main-top-bg.png b/main/manager-mobile/src/static/images/home/main-top-bg.png
new file mode 100644
index 00000000..e56ba941
Binary files /dev/null and b/main/manager-mobile/src/static/images/home/main-top-bg.png differ
diff --git a/main/manager-mobile/src/static/logo.png b/main/manager-mobile/src/static/logo.png
new file mode 100644
index 00000000..9e664970
Binary files /dev/null and b/main/manager-mobile/src/static/logo.png differ
diff --git a/main/manager-mobile/src/static/logo.svg b/main/manager-mobile/src/static/logo.svg
new file mode 100644
index 00000000..eaee6698
--- /dev/null
+++ b/main/manager-mobile/src/static/logo.svg
@@ -0,0 +1,33 @@
+
+
diff --git a/main/manager-mobile/src/static/tabbar/chat.png b/main/manager-mobile/src/static/tabbar/chat.png
new file mode 100644
index 00000000..90c4338c
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/chat.png differ
diff --git a/main/manager-mobile/src/static/tabbar/chat_activate.png b/main/manager-mobile/src/static/tabbar/chat_activate.png
new file mode 100644
index 00000000..78d38e7c
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/chat_activate.png differ
diff --git a/main/manager-mobile/src/static/tabbar/device.png b/main/manager-mobile/src/static/tabbar/device.png
new file mode 100644
index 00000000..2d624f47
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/device.png differ
diff --git a/main/manager-mobile/src/static/tabbar/device_activate.png b/main/manager-mobile/src/static/tabbar/device_activate.png
new file mode 100644
index 00000000..1b8f96f8
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/device_activate.png differ
diff --git a/main/manager-mobile/src/static/tabbar/example.png b/main/manager-mobile/src/static/tabbar/example.png
new file mode 100644
index 00000000..fd1e942b
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/example.png differ
diff --git a/main/manager-mobile/src/static/tabbar/exampleHL.png b/main/manager-mobile/src/static/tabbar/exampleHL.png
new file mode 100644
index 00000000..7501011a
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/exampleHL.png differ
diff --git a/main/manager-mobile/src/static/tabbar/home.png b/main/manager-mobile/src/static/tabbar/home.png
new file mode 100644
index 00000000..8f82e213
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/home.png differ
diff --git a/main/manager-mobile/src/static/tabbar/homeHL.png b/main/manager-mobile/src/static/tabbar/homeHL.png
new file mode 100644
index 00000000..26d37613
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/homeHL.png differ
diff --git a/main/manager-mobile/src/static/tabbar/microphone.png b/main/manager-mobile/src/static/tabbar/microphone.png
new file mode 100644
index 00000000..bde9d13b
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/microphone.png differ
diff --git a/main/manager-mobile/src/static/tabbar/microphone_activate.png b/main/manager-mobile/src/static/tabbar/microphone_activate.png
new file mode 100644
index 00000000..c6b6c416
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/microphone_activate.png differ
diff --git a/main/manager-mobile/src/static/tabbar/network.png b/main/manager-mobile/src/static/tabbar/network.png
new file mode 100644
index 00000000..d06283fa
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/network.png differ
diff --git a/main/manager-mobile/src/static/tabbar/network_activate.png b/main/manager-mobile/src/static/tabbar/network_activate.png
new file mode 100644
index 00000000..753a3f13
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/network_activate.png differ
diff --git a/main/manager-mobile/src/static/tabbar/personal.png b/main/manager-mobile/src/static/tabbar/personal.png
new file mode 100644
index 00000000..0a569a2f
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/personal.png differ
diff --git a/main/manager-mobile/src/static/tabbar/personalHL.png b/main/manager-mobile/src/static/tabbar/personalHL.png
new file mode 100644
index 00000000..8c3e66e8
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/personalHL.png differ
diff --git a/main/manager-mobile/src/static/tabbar/robot.png b/main/manager-mobile/src/static/tabbar/robot.png
new file mode 100644
index 00000000..ffe1db27
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/robot.png differ
diff --git a/main/manager-mobile/src/static/tabbar/robot_activate.png b/main/manager-mobile/src/static/tabbar/robot_activate.png
new file mode 100644
index 00000000..aa5c3055
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/robot_activate.png differ
diff --git a/main/manager-mobile/src/static/tabbar/system.png b/main/manager-mobile/src/static/tabbar/system.png
new file mode 100644
index 00000000..a84c9aa7
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/system.png differ
diff --git a/main/manager-mobile/src/static/tabbar/system_activate.png b/main/manager-mobile/src/static/tabbar/system_activate.png
new file mode 100644
index 00000000..b596f3c2
Binary files /dev/null and b/main/manager-mobile/src/static/tabbar/system_activate.png differ
diff --git a/main/manager-mobile/src/store/config.ts b/main/manager-mobile/src/store/config.ts
new file mode 100644
index 00000000..cb20b5fe
--- /dev/null
+++ b/main/manager-mobile/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: import.meta.env.VITE_APP_TITLE,
+}
+
+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/manager-mobile/src/store/index.ts b/main/manager-mobile/src/store/index.ts
new file mode 100644
index 00000000..d960c0a4
--- /dev/null
+++ b/main/manager-mobile/src/store/index.ts
@@ -0,0 +1,19 @@
+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 './config'
+export * from './plugin'
+// 模块统一导出
+export * from './user'
diff --git a/main/manager-mobile/src/store/plugin.ts b/main/manager-mobile/src/store/plugin.ts
new file mode 100644
index 00000000..200b0850
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/store/user.ts b/main/manager-mobile/src/store/user.ts
new file mode 100644
index 00000000..47f6d80c
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/style/iconfont.css b/main/manager-mobile/src/style/iconfont.css
new file mode 100644
index 00000000..35da86c6
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/style/index.scss b/main/manager-mobile/src/style/index.scss
new file mode 100644
index 00000000..e1d33c59
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/typings.d.ts b/main/manager-mobile/src/typings.d.ts
new file mode 100644
index 00000000..9ead3fb5
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/typings.ts b/main/manager-mobile/src/typings.ts
new file mode 100644
index 00000000..b48b6304
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/uni.scss b/main/manager-mobile/src/uni.scss
new file mode 100644
index 00000000..21b9e5fc
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/utils/index.ts b/main/manager-mobile/src/utils/index.ts
new file mode 100644
index 00000000..4d173825
--- /dev/null
+++ b/main/manager-mobile/src/utils/index.ts
@@ -0,0 +1,198 @@
+import { pages, subPackages } from '@/pages.json'
+import { isMpWeixin } from './platform'
+
+/**
+ * 运行时服务端地址覆盖存储键
+ */
+export const SERVER_BASE_URL_OVERRIDE_KEY = 'server_base_url_override'
+
+/**
+ * 设置/清除/获取 运行时覆盖的服务端地址
+ */
+export function setServerBaseUrlOverride(url: string) {
+ uni.setStorageSync(SERVER_BASE_URL_OVERRIDE_KEY, url)
+}
+
+export function clearServerBaseUrlOverride() {
+ uni.removeStorageSync(SERVER_BASE_URL_OVERRIDE_KEY)
+}
+
+export function getServerBaseUrlOverride(): string | null {
+ const value = uni.getStorageSync(SERVER_BASE_URL_OVERRIDE_KEY)
+ return value || null
+}
+
+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() {
+ // 若存在用户设置的覆盖地址,优先返回
+ const override = getServerBaseUrlOverride()
+ if (override)
+ return override
+
+ // 请求基准地址(默认来源于 env)
+ 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/manager-mobile/src/utils/platform.ts b/main/manager-mobile/src/utils/platform.ts
new file mode 100644
index 00000000..c714199e
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/utils/toast.ts b/main/manager-mobile/src/utils/toast.ts
new file mode 100644
index 00000000..e524b000
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/src/utils/uploadFile.ts b/main/manager-mobile/src/utils/uploadFile.ts
new file mode 100644
index 00000000..18cf5e92
--- /dev/null
+++ b/main/manager-mobile/src/utils/uploadFile.ts
@@ -0,0 +1,327 @@
+import { getEnvBaseUrl } from './index'
+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 = {
+ /** 用户头像上传地址(动态读取当前生效的 BaseURL) */
+ get USER_AVATAR() {
+ return `${getEnvBaseUrl()}/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/manager-mobile/tsconfig.json b/main/manager-mobile/tsconfig.json
new file mode 100644
index 00000000..22a48c5d
--- /dev/null
+++ b/main/manager-mobile/tsconfig.json
@@ -0,0 +1,41 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "lib": ["esnext", "dom"],
+ "baseUrl": ".",
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "paths": {
+ "@/*": ["./src/*"],
+ "@img/*": ["./src/static/*"]
+ },
+ "resolveJsonModule": true,
+ "types": [
+ "@dcloudio/types",
+ "@uni-helper/uni-types",
+ "@types/wechat-miniprogram",
+ "wot-design-uni/global.d.ts",
+ "z-paging/types",
+ "./src/typings.d.ts"
+ ],
+ "allowJs": true,
+ "noImplicitThis": true,
+ "outDir": "dist",
+ "sourceMap": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true
+ },
+ "vueCompilerOptions": {
+ "plugins": ["@uni-helper/uni-types/volar-plugin"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.js",
+ "src/**/*.d.ts",
+ "src/**/*.tsx",
+ "src/**/*.jsx",
+ "src/**/*.vue",
+ "src/**/*.json"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/main/manager-mobile/uno.config.ts b/main/manager-mobile/uno.config.ts
new file mode 100644
index 00000000..2ddbd835
--- /dev/null
+++ b/main/manager-mobile/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/manager-mobile/unpackage/res/icons/1024x1024.png b/main/manager-mobile/unpackage/res/icons/1024x1024.png
new file mode 100644
index 00000000..2ceae9c2
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/1024x1024.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/120x120.png b/main/manager-mobile/unpackage/res/icons/120x120.png
new file mode 100644
index 00000000..3eb78f64
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/120x120.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/144x144.png b/main/manager-mobile/unpackage/res/icons/144x144.png
new file mode 100644
index 00000000..a1036734
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/144x144.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/152x152.png b/main/manager-mobile/unpackage/res/icons/152x152.png
new file mode 100644
index 00000000..6829c936
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/152x152.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/167x167.png b/main/manager-mobile/unpackage/res/icons/167x167.png
new file mode 100644
index 00000000..5e9289b4
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/167x167.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/180x180.png b/main/manager-mobile/unpackage/res/icons/180x180.png
new file mode 100644
index 00000000..607fc4ac
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/180x180.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/192x192.png b/main/manager-mobile/unpackage/res/icons/192x192.png
new file mode 100644
index 00000000..4a912736
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/192x192.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/20x20.png b/main/manager-mobile/unpackage/res/icons/20x20.png
new file mode 100644
index 00000000..f4b0e598
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/20x20.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/29x29.png b/main/manager-mobile/unpackage/res/icons/29x29.png
new file mode 100644
index 00000000..403fda9f
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/29x29.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/40x40.png b/main/manager-mobile/unpackage/res/icons/40x40.png
new file mode 100644
index 00000000..88ae47c5
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/40x40.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/58x58.png b/main/manager-mobile/unpackage/res/icons/58x58.png
new file mode 100644
index 00000000..889b9fff
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/58x58.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/60x60.png b/main/manager-mobile/unpackage/res/icons/60x60.png
new file mode 100644
index 00000000..6d0b41a5
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/60x60.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/72x72.png b/main/manager-mobile/unpackage/res/icons/72x72.png
new file mode 100644
index 00000000..6cf11e53
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/72x72.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/76x76.png b/main/manager-mobile/unpackage/res/icons/76x76.png
new file mode 100644
index 00000000..a1c11df8
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/76x76.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/80x80.png b/main/manager-mobile/unpackage/res/icons/80x80.png
new file mode 100644
index 00000000..68b1891b
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/80x80.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/87x87.png b/main/manager-mobile/unpackage/res/icons/87x87.png
new file mode 100644
index 00000000..6ac4e62f
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/87x87.png differ
diff --git a/main/manager-mobile/unpackage/res/icons/96x96.png b/main/manager-mobile/unpackage/res/icons/96x96.png
new file mode 100644
index 00000000..2644d325
Binary files /dev/null and b/main/manager-mobile/unpackage/res/icons/96x96.png differ
diff --git a/main/manager-mobile/vite.config.ts b/main/manager-mobile/vite.config.ts
new file mode 100644
index 00000000..b939d205
--- /dev/null
+++ b/main/manager-mobile/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',
+
+ },
+ })
+}