From 96a0b12582c021d52e7c80bf1f302c6f88131b22 Mon Sep 17 00:00:00 2001 From: TongTongStudio Date: Fri, 7 Aug 2026 12:19:46 +0800 Subject: [PATCH] =?UTF-8?q?chore(deps):=20=E6=96=B0=E5=A2=9E=20apk-meta-pa?= =?UTF-8?q?rser=E3=80=81jszip=E3=80=81spark-md5=20=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=BA=94=E7=94=A8=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E5=92=8C=E5=AE=89=E8=A3=85=E5=8C=85=20API=EF=BC=8C=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E8=AE=BE=E5=A4=87=E8=AF=A6=E6=83=85=E6=8A=BD=E5=B1=89?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 6 +- pnpm-lock.yaml | 35 + src/api/app/category.ts | 83 + src/api/app/package.ts | 126 ++ src/api/system/device/types.ts | 2 + src/assets/icons/android.svg | 1 + src/assets/icons/appstore.svg | 1 + src/components/DeviceSn/DeviceSnDetail.vue | 1864 +---------------- src/utils/request.ts | 155 +- src/views/app-manager/category/index.vue | 292 +++ .../components/UploadDialog.vue | 479 +++++ .../app-manager/upload-package/index.vue | 570 +++++ src/views/devices/sn/config/add.ts | 141 +- src/views/devices/sn/config/edit.ts | 140 +- src/views/devices/sn/index.vue | 9 +- vite.config.ts | 9 + 16 files changed, 1915 insertions(+), 1998 deletions(-) create mode 100644 src/api/app/category.ts create mode 100644 src/api/app/package.ts create mode 100644 src/assets/icons/android.svg create mode 100644 src/assets/icons/appstore.svg create mode 100644 src/views/app-manager/category/index.vue create mode 100644 src/views/app-manager/upload-package/components/UploadDialog.vue create mode 100644 src/views/app-manager/upload-package/index.vue diff --git a/package.json b/package.json index f41aca07..f50a4945 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,9 @@ "@vueuse/core": "^14.3.0", "@wangeditor-next/editor": "^5.7.12", "@wangeditor-next/editor-for-vue": "^5.1.14", + "apk-meta-parser": "^0.2.3", + "jszip": "^3.10.1", + "spark-md5": "^3.0.2", "animate.css": "^4.1.1", "axios": "^1.18.0", "codemirror": "^5.65.21", @@ -125,5 +128,6 @@ }, "repository": "https://gitee.com/youlaiorg/vue3-element-admin.git", "author": "有来开源组织", - "license": "MIT" + "license": "MIT", + "__npminstall_done": false } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d4231f8..1a36d72b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: animate.css: specifier: ^4.1.1 version: 4.1.1 + apk-meta-parser: + specifier: ^0.2.3 + version: 0.2.3(jszip@3.10.1)(spark-md5@3.0.2) axios: specifier: ^1.18.0 version: 1.18.1 @@ -41,6 +44,9 @@ importers: exceljs: specifier: ^4.4.0 version: 4.4.0 + jszip: + specifier: ^3.10.1 + version: 3.10.1 lodash-es: specifier: ^4.18.1 version: 4.18.1 @@ -65,6 +71,9 @@ importers: sortablejs: specifier: ^1.15.7 version: 1.15.7 + spark-md5: + specifier: ^3.0.2 + version: 3.0.2 vue: specifier: ^3.5.38 version: 3.5.38(typescript@5.9.3) @@ -1052,6 +1061,11 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/rollup-win32-x64-msvc@4.62.4': + resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + cpu: [x64] + os: [win32] + '@simple-libs/child-process-utils@1.0.2': resolution: {integrity: sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==} engines: {node: '>=18'} @@ -1506,6 +1520,12 @@ packages: resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} engines: {node: '>=14'} + apk-meta-parser@0.2.3: + resolution: {integrity: sha512-wLut0LlIxCK/Ka8rSIddWoRsp31P+/40v/5eEB/nxRX1lOvFIo+XkAGUsaqiGYaPKxkMO3u4+HATwkcEvltQ6Q==} + peerDependencies: + jszip: '>=3.7.0' + spark-md5: '>=3.0.0' + archiver-utils@2.1.0: resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} engines: {node: '>= 6'} @@ -3421,6 +3441,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + spark-md5@3.0.2: + resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} + speakingurl@14.0.1: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} @@ -4741,6 +4764,9 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rollup/rollup-win32-x64-msvc@4.62.4': + optional: true + '@simple-libs/child-process-utils@1.0.2': dependencies: '@simple-libs/stream-utils': 1.2.0 @@ -5353,6 +5379,13 @@ snapshots: ansis@4.3.1: {} + apk-meta-parser@0.2.3(jszip@3.10.1)(spark-md5@3.0.2): + dependencies: + jszip: 3.10.1 + spark-md5: 3.0.2 + optionalDependencies: + '@rollup/rollup-win32-x64-msvc': 4.62.4 + archiver-utils@2.1.0: dependencies: glob: 7.2.3 @@ -7258,6 +7291,8 @@ snapshots: source-map@0.6.1: {} + spark-md5@3.0.2: {} + speakingurl@14.0.1: {} ssr-window@4.0.2: {} diff --git a/src/api/app/category.ts b/src/api/app/category.ts new file mode 100644 index 00000000..5f99b4f9 --- /dev/null +++ b/src/api/app/category.ts @@ -0,0 +1,83 @@ +import request from "@/utils/request"; + +/** 应用分类对象 */ +export interface AppCategory { + id: string; + code: string; + name: string; + icon: string; + description: string; + sort: number; + createTime?: string; + updateTime?: string; +} + +/** 分类表单(新增/更新) */ +export interface AppCategoryForm { + code: string; + name: string; + icon?: string; + description?: string; + sort?: number; +} + +const BASE_URL = "/api/v1/app/category"; + +/** 应用分类管理 API */ +export const AppCategoryAPI = { + /** 分类列表(全部) */ + list() { + return request({ + url: `${BASE_URL}/list`, + method: "get", + }); + }, + + /** 分类详情 */ + detail(id: string) { + return request({ + url: `${BASE_URL}/${id}`, + method: "get", + }); + }, + + /** 新增分类 */ + create(data: AppCategoryForm) { + return request({ + url: `${BASE_URL}`, + method: "post", + data, + }); + }, + + /** 更新分类 */ + update(id: string, data: AppCategoryForm) { + return request({ + url: `${BASE_URL}/${id}`, + method: "put", + data, + }); + }, + + /** 删除分类 */ + delete(id: string) { + return request({ + url: `${BASE_URL}/${id}`, + method: "delete", + }); + }, + + /** 上传分类图标(手动上传图片,返回访问 URL) */ + uploadIcon(file: File) { + const formData = new FormData(); + formData.append("file", file); + return request({ + url: `${BASE_URL}/icon/upload`, + method: "post", + data: formData, + headers: { "Content-Type": "multipart/form-data" }, + }); + }, +}; + +export default AppCategoryAPI; diff --git a/src/api/app/package.ts b/src/api/app/package.ts new file mode 100644 index 00000000..4ee52f8a --- /dev/null +++ b/src/api/app/package.ts @@ -0,0 +1,126 @@ +import request, { uploadHttp } from "@/utils/request"; +import type { PageResult } from "@/api/common"; + +/** 安装包类型 */ +export type PackageType = "apk" | "apks" | "aab" | "apkm" | "xapk"; + +/** 应用安装包资源对象 */ +export interface AppPackage { + id: string; + packageType: PackageType; + originalName: string; + fileUrl: string; + fileSize: number; + fileSizeText: string; + md5: string; + packageName: string; + appLabel: string; + versionName: string; + versionCode: number; + minSdk: number; + targetSdk: number; + compileSdk: number; + abiList: string[]; + permissions: string[]; + iconUrl: string; + /** 应用分类ID列表(多选) */ + categories: string[]; + /** 应用截图URL列表(与安装包、图标分目录存储) */ + screenshots: string[]; + systemApp: boolean; + debuggable: boolean; + remark: string; + createTime: string; +} + +/** 安装包分页查询参数 */ +export interface AppPackagePageQuery { + pageNum: number; + pageSize: number; + keywords?: string; + packageType?: PackageType; +} + +const BASE_URL = "/api/v1/app/packages"; + +const AppPackageAPI = { + /** 上传安装包(apk/aab/xapk/apks/apkm),自动解析详细字段 */ + upload(params: { + file: File; + packageName?: string; + versionName?: string; + versionCode?: string; + remark?: string; + /** 应用分类ID列表(多选) */ + categories?: string[]; + /** 应用截图文件列表(与安装包、图标分目录存储) */ + screenshots?: File[]; + }) { + const formData = new FormData(); + formData.append("file", params.file); + if (params.packageName) formData.append("packageName", params.packageName); + if (params.versionName) formData.append("versionName", params.versionName); + if (params.versionCode) formData.append("versionCode", params.versionCode); + if (params.remark) formData.append("remark", params.remark); + if (params.categories?.length) { + params.categories.forEach((c) => formData.append("categories", c)); + } + if (params.screenshots?.length) { + params.screenshots.forEach((s) => formData.append("screenshots", s)); + } + return uploadHttp({ + url: `${BASE_URL}/upload`, + method: "post", + data: formData, + headers: { "Content-Type": "multipart/form-data" }, + }); + }, + + /** 分页查询安装包列表 */ + page(query: AppPackagePageQuery) { + return request>({ + url: `${BASE_URL}/page`, + method: "get", + params: { + pageNum: query.pageNum, + pageSize: query.pageSize, + keywords: query.keywords, + packageType: query.packageType, + }, + }); + }, + + /** 安装包详情 */ + detail(id: string) { + return request({ + url: `${BASE_URL}/${id}`, + method: "get", + }); + }, + + /** 删除安装包 */ + delete(id: string) { + return request({ + url: `${BASE_URL}/${id}`, + method: "delete", + }); + }, + + /** 更新安装包分类(覆盖写) */ + updateCategories(id: string, categories: string[]) { + return request({ + url: `${BASE_URL}/${id}/categories`, + method: "put", + data: categories, + }); + }, +}; + +export default AppPackageAPI; + +/** 拼接静态资源完整访问地址(img / el-image 的 src 不经过 axios,需手动补 baseURL) */ +export function withBase(url?: string): string { + if (!url) return ""; + const baseApi = import.meta.env.VITE_APP_BASE_API ?? ""; + return `${baseApi}${url}`; +} diff --git a/src/api/system/device/types.ts b/src/api/system/device/types.ts index bab23a94..227d704c 100644 --- a/src/api/system/device/types.ts +++ b/src/api/system/device/types.ts @@ -68,6 +68,8 @@ export interface DeviceForm { serialno?: string; /** 设备名称 */ snName?: string; + /** 设备型号 */ + snModel?: string; /** 设备手机号 */ snMobile?: string; /** 设备在线状态(1:在线;0:离线) */ diff --git a/src/assets/icons/android.svg b/src/assets/icons/android.svg new file mode 100644 index 00000000..05cf3f7e --- /dev/null +++ b/src/assets/icons/android.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/appstore.svg b/src/assets/icons/appstore.svg new file mode 100644 index 00000000..a0ff2789 --- /dev/null +++ b/src/assets/icons/appstore.svg @@ -0,0 +1 @@ + diff --git a/src/components/DeviceSn/DeviceSnDetail.vue b/src/components/DeviceSn/DeviceSnDetail.vue index e359fe02..d2de109a 100644 --- a/src/components/DeviceSn/DeviceSnDetail.vue +++ b/src/components/DeviceSn/DeviceSnDetail.vue @@ -1,1809 +1,133 @@ diff --git a/src/utils/request.ts b/src/utils/request.ts index 7ed55a9c..14f04e38 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -18,93 +18,108 @@ const http = axios.create({ paramsSerializer: (params) => qs.stringify(params, { arrayFormat: "repeat" }), }); -http.interceptors.request.use( - (config: InternalAxiosRequestConfig) => { - const token = AuthStorage.getAccessToken(); +// 大文件上传专用实例:直连后端地址(VITE_APP_API_URL),绕过 vite dev proxy, +// 避免 vite/http-proxy 转发大请求体时缓冲/超时导致连接中断(客户端 EOFException)。 +const uploadHttp = axios.create({ + baseURL: import.meta.env.VITE_APP_API_URL, + // 大文件上传放宽超时到 10 分钟(默认 50s 对 apks 等大文件不足) + timeout: 600000, + headers: { "Content-Type": "application/json;charset=utf-8" }, +}); - // 约定:调用方设置 Authorization 为 "no-auth" 即跳过 token 注入 - if (config.headers.Authorization === "no-auth") { - delete config.headers.Authorization; - } else if (token) { - config.headers.Authorization = `Bearer ${token}`; - } +function setupInterceptors(instance: typeof http) { + instance.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + const token = AuthStorage.getAccessToken(); - return config; - }, - (error) => Promise.reject(error) -); + // 约定:调用方设置 Authorization 为 "no-auth" 即跳过 token 注入 + if (config.headers.Authorization === "no-auth") { + delete config.headers.Authorization; + } else if (token) { + config.headers.Authorization = `Bearer ${token}`; + } -http.interceptors.response.use( - (response: AxiosResponse): AxiosResponse | any => { - const { responseType } = response.config; + return config; + }, + (error) => Promise.reject(error) + ); - // 二进制数据直接透传 - if (responseType === "blob" || responseType === "arraybuffer") { - return response; - } + instance.interceptors.response.use( + (response: AxiosResponse): AxiosResponse | any => { + const { responseType } = response.config; - const { code, data, msg } = response.data; + // 二进制数据直接透传 + if (responseType === "blob" || responseType === "arraybuffer") { + return response; + } - if (code === ApiCodeEnum.SUCCESS) { - return data; - } + const { code, data, msg } = response.data; - ElMessage.error(msg || "系统出错"); - return Promise.reject(new Error(msg || "系统出错")); - }, + if (code === ApiCodeEnum.SUCCESS) { + return data; + } - async (error) => { - const { config, response } = error; + ElMessage.error(msg || "系统出错"); + return Promise.reject(new Error(msg || "系统出错")); + }, - if (!response) { - ElMessage.error("网络连接失败"); - return Promise.reject(error); - } + async (error) => { + const { config, response } = error; - const { code, msg } = response.data as ApiResult; + if (!response) { + ElMessage.error("网络连接失败"); + return Promise.reject(error); + } - // Token 过期 - if (code === ApiCodeEnum.ACCESS_TOKEN_INVALID) { - if (!config || retriedRequests.has(config)) { - await redirectToLogin("登录已过期,请重新登录"); + const { code, msg } = response.data as ApiResult; + + // Token 过期 + if (code === ApiCodeEnum.ACCESS_TOKEN_INVALID) { + if (!config || retriedRequests.has(config)) { + await redirectToLogin("登录已过期,请重新登录"); + return Promise.reject(new Error("Token Invalid")); + } + + retriedRequests.add(config); + + try { + const userStore = useUserStoreHook(); + await userStore.refreshTokenOnce(); + + const token = AuthStorage.getAccessToken(); + if (token) { + config.headers.set("Authorization", `Bearer ${token}`); + } + + return instance(config); + } catch { + await redirectToLogin("登录已过期,请重新登录"); + return Promise.reject(new Error("Token Invalid")); + } + } + + // Refresh token 失效 + if (code === ApiCodeEnum.REFRESH_TOKEN_INVALID) { + await redirectToLogin("登录已过期,请重新登录", false); return Promise.reject(new Error("Token Invalid")); } - retriedRequests.add(config); - - try { - const userStore = useUserStoreHook(); - await userStore.refreshTokenOnce(); - - const token = AuthStorage.getAccessToken(); - if (token) { - config.headers.set("Authorization", `Bearer ${token}`); - } - - return http(config); - } catch { - await redirectToLogin("登录已过期,请重新登录"); - return Promise.reject(new Error("Token refresh failed")); + // 权限不足 + if (code === ApiCodeEnum.PERMISSION_DENIED) { + const permissionStore = usePermissionStoreHook(); + await permissionStore.refreshPermissions(); + ElMessage.error(msg || "权限不足"); + return Promise.reject(new Error(msg || "权限不足")); } - } - // Refresh token 失效 - if (code === ApiCodeEnum.REFRESH_TOKEN_INVALID) { - await redirectToLogin("登录已过期,请重新登录", false); - return Promise.reject(new Error("Token Invalid")); + ElMessage.error(msg || "请求失败"); + return Promise.reject(new Error(msg || "请求失败")); } + ); +} - // 权限不足 - if (code === ApiCodeEnum.PERMISSION_DENIED) { - const permissionStore = usePermissionStoreHook(); - await permissionStore.refreshPermissions(); - ElMessage.error(msg || "权限不足"); - return Promise.reject(new Error(msg || "权限不足")); - } - - ElMessage.error(msg || "请求失败"); - return Promise.reject(new Error(msg || "请求失败")); - } -); +setupInterceptors(http); +setupInterceptors(uploadHttp); export default http; +export { uploadHttp }; diff --git a/src/views/app-manager/category/index.vue b/src/views/app-manager/category/index.vue new file mode 100644 index 00000000..869f5c65 --- /dev/null +++ b/src/views/app-manager/category/index.vue @@ -0,0 +1,292 @@ + + + + + diff --git a/src/views/app-manager/upload-package/components/UploadDialog.vue b/src/views/app-manager/upload-package/components/UploadDialog.vue new file mode 100644 index 00000000..aedbee7f --- /dev/null +++ b/src/views/app-manager/upload-package/components/UploadDialog.vue @@ -0,0 +1,479 @@ + + + + + diff --git a/src/views/app-manager/upload-package/index.vue b/src/views/app-manager/upload-package/index.vue new file mode 100644 index 00000000..119d19a4 --- /dev/null +++ b/src/views/app-manager/upload-package/index.vue @@ -0,0 +1,570 @@ + + + + + diff --git a/src/views/devices/sn/config/add.ts b/src/views/devices/sn/config/add.ts index f6303592..59b01984 100644 --- a/src/views/devices/sn/config/add.ts +++ b/src/views/devices/sn/config/add.ts @@ -1,30 +1,31 @@ -import UserAPI from "@/api/system/user"; -import type { UserForm } from "@/types/api"; +import DeviceAPI from "@/api/system/device"; +import type { DeviceForm } from "@/api/system/device/types"; import type { IModalConfig } from "@/components/DeviceSn/types"; -import { deptArr, roleArr } from "./options"; -const modalConfig: IModalConfig = { - permPrefix: "sys:user", +const modalConfig: IModalConfig = { + permPrefix: "sys:device", dialog: { - title: "新增用户", + title: "新增设备", width: 800, draggable: true, }, form: { labelWidth: 100, }, - formAction: UserAPI.create, + formAction(data) { + return DeviceAPI.create(data); + }, beforeSubmit(data) { - console.warn("提交之前处理", data); + return data; }, formItems: [ { - label: "用户名", - prop: "username", - rules: [{ required: true, message: "用户名不能为空", trigger: "blur" }], + label: "设备SN", + prop: "serialno", + rules: [{ required: true, message: "设备SN不能为空", trigger: "blur" }], type: "input", attrs: { - placeholder: "请输入用户名", + placeholder: "请输入设备序列号", }, col: { xs: 24, @@ -32,12 +33,12 @@ const modalConfig: IModalConfig = { }, }, { - label: "用户昵称", - prop: "nickname", - rules: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }], + label: "设备型号", + prop: "snModel", + rules: [{ required: true, message: "设备型号不能为空", trigger: "blur" }], type: "input", attrs: { - placeholder: "请输入用户昵称", + placeholder: "请输入设备型号", }, col: { xs: 24, @@ -45,48 +46,21 @@ const modalConfig: IModalConfig = { }, }, { - label: "所属部门", - prop: "deptId", - rules: [{ required: true, message: "所属部门不能为空", trigger: "change" }], - type: "tree-select", - attrs: { - placeholder: "请选择所属部门", - data: deptArr, - filterable: true, - "check-strictly": true, - "render-after-expand": false, - }, - // async initFn(formItem) { - // // 注意:如果initFn函数不是箭头函数,this会指向此配置项对象,那么也就可以用this来替代形参formItem - // formItem.attrs.data = await DeptAPI.getOptions(); - // }, - }, - { - type: "custom", - label: "性别", - prop: "gender", - initialValue: 1, - attrs: { style: { width: "100%" } }, - }, - { - label: "角色", - prop: "roleIds", - rules: [{ required: true, message: "用户角色不能为空", trigger: "change" }], - type: "select", - attrs: { - placeholder: "请选择", - multiple: true, - }, - options: roleArr, - initialValue: [], - // async initFn(formItem) { - // formItem.options = await RoleAPI.getOptions(); - // }, - }, - { + label: "设备昵称", + prop: "snName", + rules: [{ required: true, message: "设备昵称不能为空", trigger: "blur" }], type: "input", + attrs: { + placeholder: "请输入内容", + }, + col: { + xs: 24, + sm: 12, + }, + }, + { label: "手机号码", - prop: "mobile", + prop: "snMobile", rules: [ { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, @@ -94,25 +68,50 @@ const modalConfig: IModalConfig = { trigger: "blur", }, ], + type: "input", attrs: { placeholder: "请输入手机号码", maxlength: 11, }, + col: { + xs: 24, + sm: 12, + }, }, { - label: "邮箱", - prop: "email", - rules: [ - { - pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/, - message: "请输入正确的邮箱地址", - trigger: "blur", - }, - ], + label: "IMEI", + prop: "snImei", type: "input", attrs: { - placeholder: "请输入邮箱", - maxlength: 50, + placeholder: "请输入设备IMEI", + }, + col: { + xs: 24, + sm: 12, + }, + }, + { + label: "设备类型", + prop: "deviceType", + type: "input", + attrs: { + placeholder: "请输入设备类型", + }, + col: { + xs: 24, + sm: 12, + }, + }, + { + label: "来源", + prop: "source", + type: "input", + attrs: { + placeholder: "请输入设备来源", + }, + col: { + xs: 24, + sm: 12, }, }, { @@ -120,17 +119,11 @@ const modalConfig: IModalConfig = { prop: "status", type: "radio", options: [ - { label: "正常", value: 1 }, + { label: "启用", value: 1 }, { label: "禁用", value: 0 }, ], initialValue: 1, }, - { - type: "custom", - label: "二级弹窗", - prop: "openModal", - slotName: "openModal", - }, ], }; diff --git a/src/views/devices/sn/config/edit.ts b/src/views/devices/sn/config/edit.ts index 2ee23cef..2b244b8a 100644 --- a/src/views/devices/sn/config/edit.ts +++ b/src/views/devices/sn/config/edit.ts @@ -1,87 +1,69 @@ -import UserAPI from "@/api/system/user"; -import type { UserForm } from "@/api/system/user/types"; +import { reactive } from "vue"; +import DeviceAPI from "@/api/system/device"; +import type { DeviceForm } from "@/api/system/device/types"; import type { IModalConfig } from "@/components/DeviceSn/types"; import { DeviceEnum } from "@/enums/settings"; import { useAppStore } from "@/stores"; -import { deptArr, roleArr } from "./options"; -const modalConfig: IModalConfig = { - permPrefix: "sys:user", +/** 只读输入框配置(展示常用参数,不可编辑) */ +const readonlyInput = (placeholder = "系统自动填充") => ({ + type: "input" as const, + attrs: { + placeholder, + readonly: true, + disabled: true, + }, +}); + +const modalConfig: IModalConfig = { + permPrefix: "sys:device", component: "drawer", drawer: { - title: "修改用户", + title: "修改设备", size: useAppStore().device === DeviceEnum.MOBILE ? "80%" : 500, }, pk: "id", + // 编辑只回传可修改字段,其余保持不变 beforeSubmit(data) { - console.warn("beforeSubmit", data); + return { + id: data.id, + serialno: data.serialno, + snName: data.snName, + status: data.status, + } as DeviceForm; }, formAction(data) { - return UserAPI.update(data.id as string, data); + return DeviceAPI.update(data.id as string, data); }, formItems: [ { - label: "用户名", - prop: "username", - rules: [{ required: true, message: "用户名不能为空", trigger: "blur" }], + label: "设备SN", + prop: "serialno", type: "input", attrs: { - placeholder: "请输入用户名", + placeholder: "设备序列号", readonly: true, }, }, { - label: "用户昵称", - prop: "nickname", - rules: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }], + label: "设备型号", + prop: "snModel", + rules: [{ required: true, message: "设备型号不能为空", trigger: "blur" }], + ...readonlyInput("设备型号"), + }, + { + label: "设备昵称", + prop: "snName", + rules: [{ required: true, message: "设备昵称不能为空", trigger: "blur" }], type: "input", attrs: { - placeholder: "请输入用户昵称", + placeholder: "请输入设备昵称", + maxlength: 50, }, }, { - label: "所属部门", - prop: "deptId", - rules: [{ required: true, message: "所属部门不能为空", trigger: "blur" }], - type: "tree-select", - attrs: { - placeholder: "请选择所属部门", - data: deptArr, // setup,Vue会自动解包ref,不需要.value - filterable: true, - "check-strictly": true, - "render-after-expand": false, - }, - // async initFn(formItem) { - // // 注意:如果initFn函数不是箭头函数,this会指向此配置项对象,那么也就可以用this来替代形参formItem - // formItem.attrs.data = await DeptAPI.getOptions(); - // }, - }, - { - type: "custom", - label: "性别", - prop: "gender", - initialValue: 1, - attrs: { style: { width: "100%" } }, - }, - { - label: "角色", - prop: "roleIds", - rules: [{ required: true, message: "用户角色不能为空", trigger: "blur" }], - type: "select", - attrs: { - placeholder: "请选择", - multiple: true, - }, - options: roleArr, - initialValue: [], - // async initFn(formItem) { - // formItem.options = await RoleAPI.getOptions(); - // }, - }, - { - type: "input", label: "手机号码", - prop: "mobile", + prop: "snMobile", rules: [ { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, @@ -89,26 +71,32 @@ const modalConfig: IModalConfig = { trigger: "blur", }, ], - attrs: { - placeholder: "请输入手机号码", - maxlength: 11, - }, + ...readonlyInput("绑定手机号"), }, { - label: "邮箱", - prop: "email", - rules: [ - { - pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/, - message: "请输入正确的邮箱地址", - trigger: "blur", - }, - ], - type: "input", - attrs: { - placeholder: "请输入邮箱", - maxlength: 50, - }, + label: "IMEI", + prop: "snImei", + ...readonlyInput("设备IMEI"), + }, + { + label: "设备类型", + prop: "deviceType", + ...readonlyInput("设备类型"), + }, + { + label: "来源", + prop: "source", + ...readonlyInput("设备来源"), + }, + { + label: "激活时间", + prop: "activationTime", + ...readonlyInput("激活时间"), + }, + { + label: "最后上线", + prop: "lastOnline", + ...readonlyInput("最后上线时间"), }, { label: "状态", @@ -116,7 +104,7 @@ const modalConfig: IModalConfig = { type: "switch", attrs: { inlinePrompt: true, - activeText: "正常", + activeText: "启用", inactiveText: "禁用", activeValue: 1, inactiveValue: 0, diff --git a/src/views/devices/sn/index.vue b/src/views/devices/sn/index.vue index 662888c9..befbdc5a 100644 --- a/src/views/devices/sn/index.vue +++ b/src/views/devices/sn/index.vue @@ -103,7 +103,6 @@ const { handleResetClick, handleAddClick, handleEditClick, - handleViewClick, handleSubmitClick, handleExportClick, handleSearchClick, @@ -125,12 +124,8 @@ function handleReturnClick() { // 表格工具 const handleOperateClick = (data: IObject) => { if (data.name === "detail") { - editModalConfig.drawer = { ...editModalConfig.drawer, title: "查看" }; - handleViewClick(data.row, async () => { - // 加载下拉数据源,建议在初始化配置项"initFn 中加载,避免多次请求 - // editModalConfig.formItems[2]!.attrs!.data = await DeptAPI.getOptions(); - return await DeviceAPI.getFormData(data.row.serialno as string); // 根据SN获取详情 - }); + // 打开设备详情抽屉,读取常用参数 + handleSerialnoClick(data.row); } else if (data.name === "edit") { editModalConfig.drawer = { ...editModalConfig.drawer, title: "修改" }; handleEditClick(data.row, async () => { diff --git a/vite.config.ts b/vite.config.ts index d76aea73..335fb980 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -48,6 +48,15 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { changeOrigin: true, target: env.VITE_APP_API_URL, rewrite: (path: string) => path.replace(new RegExp(`^${env.VITE_APP_BASE_API}`), ""), + // 大文件上传放宽代理超时(默认 2 分钟,apks 等大文件易中断) + timeout: 600000, + proxyTimeout: 600000, + }, + // 静态资源(安装包图标/截图/分类图标等由后端以 /static/** 提供)需代理到后端, + // 否则 dev server 会返回 SPA 兜底 index.html(text/html),导致图片 304/不显示。 + "/static": { + changeOrigin: true, + target: env.VITE_APP_API_URL, }, }, },