From 742b32490f5ef74ae472f00a19fab3d727b72d39 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Thu, 30 Jul 2026 17:13:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(DeviceSn):=20=E6=B7=BB=E5=8A=A0=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E7=BB=84=E4=BB=B6=E5=B9=B6=E6=9B=B4=E6=96=B0=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E6=8E=A5=E5=8F=A3=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 4 +- src/api/system/device/index.ts | 312 ++++++ src/api/system/device/types.ts | 163 +++ src/assets/icons/barcode-box-line.svg | 9 + src/assets/icons/debug.svg | 9 + src/assets/icons/developer.svg | 12 + src/assets/icons/mobile_phone.svg | 12 + src/assets/icons/screenshot.svg | 1 + src/assets/images/iphone-17-pro.webp | Bin 0 -> 41422 bytes src/components/DeviceSn/DevicePageContent.vue | 977 ++++++++++++++++++ src/components/DeviceSn/DevicePageModal.vue | 274 +++++ src/components/DeviceSn/DevicePageSearch.vue | 169 +++ src/components/DeviceSn/DeviceSnDetail.vue | 735 +++++++++++++ src/components/DeviceSn/types.ts | 222 ++++ src/components/DeviceSn/usePage.ts | 105 ++ src/views/devices/sn/config/add.ts | 138 +++ src/views/devices/sn/config/content.ts | 182 ++++ src/views/devices/sn/config/edit.ts | 128 +++ src/views/devices/sn/config/options.ts | 27 + src/views/devices/sn/config/search.ts | 63 ++ src/views/devices/sn/index.vue | 223 ++++ types/components.d.ts | 4 + 22 files changed, 3767 insertions(+), 2 deletions(-) create mode 100644 src/api/system/device/index.ts create mode 100644 src/api/system/device/types.ts create mode 100644 src/assets/icons/barcode-box-line.svg create mode 100644 src/assets/icons/debug.svg create mode 100644 src/assets/icons/developer.svg create mode 100644 src/assets/icons/mobile_phone.svg create mode 100644 src/assets/icons/screenshot.svg create mode 100644 src/assets/images/iphone-17-pro.webp create mode 100644 src/components/DeviceSn/DevicePageContent.vue create mode 100644 src/components/DeviceSn/DevicePageModal.vue create mode 100644 src/components/DeviceSn/DevicePageSearch.vue create mode 100644 src/components/DeviceSn/DeviceSnDetail.vue create mode 100644 src/components/DeviceSn/types.ts create mode 100644 src/components/DeviceSn/usePage.ts create mode 100644 src/views/devices/sn/config/add.ts create mode 100644 src/views/devices/sn/config/content.ts create mode 100644 src/views/devices/sn/config/edit.ts create mode 100644 src/views/devices/sn/config/options.ts create mode 100644 src/views/devices/sn/config/search.ts create mode 100644 src/views/devices/sn/index.vue diff --git a/.env.development b/.env.development index 60906d47..5fc3077a 100644 --- a/.env.development +++ b/.env.development @@ -6,9 +6,9 @@ VITE_APP_TITLE=vue3-element-admin VITE_APP_BASE_API=/dev-api # 接口地址 -VITE_APP_API_URL=https://api.youlai.tech # 线上 +# VITE_APP_API_URL=https://api.youlai.tech # 线上 # VITE_APP_API_URL=https://api.youlai.tech/v2 # 线上(多租户) -# VITE_APP_API_URL=http://localhost:8000 # 本地 +VITE_APP_API_URL=http://192.168.5.224:8000 # 本地 # 启用 Mock 服务(true:开启 false:关闭) diff --git a/src/api/system/device/index.ts b/src/api/system/device/index.ts new file mode 100644 index 00000000..619becde --- /dev/null +++ b/src/api/system/device/index.ts @@ -0,0 +1,312 @@ +import request from "@/utils/request"; +import type { + DeviceInfo, + DeviceForm, + DeviceQueryParams, + DeviceItem, + DeviceProfileDetail, + DeviceProfileForm, + DevicePasswordChangeForm, + DevicePasswordVerifyForm, + DeviceMobileUpdateForm, + DeviceEmailUpdateForm, +} from "./types"; + +import type { ExcelResult, PageResult, OptionItem } from "@/api/common"; + +const DEVICE_BASE_URL = "/api/v1/device"; + +const DeviceAPI = { + /** + * 获取当前登录用户信息 + * + * @returns 登录用户昵称、头像信息,包括角色和权限 + */ + getInfo() { + return request({ + url: `${DEVICE_BASE_URL}/me`, + method: "get", + }); + }, + + /** + * 获取用户分页列表 + * + * @param queryParams 查询参数 + */ + getPage(queryParams: DeviceQueryParams) { + return request>({ + url: `${DEVICE_BASE_URL}`, + method: "get", + params: queryParams, + }); + }, + + /** + * 获取设备表单详情 + * + * @param sn 设备SN + * @returns 设备表单详情 + */ + getFormData(sn: string) { + return request({ + url: `${DEVICE_BASE_URL}/${sn}/info`, + method: "get", + }); + }, + + /** + * 添加设备 + * + * @param data 设备表单数据 + */ + create(data: DeviceForm) { + return request({ + url: `${DEVICE_BASE_URL}`, + method: "post", + data, + }); + }, + + /** + * 修改用户 + * + * @param id 用户ID + * @param data 用户表单数据 + */ + update(id: string, data: DeviceForm) { + return request({ + url: `${DEVICE_BASE_URL}/${id}`, + method: "put", + data, + }); + }, + + /** + * 修改用户密码 + * + * @param id 用户ID + * @param password 新密码 + */ + resetPassword(id: string, password: string) { + return request({ + url: `${DEVICE_BASE_URL}/${id}/password/reset`, + method: "put", + params: { password }, + }); + }, + + /** + * 批量删除用户,多个以英文逗号(,)分割 + * + * @param ids 用户ID字符串,多个以英文逗号(,)分割 + */ + deleteByIds(ids: string) { + return request({ + url: `${DEVICE_BASE_URL}/${ids}`, + method: "delete", + }); + }, + + /** 下载用户导入模板 */ + downloadTemplate() { + return request({ + url: `${DEVICE_BASE_URL}/template`, + method: "get", + responseType: "blob", + }); + }, + + /** + * 导出用户 + * + * @param queryParams 查询参数 + */ + export(queryParams: DeviceQueryParams) { + return request({ + url: `${DEVICE_BASE_URL}/export`, + method: "get", + params: queryParams, + responseType: "blob", + }); + }, + + /** + * 导入用户 + * + * @param file 导入文件 + */ + import(file: File) { + const formData = new FormData(); + formData.append("file", file); + return request({ + url: `${DEVICE_BASE_URL}/import`, + method: "post", + data: formData, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, + + /** 获取个人中心用户信息 */ + getProfile() { + return request({ + url: `${DEVICE_BASE_URL}/profile`, + method: "get", + }); + }, + + /** 修改个人中心用户信息 */ + updateProfile(data: DeviceProfileForm) { + return request({ + url: `${DEVICE_BASE_URL}/profile`, + method: "put", + data, + }); + }, + + /** 修改个人中心用户密码 */ + changePassword(data: DevicePasswordChangeForm) { + return request({ + url: `${DEVICE_BASE_URL}/password`, + method: "put", + data, + }); + }, + + /** 发送短信验证码(绑定或更换手机号)*/ + sendMobileCode(mobile: string) { + return request({ + url: `${DEVICE_BASE_URL}/mobile/code`, + method: "post", + params: { mobile }, + }); + }, + + /** 绑定或更换手机号 */ + bindOrChangeMobile(data: DeviceMobileUpdateForm) { + return request({ + url: `${DEVICE_BASE_URL}/mobile`, + method: "put", + data, + }); + }, + + /** 解绑手机号 */ + unbindMobile(data: DevicePasswordVerifyForm) { + return request({ + url: `${DEVICE_BASE_URL}/mobile`, + method: "delete", + data, + }); + }, + + /** 发送邮箱验证码(绑定或更换邮箱)*/ + sendEmailCode(email: string) { + return request({ + url: `${DEVICE_BASE_URL}/email/code`, + method: "post", + params: { email }, + }); + }, + + /** 绑定或更换邮箱 */ + bindOrChangeEmail(data: DeviceEmailUpdateForm) { + return request({ + url: `${DEVICE_BASE_URL}/email`, + method: "put", + data, + }); + }, + + /** 解绑邮箱 */ + unbindEmail(data: DevicePasswordVerifyForm) { + return request({ + url: `${DEVICE_BASE_URL}/email`, + method: "delete", + data, + }); + }, + + /** + * 获取用户下拉列表 + */ + getOptions() { + return request({ + url: `${DEVICE_BASE_URL}/options`, + method: "get", + }); + }, + /** + * 截图刷新 + * @param sn 设备SN号 + */ + deviceRefresh(sn: string) { + return request({ + url: `${DEVICE_BASE_URL}/refresh`, + method: "post", + params: { sn }, + }); + }, + /** + * 截图设备 + * @param sn 设备SN号 + */ + deviceScreenshot(sn: string) { + return request({ + url: `${DEVICE_BASE_URL}/screenshot`, + method: "post", + params: { sn }, + }); + }, + + /** + * 设备重启 + * @param sn 设备SN号 + */ + deviceReboot(sn: string) { + return request({ + url: `${DEVICE_BASE_URL}/reboot`, + method: "post", + params: { sn }, + }); + }, + /** + * 设备关机 + * @param sn 设备SN号 + */ + deviceShutdown(sn: string) { + return request({ + url: `${DEVICE_BASE_URL}/shutdown`, + method: "post", + params: { sn }, + }); + }, + + /** + * 定位设备 + * @param sn 设备SN号 + */ + deviceLocate(sn: string) { + return request({ + url: `${DEVICE_BASE_URL}/locate`, + method: "post", + params: { sn }, + }); + }, + + /** + * 重置设备 + * @param sn 设备SN号 + */ + deviceRestore(sn: string) { + return request({ + url: `${DEVICE_BASE_URL}/restore`, + method: "post", + params: { sn }, + }); + }, +}; + +export default DeviceAPI; diff --git a/src/api/system/device/types.ts b/src/api/system/device/types.ts new file mode 100644 index 00000000..1cb208e5 --- /dev/null +++ b/src/api/system/device/types.ts @@ -0,0 +1,163 @@ +/** + * Device 用户类型定义 + */ + +import type { BaseQueryParams } from "@/api/common"; + +/** 登录用户信息 */ +export interface DeviceInfo { + /** 用户ID */ + userId?: string; + /** 用户名 */ + username?: string; + /** 用户昵称 */ + nickname?: string; + /** 头像URL */ + avatar?: string; + /** 租户切换权限(true 可切换租户) */ + canSwitchTenant?: boolean; + /** 角色集合 */ + roles: string[]; + /** 权限集合 */ + perms: string[]; +} + +/** 用户分页查询参数 */ +export interface DeviceQueryParams extends BaseQueryParams { + /** 搜索关键字 */ + keywords?: string; + /** 用户状态 */ + status?: number; + /** 部门ID */ + deptId?: string; + /** 创建时间 */ + createTime?: [string, string]; +} + +/** 用户分页对象 */ +export interface DeviceItem { + /** 用户ID */ + id: string; + /** 用户头像地址 */ + avatar?: string; + /** 创建时间 */ + createTime?: Date; + /** 部门名称 */ + deptName?: string; + /** 用户邮箱 */ + email?: string; + /** 性别 */ + gender?: number; + /** 手机号 */ + mobile?: string; + /** 用户昵称 */ + nickname?: string; + /** 角色名称,多个使用英文逗号(,)分割 */ + roleNames?: string; + /** 用户状态(1:启用;0:禁用) */ + status?: number; + /** 用户名 */ + Devicename?: string; +} + +/** 设备表单对象 */ +export interface DeviceForm { + /** 设备ID */ + id?: string; + /** 设备序列号 */ + serialno?: string; + /** 设备名称 */ + snName?: string; + /** 设备手机号 */ + snMobile?: string; + /** 设备在线状态(1:在线;0:离线) */ + onlineStatus?: number; + /** 设备IMEI */ + snImei?: string; + /** 所属部门 */ + department?: string; + /** 分组 */ + group?: string; + /** 设备类型 */ + deviceType?: string; + /** 设备来源 */ + source?: string; + /** 激活时间 */ + activationTime?: string; + /** 最后上线时间 */ + lastOnline?: string; + /** 角色ID集合 */ + roleIds?: number[]; + /** 设备状态(1:正常;0:禁用) */ + status?: number; +} + +/** 个人中心用户信息 */ +export interface DeviceProfileDetail { + /** 用户ID */ + id?: string; + /** 用户名 */ + Devicename?: string; + /** 用户昵称 */ + nickname?: string; + /** 头像URL */ + avatar?: string; + /** 性别 */ + gender?: number; + /** 手机号 */ + mobile?: string; + /** 邮箱 */ + email?: string; + /** 部门名称 */ + deptName?: string; + /** 角色名称 */ + roleNames?: string; + /** 创建时间 */ + createTime?: Date; +} + +/** 个人中心用户信息表单 */ +export interface DeviceProfileForm { + /** 用户昵称 */ + nickname?: string; + /** 头像URL */ + avatar?: string; + /** 性别 */ + gender?: number; +} + +/** 修改密码表单 */ +export interface DevicePasswordChangeForm { + /** 原密码 */ + oldPassword?: string; + /** 新密码 */ + newPassword?: string; + /** 确认新密码 */ + confirmPassword?: string; +} + +/** 密码校验表单 */ +export interface DevicePasswordVerifyForm { + /** 当前密码 */ + password?: string; +} + +/** 修改手机表单 */ +export interface DeviceMobileUpdateForm { + /** 手机号 */ + mobile?: string; + /** 验证码 */ + code?: string; + /** 当前密码 */ + password?: string; +} + +/** 修改邮箱表单 */ +export interface DeviceEmailUpdateForm { + /** 邮箱 */ + email?: string; + /** 验证码 */ + code?: string; + /** 当前密码 */ + password?: string; +} diff --git a/src/assets/icons/barcode-box-line.svg b/src/assets/icons/barcode-box-line.svg new file mode 100644 index 00000000..89451807 --- /dev/null +++ b/src/assets/icons/barcode-box-line.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/src/assets/icons/debug.svg b/src/assets/icons/debug.svg new file mode 100644 index 00000000..63ce038a --- /dev/null +++ b/src/assets/icons/debug.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/src/assets/icons/developer.svg b/src/assets/icons/developer.svg new file mode 100644 index 00000000..d4b31d03 --- /dev/null +++ b/src/assets/icons/developer.svg @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/icons/mobile_phone.svg b/src/assets/icons/mobile_phone.svg new file mode 100644 index 00000000..7e745e23 --- /dev/null +++ b/src/assets/icons/mobile_phone.svg @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/icons/screenshot.svg b/src/assets/icons/screenshot.svg new file mode 100644 index 00000000..7e3d9159 --- /dev/null +++ b/src/assets/icons/screenshot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/iphone-17-pro.webp b/src/assets/images/iphone-17-pro.webp new file mode 100644 index 0000000000000000000000000000000000000000..a7854639d0bd1f6b1545135e213bc7b941a041a5 GIT binary patch literal 41422 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 literal 0 HcmV?d00001 diff --git a/src/components/DeviceSn/DevicePageContent.vue b/src/components/DeviceSn/DevicePageContent.vue new file mode 100644 index 00000000..31be5bf7 --- /dev/null +++ b/src/components/DeviceSn/DevicePageContent.vue @@ -0,0 +1,977 @@ + + + + + diff --git a/src/components/DeviceSn/DevicePageModal.vue b/src/components/DeviceSn/DevicePageModal.vue new file mode 100644 index 00000000..bf8ba4f0 --- /dev/null +++ b/src/components/DeviceSn/DevicePageModal.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/src/components/DeviceSn/DevicePageSearch.vue b/src/components/DeviceSn/DevicePageSearch.vue new file mode 100644 index 00000000..1cdb4610 --- /dev/null +++ b/src/components/DeviceSn/DevicePageSearch.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/src/components/DeviceSn/DeviceSnDetail.vue b/src/components/DeviceSn/DeviceSnDetail.vue new file mode 100644 index 00000000..68886f92 --- /dev/null +++ b/src/components/DeviceSn/DeviceSnDetail.vue @@ -0,0 +1,735 @@ + + + + + diff --git a/src/components/DeviceSn/types.ts b/src/components/DeviceSn/types.ts new file mode 100644 index 00000000..d0043173 --- /dev/null +++ b/src/components/DeviceSn/types.ts @@ -0,0 +1,222 @@ +import type { DialogProps, DrawerProps, FormItemRule, PaginationProps } from "element-plus"; +import type { FormProps, TableProps, ColProps, ButtonProps, CardProps } from "element-plus"; +import type PageContent from "./DevicePageContent.vue"; +import type PageModal from "./DevicePageModal.vue"; +import type PageSearch from "./DevicePageSearch.vue"; +import type SnDetail from "./DeviceSnDetail.vue"; +import type { CSSProperties } from "vue"; +import type { PageResult } from "@/types/api/common"; + +export type PageSearchInstance = InstanceType; +export type PageContentInstance = InstanceType; +export type PageModalInstance = InstanceType; +export type SnDetailInstance = InstanceType; + +export type IObject = Record; + +type DateComponent = "date-picker" | "time-picker" | "time-select" | "custom-tag" | "input-tag"; +type InputComponent = "input" | "select" | "input-number" | "cascader" | "tree-select"; +type OtherComponent = "text" | "radio" | "checkbox" | "switch" | "icon-select" | "custom"; +export type ISearchComponent = DateComponent | InputComponent | "custom"; +export type IComponentType = DateComponent | InputComponent | OtherComponent; + +type ToolbarLeft = "add" | "delete" | "import" | "export"; +type ToolbarRight = "refresh" | "filter" | "imports" | "exports" | "search"; +type ToolbarTable = "edit" | "view" | "delete"; +export type IToolsButton = { + name: string; // 按钮名称 + text?: string; // 按钮文本 + perm?: Array | string; // 权限标识(可以是完整权限字符串如'sys:user:create'或操作权限如'create') + attrs?: Partial & { style?: CSSProperties }; // 按钮属性 + render?: (row: IObject) => boolean; // 条件渲染 + children?: IToolsButton[]; +}; +export type IToolsDefault = ToolbarLeft | ToolbarRight | ToolbarTable | IToolsButton; + +export interface IOperateData { + name: string; + row: IObject; + column: IObject; + $index: number; +} + +export interface ISearchConfig { + // 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验 + permPrefix?: string; + // 标签冒号(默认:false) + colon?: boolean; + // 表单项(默认:[]) + formItems?: IFormItems; + // 是否开启展开和收缩(默认:true) + isExpandable?: boolean; + // 默认展示的表单项数量(默认:3) + showNumber?: number; + // 卡片属性 + cardAttrs?: Partial & { style?: CSSProperties }; + // form组件属性 + form?: IForm; + // 自适应网格布局(使用时表单不要添加 style: { width: "200px" }) + grid?: boolean | "left" | "right"; +} + +export interface IContentConfig { + // 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验 + permPrefix?: string; + // table组件属性 + table?: Partial, "data">>; + // 分页组件位置(默认:left) + pagePosition?: "left" | "right"; + // pagination组件属性 + pagination?: + | boolean + | Partial< + Omit< + PaginationProps, + "v-model:page-size" | "v-model:current-page" | "total" | "currentPage" + > + >; + // 列表的网络请求函数(需返回promise) + indexAction: (queryParams: TQuery) => Promise | TItem[]>; + // 默认的分页相关的请求参数 + request?: { + pageName: string; + limitName: string; + }; + // 分页接口统一返回 PageResult { list, total } + // 修改属性的网络请求函数(需返回promise) + modifyAction?: (data: { + [key: string]: any; + field: string; + value: boolean | string | number; + }) => Promise; + // 删除的网络请求函数(需返回promise) + deleteAction?: (ids: string) => Promise; + // 后端导出的网络请求函数(需返回promise) + exportAction?: (queryParams: TQuery) => Promise; + // 前端全量导出的网络请求函数(需返回promise) + exportsAction?: (queryParams: TQuery) => Promise; + // 导入模板 + importTemplate?: string | (() => Promise); + // 后端导入的网络请求函数(需返回promise) + importAction?: (file: File) => Promise; + // 前端导入的网络请求函数(需返回promise) + importsAction?: (data: IObject[]) => Promise; + // 主键名(默认为id) + pk?: string; + // 表格工具栏(默认:add,delete,export,也可自定义) + toolbar?: Array; + // 表格工具栏右侧图标(默认:refresh,filter,imports,exports,search) + defaultToolbar?: Array; + // table组件列属性(额外的属性templet,operat,slotName) + cols: Array<{ + type?: "default" | "selection" | "index" | "expand"; + label?: string; + prop?: string; + width?: string | number; + align?: "left" | "center" | "right"; + columnKey?: string; + reserveSelection?: boolean; + // 列是否显示 + show?: boolean; + // 模板 + templet?: + | "image" + | "list" + | "url" + | "switch" + | "input" + | "price" + | "percent" + | "icon" + | "date" + | "tool" + | "custom"; + // image模板相关参数 + imageWidth?: number; + imageHeight?: number; + // list模板相关参数 + selectList?: IObject; + // switch模板相关参数 + activeValue?: boolean | string | number; + inactiveValue?: boolean | string | number; + activeText?: string; + inactiveText?: string; + // input模板相关参数 + inputType?: string; + // price模板相关参数 + priceFormat?: string; + // date模板相关参数 + dateFormat?: string; + // tool模板相关参数 + operat?: Array; + // filter值拼接符 + filterJoin?: string; + [key: string]: any; + // 初始化数据函数 + initFn?: (item: IObject) => void; + }>; +} + +export interface IModalConfig { + // 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验 + permPrefix?: string; + // 标签冒号(默认:false) + colon?: boolean; + // 主键名(主要用于编辑数据,默认为id) + pk?: string; + // 组件类型(默认:dialog) + component?: "dialog" | "drawer"; + // dialog组件属性 + dialog?: Partial>; + // drawer组件属性 + drawer?: Partial>; + // form组件属性 + form?: IForm; + // 表单项 + formItems: IFormItems; + // 提交之前处理 + beforeSubmit?: (data: T) => void; + // 提交的网络请求函数(需返回promise) + formAction?: (data: T) => Promise; +} + +export type IForm = Partial>; + +// 表单项 +export type IFormItems = Array<{ + // 组件类型(如input,select,radio,custom等) + type: T; + // 标签提示 + tips?: string | IObject; + // 标签文本 + label: string; + // 键名 + prop: string; + // 组件属性 + attrs?: IObject; + // 组件可选项(只适用于select,radio,checkbox组件) + options?: Array<{ label: string; value: any; [key: string]: any }> | Ref; + // 验证规则 + rules?: FormItemRule[]; + // 初始值 + initialValue?: any; + // 插槽名(适用于自定义组件,设置类型为custom) + slotName?: string; + // 是否隐藏 + hidden?: boolean; + // layout组件Col属性 + col?: Partial; + // 组件事件 + events?: Record void>; + // 初始化数据函数扩展 + initFn?: (item: IObject) => void; +}>; + +export interface IPageForm { + // 主键名(主要用于编辑数据,默认为id) + pk?: string; + // form组件属性 + form?: IForm; + // 表单项 + formItems: IFormItems; +} diff --git a/src/components/DeviceSn/usePage.ts b/src/components/DeviceSn/usePage.ts new file mode 100644 index 00000000..45fd36c7 --- /dev/null +++ b/src/components/DeviceSn/usePage.ts @@ -0,0 +1,105 @@ +import { ref } from "vue"; +import type { IObject, PageContentInstance, PageModalInstance, PageSearchInstance } from "./types"; + +function usePage() { + const searchRef = ref(); + const contentRef = ref(); + const addModalRef = ref(); + const editModalRef = ref(); + + // 搜索 + function handleQueryClick(queryParams: IObject) { + const filterParams = contentRef.value?.getFilterParams(); + contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true); + } + // 重置 + function handleResetClick(queryParams: IObject) { + const filterParams = contentRef.value?.getFilterParams(); + contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true); + } + // 新增 + function handleAddClick(RefImpl?: Ref) { + if (RefImpl) { + RefImpl?.value.setModalVisible(); + RefImpl?.value.handleDisabled(false); + } else { + addModalRef.value?.setModalVisible(); + addModalRef.value?.handleDisabled(false); + } + } + // 编辑 + async function handleEditClick( + row: IObject, + callback?: (result?: IObject) => IObject, + RefImpl?: Ref + ) { + if (RefImpl) { + RefImpl.value?.setModalVisible(); + RefImpl.value?.handleDisabled(false); + const from = await (callback?.(row) ?? Promise.resolve(row)); + RefImpl.value?.setFormData(from ? from : row); + } else { + editModalRef.value?.setModalVisible(); + editModalRef.value?.handleDisabled(false); + const from = await (callback?.(row) ?? Promise.resolve(row)); + editModalRef.value?.setFormData(from ? from : row); + } + } + // 查看 + async function handleViewClick( + row: IObject, + callback?: (result?: IObject) => IObject, + RefImpl?: Ref + ) { + if (RefImpl) { + RefImpl.value?.setModalVisible(); + RefImpl.value?.handleDisabled(true); + const from = await (callback?.(row) ?? Promise.resolve(row)); + RefImpl.value?.setFormData(from ? from : row); + } else { + editModalRef.value?.setModalVisible(); + editModalRef.value?.handleDisabled(true); + const from = await (callback?.(row) ?? Promise.resolve(row)); + editModalRef.value?.setFormData(from ? from : row); + } + } + // 表单提交 + function handleSubmitClick() { + //根据检索条件刷新列表数据 + const queryParams = searchRef.value?.getQueryParams(); + contentRef.value?.fetchPageData(queryParams, true); + } + // 导出 + function handleExportClick() { + // 根据检索条件导出数据 + const queryParams = searchRef.value?.getQueryParams(); + contentRef.value?.exportPageData(queryParams); + } + // 搜索显隐 + function handleSearchClick() { + searchRef.value?.toggleVisible(); + } + // 筛选数据 + function handleFilterChange(filterParams: IObject) { + const queryParams = searchRef.value?.getQueryParams(); + contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true); + } + + return { + searchRef, + contentRef, + addModalRef, + editModalRef, + handleQueryClick, + handleResetClick, + handleAddClick, + handleEditClick, + handleViewClick, + handleSubmitClick, + handleExportClick, + handleSearchClick, + handleFilterChange, + }; +} + +export default usePage; diff --git a/src/views/devices/sn/config/add.ts b/src/views/devices/sn/config/add.ts new file mode 100644 index 00000000..f6303592 --- /dev/null +++ b/src/views/devices/sn/config/add.ts @@ -0,0 +1,138 @@ +import UserAPI from "@/api/system/user"; +import type { UserForm } from "@/types/api"; +import type { IModalConfig } from "@/components/DeviceSn/types"; +import { deptArr, roleArr } from "./options"; + +const modalConfig: IModalConfig = { + permPrefix: "sys:user", + dialog: { + title: "新增用户", + width: 800, + draggable: true, + }, + form: { + labelWidth: 100, + }, + formAction: UserAPI.create, + beforeSubmit(data) { + console.warn("提交之前处理", data); + }, + formItems: [ + { + label: "用户名", + prop: "username", + rules: [{ required: true, message: "用户名不能为空", trigger: "blur" }], + type: "input", + attrs: { + placeholder: "请输入用户名", + }, + col: { + xs: 24, + sm: 12, + }, + }, + { + label: "用户昵称", + prop: "nickname", + rules: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }], + type: "input", + attrs: { + placeholder: "请输入用户昵称", + }, + col: { + xs: 24, + sm: 12, + }, + }, + { + 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(); + // }, + }, + { + type: "input", + label: "手机号码", + prop: "mobile", + rules: [ + { + pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, + message: "请输入正确的手机号码", + trigger: "blur", + }, + ], + attrs: { + placeholder: "请输入手机号码", + maxlength: 11, + }, + }, + { + 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: "状态", + prop: "status", + type: "radio", + options: [ + { label: "正常", value: 1 }, + { label: "禁用", value: 0 }, + ], + initialValue: 1, + }, + { + type: "custom", + label: "二级弹窗", + prop: "openModal", + slotName: "openModal", + }, + ], +}; + +// 如果有异步数据会修改配置的,推荐用reactive包裹,而纯静态配置的可以直接导出 +export default reactive(modalConfig); diff --git a/src/views/devices/sn/config/content.ts b/src/views/devices/sn/config/content.ts new file mode 100644 index 00000000..9dd28d70 --- /dev/null +++ b/src/views/devices/sn/config/content.ts @@ -0,0 +1,182 @@ +import DeviceAPI from "@/api/system/device"; +import RoleAPI from "@/api/system/role"; +import type { DeviceQueryParams, DeviceItem } from "@/api/system/device/types"; +import type { IContentConfig } from "@/components/DeviceSn/types"; + +const contentConfig: IContentConfig = { + permPrefix: "sys:user", // 不写不进行按钮权限校验 + table: { + border: true, + highlightCurrentRow: true, + }, + pagination: { + background: true, + layout: "prev,pager,next,jumper,total,sizes", + pageSize: 20, + pageSizes: [10, 20, 30, 50], + }, + indexAction(params) { + return DeviceAPI.getPage(params); + }, + deleteAction: DeviceAPI.deleteByIds, + importAction(file) { + return DeviceAPI.import(file); + }, + exportAction: DeviceAPI.export, + importTemplate: DeviceAPI.downloadTemplate, + importsAction(data) { + // 模拟导入数据 + console.warn("importsAction", data); + return Promise.resolve(); + }, + async exportsAction(params) { + // 模拟获取到的是全量数据 + const data = await DeviceAPI.getPage(params); + console.warn("exportsAction", data.list); + return data.list; + }, + pk: "id", + toolbar: [ + "add", + "delete", + "import", + "export", + { + name: "custom1", + text: "自定义1", + perm: "add", + attrs: { icon: "plus", color: "#626AEF" }, + }, + ], + defaultToolbar: ["refresh", "filter", "imports", "exports", "search"], + cols: [ + { type: "selection", width: 50, align: "center" }, + { label: "编号", align: "center", prop: "id", width: 100, show: false }, + { + label: "SN", + align: "center", + prop: "serialno", + templet: "custom", + slotName: "serialno", + width: 150, + }, + { label: "设备型号", align: "center", prop: "snModel", width: 200 }, + { label: "设备昵称", align: "center", prop: "snName", width: 200 }, + { label: "推送ID", align: "center", prop: "pushId", width: 200 }, + // { label: "设备 IMEI", align: "center", prop: "snImei", width: 200 }, + // { label: "设备版本号", align: "center", prop: "snDispalyId", width: 400 }, + // { label: "头像", align: "center", prop: "avatar", templet: "image" }, + // { + // label: "性别", + // align: "center", + // prop: "gender", + // width: 100, + // templet: "custom", + // slotName: "gender", + // }, + // { label: "部门", align: "center", prop: "deptName", width: 120 }, + // { + // label: "角色", + // align: "center", + // prop: "roleNames", + // width: 120, + // columnKey: "roleIds", + // filters: [], + // filterMultiple: true, + // filterJoin: ",", + // async initFn(colItem) { + // const roleOptions = await RoleAPI.getOptions(); + // colItem.filters = roleOptions.map((item) => { + // return { text: item.label, value: item.value }; + // }); + // }, + // }, + { + label: "手机号码", + align: "center", + prop: "snMobile", + templet: "custom", + slotName: "mobile", + width: 150, + }, + { + label: "状态", + align: "center", + prop: "status", + templet: "custom", + slotName: "status", + width: 120, + }, + { label: "创建时间", align: "center", prop: "createTime", width: 180 }, + { label: "激活时间", align: "center", prop: "activateTime", width: 180 }, + { + label: "操作", + align: "center", + fixed: "right", + width: 280, + templet: "tool", + operat: [ + { + name: "detail", + text: "详情", + attrs: { icon: "Document", type: "primary" }, + }, + // { + // name: "reset_pwd", + // text: "重置密码", + // // perm: "password-reset", + // attrs: { + // icon: "refresh-left", + // // color: "#626AEF", // 使用 text 属性,颜色不生效 + // style: { + // "--el-button-text-color": "#626AEF", + // "--el-button-hover-link-text-color": "#9197f4", + // }, + // }, + // }, + "edit", + "delete", + { + name: "more", // 主按钮名称 + text: "更多", // 显示文本 + attrs: { icon: "more", type: "primary" }, // 图标 + // children 数组定义下拉菜单项 + children: [ + { + name: "refresh", + text: "刷新", + attrs: { icon: "Refresh" }, + }, + { + name: "screenshot", + text: "截图", + attrs: { icon: "FullScreen" }, + }, + { + name: "locate", + text: "定位", + attrs: { icon: "Position" }, + }, + { + name: "shutdown", + text: "关机", + attrs: { icon: "SwitchButton" }, + }, + { + name: "reboot", + text: "重启", + attrs: { icon: "SwitchButton" }, + }, + { + name: "refactory", + text: "重置", + attrs: { icon: "RefreshLeft" }, + }, + ], + }, + ], + }, + ], +}; + +export default contentConfig; diff --git a/src/views/devices/sn/config/edit.ts b/src/views/devices/sn/config/edit.ts new file mode 100644 index 00000000..2ee23cef --- /dev/null +++ b/src/views/devices/sn/config/edit.ts @@ -0,0 +1,128 @@ +import UserAPI from "@/api/system/user"; +import type { UserForm } from "@/api/system/user/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", + component: "drawer", + drawer: { + title: "修改用户", + size: useAppStore().device === DeviceEnum.MOBILE ? "80%" : 500, + }, + pk: "id", + beforeSubmit(data) { + console.warn("beforeSubmit", data); + }, + formAction(data) { + return UserAPI.update(data.id as string, data); + }, + formItems: [ + { + label: "用户名", + prop: "username", + rules: [{ required: true, message: "用户名不能为空", trigger: "blur" }], + type: "input", + attrs: { + placeholder: "请输入用户名", + readonly: true, + }, + }, + { + label: "用户昵称", + prop: "nickname", + rules: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }], + type: "input", + attrs: { + placeholder: "请输入用户昵称", + }, + }, + { + 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", + rules: [ + { + pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, + message: "请输入正确的手机号码", + trigger: "blur", + }, + ], + attrs: { + placeholder: "请输入手机号码", + maxlength: 11, + }, + }, + { + 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: "状态", + prop: "status", + type: "switch", + attrs: { + inlinePrompt: true, + activeText: "正常", + inactiveText: "禁用", + activeValue: 1, + inactiveValue: 0, + }, + }, + ], +}; + +export default reactive(modalConfig); diff --git a/src/views/devices/sn/config/options.ts b/src/views/devices/sn/config/options.ts new file mode 100644 index 00000000..0e0baa8b --- /dev/null +++ b/src/views/devices/sn/config/options.ts @@ -0,0 +1,27 @@ +/** 公共下载数据,减少重复请求次数 */ +import DeptAPI from "@/api/system/dept"; +import RoleAPI from "@/api/system/role"; + +interface OptionItem { + label: string; + value: any; + [key: string]: any; // 允许其他属性 +} + +// 明确指定类型为 OptionItem[] +export const deptArr = ref([]); +export const roleArr = ref([]); +export const stateArr = ref([ + { label: "启用", value: 1 }, + { label: "禁用", value: 0 }, +]); + +// 初始化逻辑,在 onMounted 钩子中调用 +export const initOptions = async () => { + // 使用Promise.all并行请求 + const [dept, roles] = await Promise.all([DeptAPI.getOptions(), RoleAPI.getOptions()]); + // 获取部门选项并赋值 + deptArr.value = dept; + // 获取角色选项并赋值 + roleArr.value = roles; +}; diff --git a/src/views/devices/sn/config/search.ts b/src/views/devices/sn/config/search.ts new file mode 100644 index 00000000..de6c4d76 --- /dev/null +++ b/src/views/devices/sn/config/search.ts @@ -0,0 +1,63 @@ +import type { ISearchConfig } from "@/components/DeviceSn/types"; +import { deptArr, stateArr } from "./options"; + +const searchConfig: ISearchConfig = { + permPrefix: "sys:user", + formItems: [ + { + tips: "支持模糊搜索", + type: "input", + label: "关键字", + prop: "keywords", + attrs: { + placeholder: "用户名/昵称/手机号", + clearable: true, + style: { width: "200px" }, + }, + }, + { + type: "tree-select", + label: "部门", + prop: "deptId", + attrs: { + placeholder: "请选择", + data: deptArr, + filterable: true, + "check-strictly": true, + "render-after-expand": false, + clearable: true, + style: { width: "200px" }, + }, + // async initFn(formItem) { + // // 注意:如果initFn函数不是箭头函数,this会指向此配置项对象,那么也就可以用this来替代形参formItem + // formItem.attrs.data = await DeptAPI.getOptions(); + // }, + }, + { + type: "select", + label: "状态", + prop: "status", + attrs: { + placeholder: "全部", + clearable: true, + style: { width: "200px" }, + }, + options: stateArr, + }, + { + type: "date-picker", + label: "创建时间", + prop: "createTime", + attrs: { + type: "daterange", + "range-separator": "~", + "start-placeholder": "开始时间", + "end-placeholder": "截止时间", + "value-format": "YYYY-MM-DD", + style: { width: "200px" }, + }, + }, + ], +}; + +export default searchConfig; diff --git a/src/views/devices/sn/index.vue b/src/views/devices/sn/index.vue new file mode 100644 index 00000000..662888c9 --- /dev/null +++ b/src/views/devices/sn/index.vue @@ -0,0 +1,223 @@ + + + diff --git a/types/components.d.ts b/types/components.d.ts index 8a149896..275700f7 100644 --- a/types/components.d.ts +++ b/types/components.d.ts @@ -119,6 +119,10 @@ declare module 'vue' { UserDeptTree: typeof import('./../src/views/system/user/components/UserDeptTree.vue')['default'] UserImportDialog: typeof import('./../src/views/system/user/components/UserImportDialog.vue')['default'] WangEditor: typeof import('./../src/components/WangEditor/index.vue')['default'] + DevicePageContent: typeof import('./../src/components/DeviceSn/DevicePageContent.vue')['default'] + DevicePageModal: typeof import('./../src/components/DeviceSn/DevicePageModal.vue')['default'] + DevicePageSearch: typeof import('./../src/components/DeviceSn/DevicePageSearch.vue')['default'] + DeviceSnDetail: typeof import('./../src/components/DeviceSn/DeviceSnDetail.vue')['default'] } export interface GlobalDirectives { vLoading: typeof import('element-plus/es')['ElLoadingDirective']