From dc79401c1302a921eec76eadcfa8db3a2fb26a79 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Tue, 18 Nov 2025 18:25:21 +0800 Subject: [PATCH] feat(utils): add common utility functions and validation constants --- src/constants/index.ts | 88 ++++++++++ src/enums/common/dialog-enum.ts | 12 ++ src/enums/common/status-enum.ts | 22 +++ src/enums/index.ts | 4 + src/enums/system/user-enum.ts | 11 ++ src/utils/dom.ts | 51 ++++++ src/utils/download.ts | 73 ++++++++ src/utils/format.ts | 86 ++++++++++ src/utils/index.ts | 74 +++------ src/utils/validate.ts | 59 +++++++ src/views/login/index.vue | 112 ++++++++----- .../{DeptTree.vue => UserDeptTree.vue} | 0 .../{UserImport.vue => UserImportDialog.vue} | 0 src/views/system/user/index.vue | 157 ++++++------------ 14 files changed, 548 insertions(+), 201 deletions(-) create mode 100644 src/enums/common/dialog-enum.ts create mode 100644 src/enums/common/status-enum.ts create mode 100644 src/enums/system/user-enum.ts create mode 100644 src/utils/dom.ts create mode 100644 src/utils/download.ts create mode 100644 src/utils/format.ts create mode 100644 src/utils/validate.ts rename src/views/system/user/components/{DeptTree.vue => UserDeptTree.vue} (100%) rename src/views/system/user/components/{UserImport.vue => UserImportDialog.vue} (100%) diff --git a/src/constants/index.ts b/src/constants/index.ts index 1104d268..ef122d45 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -72,3 +72,91 @@ export const ALL_STORAGE_KEYS = { } as const; export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS]; + +/** + * 表单验证规则常量 + * 提供常用的验证规则,减少重复代码 + * + * @example + * ```ts + * const rules = reactive({ + * username: [VALIDATORS.required("用户名不能为空")], + * email: [VALIDATORS.email], + * mobile: [VALIDATORS.mobile], + * }); + * ``` + */ +export const VALIDATORS = { + /** + * 必填验证 + * @param message 错误提示信息 + */ + required: (message: string) => ({ + required: true, + message, + trigger: "blur", + }), + + /** + * 邮箱格式验证 + */ + email: { + pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/, + message: "请输入正确的邮箱地址", + trigger: "blur", + }, + + /** + * 手机号码验证(中国大陆) + */ + mobile: { + pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, + message: "请输入正确的手机号码", + trigger: "blur", + }, + + /** + * 身份证号码验证(中国大陆) + */ + idCard: { + pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, + message: "请输入正确的身份证号码", + trigger: "blur", + }, + + /** + * URL 格式验证 + */ + url: { + pattern: /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i, + message: "请输入正确的URL地址", + trigger: "blur", + }, + + /** + * 数字验证 + */ + number: { + pattern: /^\d+$/, + message: "请输入数字", + trigger: "blur", + }, + + /** + * 整数验证(正整数、负整数、0) + */ + integer: { + pattern: /^-?\d+$/, + message: "请输入整数", + trigger: "blur", + }, + + /** + * 正整数验证 + */ + positiveInteger: { + pattern: /^[1-9]\d*$/, + message: "请输入正整数", + trigger: "blur", + }, +} as const; diff --git a/src/enums/common/dialog-enum.ts b/src/enums/common/dialog-enum.ts new file mode 100644 index 00000000..90c983cc --- /dev/null +++ b/src/enums/common/dialog-enum.ts @@ -0,0 +1,12 @@ +/** + * 通用对话框模式枚举 + * @description 定义对话框的操作模式(创建、编辑、查看) + */ +export enum DialogMode { + /** 创建模式 - 新增数据 */ + CREATE = "create", + /** 编辑模式 - 修改数据 */ + EDIT = "edit", + /** 查看模式 - 只读展示 */ + VIEW = "view", +} diff --git a/src/enums/common/status-enum.ts b/src/enums/common/status-enum.ts new file mode 100644 index 00000000..cce10afd --- /dev/null +++ b/src/enums/common/status-enum.ts @@ -0,0 +1,22 @@ +/** + * 通用状态枚举 + * 适用于大多数业务实体的启用/禁用状态 + */ +export enum CommonStatus { + /** 禁用 */ + DISABLED = 0, + /** 启用 */ + ENABLED = 1, +} + +/** + * 审核状态枚举 + */ +export enum AuditStatus { + /** 待审核 */ + PENDING = 0, + /** 已通过 */ + APPROVED = 1, + /** 已拒绝 */ + REJECTED = 2, +} diff --git a/src/enums/index.ts b/src/enums/index.ts index 79189297..c7f6fa99 100644 --- a/src/enums/index.ts +++ b/src/enums/index.ts @@ -8,4 +8,8 @@ export * from "./settings/theme-enum"; export * from "./settings/locale-enum"; export * from "./settings/device-enum"; +export * from "./common/dialog-enum"; +export * from "./common/status-enum"; + export * from "./system/menu-enum"; +export * from "./system/user-enum"; diff --git a/src/enums/system/user-enum.ts b/src/enums/system/user-enum.ts new file mode 100644 index 00000000..af4258c5 --- /dev/null +++ b/src/enums/system/user-enum.ts @@ -0,0 +1,11 @@ +/** + * 用户性别枚举 + */ +export enum UserGender { + /** 未知 */ + UNKNOWN = 0, + /** 男 */ + MALE = 1, + /** 女 */ + FEMALE = 2, +} diff --git a/src/utils/dom.ts b/src/utils/dom.ts new file mode 100644 index 00000000..9c27924c --- /dev/null +++ b/src/utils/dom.ts @@ -0,0 +1,51 @@ +/** + * DOM 操作相关工具函数 + */ + +/** + * 检查元素是否包含指定 class + * @param ele HTML 元素 + * @param cls class 名称 + * @returns 是否包含 + * + * @example + * ```ts + * const hasActiveClass = hasClass(element, 'active'); + * ``` + */ +export function hasClass(ele: HTMLElement, cls: string): boolean { + return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)")); +} + +/** + * 为元素添加 class + * @param ele HTML 元素 + * @param cls class 名称 + * + * @example + * ```ts + * addClass(element, 'active'); + * ``` + */ +export function addClass(ele: HTMLElement, cls: string): void { + if (!hasClass(ele, cls)) { + ele.className += " " + cls; + } +} + +/** + * 从元素移除 class + * @param ele HTML 元素 + * @param cls class 名称 + * + * @example + * ```ts + * removeClass(element, 'active'); + * ``` + */ +export function removeClass(ele: HTMLElement, cls: string): void { + if (hasClass(ele, cls)) { + const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)"); + ele.className = ele.className.replace(reg, " "); + } +} diff --git a/src/utils/download.ts b/src/utils/download.ts new file mode 100644 index 00000000..f1f6ac6a --- /dev/null +++ b/src/utils/download.ts @@ -0,0 +1,73 @@ +/** + * 文件下载工具函数 + */ + +/** + * 从响应头中提取文件名 + * @param contentDisposition Content-Disposition 响应头 + * @returns 解码后的文件名 + */ +function extractFileName(contentDisposition: string): string { + if (!contentDisposition) { + return `download_${Date.now()}`; + } + + // 尝试从 filename*=UTF-8'' 格式中提取 + const filenameRegex = /filename\*=UTF-8''(.+)/; + const matches = filenameRegex.exec(contentDisposition); + if (matches && matches[1]) { + return decodeURIComponent(matches[1]); + } + + // 尝试从 filename= 格式中提取 + const fallbackRegex = /filename=([^;]+)/; + const fallbackMatches = fallbackRegex.exec(contentDisposition); + if (fallbackMatches && fallbackMatches[1]) { + return decodeURI(fallbackMatches[1].replace(/"/g, "")); + } + + return `download_${Date.now()}`; +} + +/** + * 下载文件 + * @param response Axios 响应对象 + * @param customFileName 自定义文件名(可选) + * + * @example + * ```ts + * // 基础用法 + * const response = await UserAPI.export(queryParams); + * downloadFile(response); + * + * // 自定义文件名 + * downloadFile(response, "用户列表.xlsx"); + * ``` + */ +export function downloadFile(response: any, customFileName?: string): void { + try { + const fileData = response.data; + const contentDisposition = response.headers["content-disposition"]; + const fileName = customFileName || extractFileName(contentDisposition); + + // 创建 Blob 对象 + const blob = new Blob([fileData]); + + // 创建下载链接 + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = downloadUrl; + link.download = fileName; + + // 触发下载 + document.body.appendChild(link); + link.click(); + + // 清理 + document.body.removeChild(link); + window.URL.revokeObjectURL(downloadUrl); + } catch (error) { + console.error("文件下载失败:", error); + throw error; + } +} diff --git a/src/utils/format.ts b/src/utils/format.ts new file mode 100644 index 00000000..a6c59598 --- /dev/null +++ b/src/utils/format.ts @@ -0,0 +1,86 @@ +/** + * 数据格式化相关工具函数 + */ + +/** + * 格式化增长率 + * 保留两位小数,去掉末尾的 0,取绝对值 + * + * @param growthRate 增长率(小数形式,如 0.15 表示 15%) + * @returns 格式化后的增长率字符串 + * + * @example + * ```ts + * formatGrowthRate(0.1234); // "12.34%" + * formatGrowthRate(0.1000); // "10%" + * formatGrowthRate(0); // "-" + * formatGrowthRate(-0.05); // "5%"(取绝对值) + * ``` + */ +export function formatGrowthRate(growthRate: number): string { + if (growthRate === 0) { + return "-"; + } + + const formattedRate = Math.abs(growthRate * 100) + .toFixed(2) + .replace(/\.?0+$/, ""); + + return formattedRate + "%"; +} + +/** + * 格式化文件大小 + * @param bytes 字节数 + * @param decimals 保留小数位数,默认 2 + * @returns 格式化后的文件大小字符串 + * + * @example + * ```ts + * formatFileSize(1024); // "1 KB" + * formatFileSize(1048576); // "1 MB" + * formatFileSize(1234567); // "1.18 MB" + * ``` + */ +export function formatFileSize(bytes: number, decimals: number = 2): string { + if (bytes === 0) return "0 Bytes"; + + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + " " + sizes[i]; +} + +/** + * 格式化数字,添加千分位分隔符 + * @param num 数字 + * @returns 格式化后的字符串 + * + * @example + * ```ts + * formatNumber(1234567); // "1,234,567" + * formatNumber(1234567.89); // "1,234,567.89" + * ``` + */ +export function formatNumber(num: number): string { + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); +} + +/** + * 格式化金额(人民币) + * @param amount 金额 + * @param decimals 保留小数位数,默认 2 + * @returns 格式化后的金额字符串 + * + * @example + * ```ts + * formatCurrency(1234567); // "¥1,234,567.00" + * formatCurrency(1234567.8); // "¥1,234,567.80" + * formatCurrency(1234567, 0); // "¥1,234,567" + * ``` + */ +export function formatCurrency(amount: number, decimals: number = 2): string { + const formatted = amount.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return "¥" + formatted; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 67fce789..64f1209f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,58 +1,26 @@ /** - * Check if an element has a class - * @param {HTMLElement} ele - * @param {string} cls - * @returns {boolean} - */ -export function hasClass(ele: HTMLElement, cls: string) { - return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)")); -} - -/** - * Add class to element - * @param {HTMLElement} ele - * @param {string} cls - */ -export function addClass(ele: HTMLElement, cls: string) { - if (!hasClass(ele, cls)) ele.className += " " + cls; -} - -/** - * Remove class from element - * @param {HTMLElement} ele - * @param {string} cls - */ -export function removeClass(ele: HTMLElement, cls: string) { - if (hasClass(ele, cls)) { - const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)"); - ele.className = ele.className.replace(reg, " "); - } -} - -/** - * 判断是否是外部链接 + * 工具函数统一导出 * - * @param {string} path - * @returns {Boolean} + * 本文件作为 barrel export,统一管理所有工具函数的导出 + * 各类工具函数按功能分类存放在不同文件中: + * - dom.ts: DOM 操作相关 + * - validate.ts: 数据验证相关 + * - format.ts: 数据格式化相关 + * - download.ts: 文件下载相关 + * - auth.ts: 权限认证相关 + * - storage.ts: 本地存储相关 + * - request.ts: 网络请求相关 + * - theme.ts: 主题相关 */ -export function isExternal(path: string) { - const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path); - return isExternal; -} -/** - * 格式化增长率,保留两位小数 ,并且去掉末尾的0 取绝对值 - * - * @param growthRate - * @returns - */ -export function formatGrowthRate(growthRate: number) { - if (growthRate === 0) { - return "-"; - } +// DOM 操作 +export { hasClass, addClass, removeClass } from "./dom"; - const formattedRate = Math.abs(growthRate * 100) - .toFixed(2) - .replace(/\.?0+$/, ""); - return formattedRate + "%"; -} +// 数据验证 +export { isExternal, isValidURL, isEmail, isMobile } from "./validate"; + +// 数据格式化 +export { formatGrowthRate, formatFileSize, formatNumber, formatCurrency } from "./format"; + +// 文件下载 +export { downloadFile } from "./download"; diff --git a/src/utils/validate.ts b/src/utils/validate.ts new file mode 100644 index 00000000..88225f35 --- /dev/null +++ b/src/utils/validate.ts @@ -0,0 +1,59 @@ +/** + * 数据验证相关工具函数 + */ + +/** + * 判断是否是外部链接 + * @param path 路径字符串 + * @returns 是否是外部链接 + * + * @example + * ```ts + * isExternal('https://example.com'); // true + * isExternal('/dashboard'); // false + * isExternal('mailto:admin@example.com'); // true + * ``` + */ +export function isExternal(path: string): boolean { + return /^(https?:|http?:|mailto:|tel:)/.test(path); +} + +/** + * 判断是否是有效的 URL + * @param url URL 字符串 + * @returns 是否是有效 URL + * + * @example + * ```ts + * isValidURL('https://example.com'); // true + * isValidURL('not a url'); // false + * ``` + */ +export function isValidURL(url: string): boolean { + try { + new URL(url); + return true; + } catch { + return false; + } +} + +/** + * 判断是否是邮箱地址 + * @param email 邮箱字符串 + * @returns 是否是有效邮箱 + */ +export function isEmail(email: string): boolean { + const pattern = /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/; + return pattern.test(email); +} + +/** + * 判断是否是手机号码(中国大陆) + * @param mobile 手机号字符串 + * @returns 是否是有效手机号 + */ +export function isMobile(mobile: string): boolean { + const pattern = /^1[3|4|5|6|7|8|9][0-9]\d{8}$/; + return pattern.test(mobile); +} diff --git a/src/views/login/index.vue b/src/views/login/index.vue index 856f61ef..df723e32 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -1,7 +1,7 @@ @@ -52,25 +52,21 @@ import DarkModeSwitch from "@/components/DarkModeSwitch/index.vue"; type LayoutMap = "login" | "register" | "resetPwd"; -const t = useI18n().t; +const { t } = useI18n(); +const component = ref("login"); -const component = ref("login"); // 切换显示的组件 const formComponents = { login: defineAsyncComponent(() => import("./components/Login.vue")), register: defineAsyncComponent(() => import("./components/Register.vue")), resetPwd: defineAsyncComponent(() => import("./components/ResetPwd.vue")), }; -// 投票通知 -const voteUrl = "https://gitee.com/activity/2025opensource?ident=I6VXEH"; -// 保存通知实例,用于在组件卸载时关闭 let notificationInstance: ReturnType | null = null; -// 显示投票通知 const showVoteNotification = () => { notificationInstance = ElNotification({ title: "⭐ Gitee 2025 开源评选 · 诚邀您的支持! 🙏", - message: `我正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧!
点击投票 →`, + message: `我正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧!
点击投票 →`, type: "success", position: "bottom-right", duration: 0, @@ -78,14 +74,10 @@ const showVoteNotification = () => { }); }; -// 延迟显示 onMounted(() => { - setTimeout(() => { - showVoteNotification(); - }, 500); + setTimeout(showVoteNotification, 500); }); -// 组件卸载时关闭通知 onBeforeUnmount(() => { if (notificationInstance) { notificationInstance.close(); @@ -95,6 +87,8 @@ onBeforeUnmount(() => { diff --git a/src/views/system/user/components/DeptTree.vue b/src/views/system/user/components/UserDeptTree.vue similarity index 100% rename from src/views/system/user/components/DeptTree.vue rename to src/views/system/user/components/UserDeptTree.vue diff --git a/src/views/system/user/components/UserImport.vue b/src/views/system/user/components/UserImportDialog.vue similarity index 100% rename from src/views/system/user/components/UserImport.vue rename to src/views/system/user/components/UserImportDialog.vue diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue index b8b9bfd4..8ee23dd5 100644 --- a/src/views/system/user/index.vue +++ b/src/views/system/user/index.vue @@ -4,7 +4,7 @@ - + @@ -90,7 +90,7 @@ @@ -166,8 +166,8 @@ @@ -240,7 +240,7 @@ - + @@ -250,32 +250,35 @@ import { computed, onMounted, reactive, ref } from "vue"; import { useDebounceFn } from "@vueuse/core"; // ==================== 2. Element Plus ==================== -import { ElMessage, ElMessageBox } from "element-plus"; +import { ElMessage, ElMessageBox, type FormInstance } from "element-plus"; // ==================== 3. 类型定义 ==================== import type { UserForm, UserPageQuery, UserPageVO } from "@/api/system/user-api"; + +// ==================== 3.5 工具函数 ==================== +import { downloadFile } from "@/utils"; +import { VALIDATORS } from "@/constants"; // ==================== 4. API 服务 ==================== import UserAPI from "@/api/system/user-api"; import DeptAPI from "@/api/system/dept-api"; import RoleAPI from "@/api/system/role-api"; // ==================== 5. Store ==================== -import { useAppStore } from "@/store/modules/app-store"; -import { useUserStore } from "@/store"; +import { useUserStore, useAppStore } from "@/store"; // ==================== 6. Enums ==================== -import { DeviceEnum } from "@/enums/settings/device-enum"; +import { DeviceEnum, DialogMode, CommonStatus } from "@/enums"; // ==================== 7. Composables ==================== import { useAiAction, useTableSelection } from "@/composables"; // ==================== 8. 组件 ==================== -import DeptTree from "./components/DeptTree.vue"; -import UserImport from "./components/UserImport.vue"; +import UserDeptTree from "./components/UserDeptTree.vue"; +import UserImportDialog from "./components/UserImportDialog.vue"; // ==================== 组件配置 ==================== defineOptions({ - name: "SystemUser", + name: "User", inheritAttrs: false, }); @@ -286,8 +289,8 @@ const userStore = useUserStore(); // ==================== 响应式状态 ==================== // DOM 引用 -const queryFormRef = ref(); -const userFormRef = ref(); +const queryFormRef = ref(); +const userFormRef = ref(); // 列表查询参数 const queryParams = reactive({ @@ -296,20 +299,24 @@ const queryParams = reactive({ }); // 列表数据 -const pageData = ref(); +const userList = ref([]); const total = ref(0); const loading = ref(false); // 弹窗状态 -const dialog = reactive({ +const dialogState = reactive({ visible: false, title: "新增用户", + mode: DialogMode.CREATE, }); +// 初始表单数据 +const initialFormData: UserForm = { + status: CommonStatus.ENABLED, +}; + // 表单数据 -const formData = reactive({ - status: 1, -}); +const formData = reactive({ ...initialFormData }); // 下拉选项数据 const deptOptions = ref(); @@ -328,48 +335,12 @@ const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600 // ==================== 表单验证规则 ==================== const rules = reactive({ - username: [ - { - required: true, - message: "用户名不能为空", - trigger: "blur", - }, - ], - nickname: [ - { - required: true, - message: "用户昵称不能为空", - trigger: "blur", - }, - ], - deptId: [ - { - required: true, - message: "所属部门不能为空", - trigger: "blur", - }, - ], - roleIds: [ - { - required: true, - message: "用户角色不能为空", - trigger: "blur", - }, - ], - email: [ - { - pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/, - message: "请输入正确的邮箱地址", - trigger: "blur", - }, - ], - mobile: [ - { - pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, - message: "请输入正确的手机号码", - trigger: "blur", - }, - ], + username: [VALIDATORS.required("用户名不能为空")], + nickname: [VALIDATORS.required("用户昵称不能为空")], + deptId: [VALIDATORS.required("所属部门不能为空")], + roleIds: [VALIDATORS.required("用户角色不能为空")], + email: [VALIDATORS.email], + mobile: [VALIDATORS.mobile], }); // ==================== 数据加载 ==================== @@ -381,7 +352,7 @@ async function fetchUserList(): Promise { loading.value = true; try { const data = await UserAPI.getPage(queryParams); - pageData.value = data.list; + userList.value = data.list; total.value = data.total; } catch (error) { ElMessage.error("获取用户列表失败"); @@ -408,7 +379,7 @@ function handleQuery(): Promise { * 重置查询条件 */ function handleResetQuery(): void { - queryFormRef.value.resetFields(); + queryFormRef.value?.resetFields(); queryParams.deptId = undefined; queryParams.createTime = undefined; handleQuery(); @@ -446,7 +417,7 @@ function handleResetPassword(row: UserPageVO): void { * @param id 用户ID(编辑时传入) */ async function handleOpenDialog(id?: string): Promise { - dialog.visible = true; + dialogState.visible = true; // 并行加载下拉选项数据 try { @@ -461,7 +432,8 @@ async function handleOpenDialog(id?: string): Promise { // 编辑:加载用户数据 if (id) { - dialog.title = "修改用户"; + dialogState.title = "修改用户"; + dialogState.mode = DialogMode.EDIT; try { const data = await UserAPI.getFormData(id); Object.assign(formData, data); @@ -471,7 +443,8 @@ async function handleOpenDialog(id?: string): Promise { } } else { // 新增:设置默认值 - dialog.title = "新增用户"; + dialogState.title = "新增用户"; + dialogState.mode = DialogMode.CREATE; } } @@ -479,20 +452,21 @@ async function handleOpenDialog(id?: string): Promise { * 关闭用户表单弹窗 */ function handleCloseDialog(): void { - dialog.visible = false; - userFormRef.value.resetFields(); - userFormRef.value.clearValidate(); + dialogState.visible = false; - // 重置表单数据 - formData.id = undefined; - formData.status = 1; + // 安全地重置表单 + userFormRef.value?.resetFields(); + userFormRef.value?.clearValidate(); + + // 完全重置表单数据 + Object.assign(formData, initialFormData); } /** * 提交用户表单(防抖) */ const handleSubmit = useDebounceFn(async () => { - const valid = await userFormRef.value.validate().catch(() => false); + const valid = await userFormRef.value?.validate().catch(() => false); if (!valid) return; const userId = formData.id; @@ -514,14 +488,14 @@ const handleSubmit = useDebounceFn(async () => { } finally { loading.value = false; } -}, 1000); +}, 300); /** * 删除用户 * @param id 用户ID(单个删除时传入) */ function handleDelete(id?: string): void { - const userIds = id ? id : selectedIds.value.join(","); + const userIds = id ?? selectedIds.value.join(","); if (!userIds) { ElMessage.warning("请勾选删除项"); @@ -579,27 +553,7 @@ function handleOpenImportDialog(): void { async function handleExport(): Promise { try { const response = await UserAPI.export(queryParams); - const fileData = response.data; - const contentDisposition = response.headers["content-disposition"]; - const fileName = decodeURI(contentDisposition.split(";")[1].split("=")[1]); - const fileType = - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; - - // 创建下载链接 - const blob = new Blob([fileData], { type: fileType }); - const downloadUrl = window.URL.createObjectURL(blob); - const downloadLink = document.createElement("a"); - downloadLink.href = downloadUrl; - downloadLink.download = fileName; - - // 触发下载 - document.body.appendChild(downloadLink); - downloadLink.click(); - - // 清理 - document.body.removeChild(downloadLink); - window.URL.revokeObjectURL(downloadUrl); - + downloadFile(response); ElMessage.success("导出成功"); } catch (error) { ElMessage.error("导出失败"); @@ -656,9 +610,6 @@ useAiAction({ /** * 组件挂载时初始化数据 - * - * 注意:这里会先加载列表数据,如果 URL 中有 AI 参数(如搜索关键字), - * useAiAction 会在 nextTick 中再次执行搜索,这是预期行为 */ onMounted(() => { handleQuery();