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 00000000..a7854639 Binary files /dev/null and b/src/assets/images/iphone-17-pro.webp differ 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']