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](../../docs/images/manager-mobile/生成appid.png) +- 复制生成的 AppID 到环境变量 `VITE_UNI_APPID` 中 + +#### 2. 本地运行步骤 +![本地运行](../../docs/images/manager-mobile/本地运行.png) + +**App 本地调试:** +1. 用 HBuilderX 导入 `manager-mobile` 目录 +2. 重新识别项目 +3. 连接手机或使用模拟器进行真机调试 + +**项目识别问题解决:** +![重新识别项目](../../docs/images/manager-mobile/重新识别项目.png) + +如果 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](../../docs/images/manager-mobile/打包发行步骤1.png) + +1. 确保已配置正确的 `VITE_UNI_APPID` +2. 运行 `pnpm build:app`,产物在 `dist/build/app` +3. 用 HBuilderX 导入项目目录 +4. 在 HBuilderX 中点击"发行" → "原生App-云打包" + +**步骤二:配置打包参数** +![打包发行步骤2](../../docs/images/manager-mobile/打包发行步骤2.png) + +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 @@ + + + + + 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": "分包页面" + } +} + + + + + + + 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 @@ + + +