feat(utils): add common utility functions and validation constants
This commit is contained in:
@@ -72,3 +72,91 @@ export const ALL_STORAGE_KEYS = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS];
|
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;
|
||||||
|
|||||||
12
src/enums/common/dialog-enum.ts
Normal file
12
src/enums/common/dialog-enum.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 通用对话框模式枚举
|
||||||
|
* @description 定义对话框的操作模式(创建、编辑、查看)
|
||||||
|
*/
|
||||||
|
export enum DialogMode {
|
||||||
|
/** 创建模式 - 新增数据 */
|
||||||
|
CREATE = "create",
|
||||||
|
/** 编辑模式 - 修改数据 */
|
||||||
|
EDIT = "edit",
|
||||||
|
/** 查看模式 - 只读展示 */
|
||||||
|
VIEW = "view",
|
||||||
|
}
|
||||||
22
src/enums/common/status-enum.ts
Normal file
22
src/enums/common/status-enum.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 通用状态枚举
|
||||||
|
* 适用于大多数业务实体的启用/禁用状态
|
||||||
|
*/
|
||||||
|
export enum CommonStatus {
|
||||||
|
/** 禁用 */
|
||||||
|
DISABLED = 0,
|
||||||
|
/** 启用 */
|
||||||
|
ENABLED = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核状态枚举
|
||||||
|
*/
|
||||||
|
export enum AuditStatus {
|
||||||
|
/** 待审核 */
|
||||||
|
PENDING = 0,
|
||||||
|
/** 已通过 */
|
||||||
|
APPROVED = 1,
|
||||||
|
/** 已拒绝 */
|
||||||
|
REJECTED = 2,
|
||||||
|
}
|
||||||
@@ -8,4 +8,8 @@ export * from "./settings/theme-enum";
|
|||||||
export * from "./settings/locale-enum";
|
export * from "./settings/locale-enum";
|
||||||
export * from "./settings/device-enum";
|
export * from "./settings/device-enum";
|
||||||
|
|
||||||
|
export * from "./common/dialog-enum";
|
||||||
|
export * from "./common/status-enum";
|
||||||
|
|
||||||
export * from "./system/menu-enum";
|
export * from "./system/menu-enum";
|
||||||
|
export * from "./system/user-enum";
|
||||||
|
|||||||
11
src/enums/system/user-enum.ts
Normal file
11
src/enums/system/user-enum.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* 用户性别枚举
|
||||||
|
*/
|
||||||
|
export enum UserGender {
|
||||||
|
/** 未知 */
|
||||||
|
UNKNOWN = 0,
|
||||||
|
/** 男 */
|
||||||
|
MALE = 1,
|
||||||
|
/** 女 */
|
||||||
|
FEMALE = 2,
|
||||||
|
}
|
||||||
51
src/utils/dom.ts
Normal file
51
src/utils/dom.ts
Normal file
@@ -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, " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/utils/download.ts
Normal file
73
src/utils/download.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/utils/format.ts
Normal file
86
src/utils/format.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
* 本文件作为 barrel export,统一管理所有工具函数的导出
|
||||||
* @returns {Boolean}
|
* 各类工具函数按功能分类存放在不同文件中:
|
||||||
|
* - 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// DOM 操作
|
||||||
* 格式化增长率,保留两位小数 ,并且去掉末尾的0 取绝对值
|
export { hasClass, addClass, removeClass } from "./dom";
|
||||||
*
|
|
||||||
* @param growthRate
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function formatGrowthRate(growthRate: number) {
|
|
||||||
if (growthRate === 0) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedRate = Math.abs(growthRate * 100)
|
// 数据验证
|
||||||
.toFixed(2)
|
export { isExternal, isValidURL, isEmail, isMobile } from "./validate";
|
||||||
.replace(/\.?0+$/, "");
|
|
||||||
return formattedRate + "%";
|
// 数据格式化
|
||||||
}
|
export { formatGrowthRate, formatFileSize, formatNumber, formatCurrency } from "./format";
|
||||||
|
|
||||||
|
// 文件下载
|
||||||
|
export { downloadFile } from "./download";
|
||||||
|
|||||||
59
src/utils/validate.ts
Normal file
59
src/utils/validate.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<!-- 右侧切换主题、语言按钮 -->
|
<!-- 右侧切换主题、语言按钮 -->
|
||||||
<div class="action-bar">
|
<div class="login-toolbar">
|
||||||
<el-tooltip :content="t('login.themeToggle')" placement="bottom">
|
<el-tooltip :content="t('login.themeToggle')" placement="bottom">
|
||||||
<CommonWrapper>
|
<CommonWrapper>
|
||||||
<DarkModeSwitch />
|
<DarkModeSwitch />
|
||||||
@@ -14,16 +14,14 @@
|
|||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<!-- 登录页主体 -->
|
<!-- 登录页主体 -->
|
||||||
<div flex-1 flex-center>
|
<div class="login-content">
|
||||||
<div
|
<div class="login-card">
|
||||||
class="p-4xl w-full h-auto sm:w-450px sm:h-700px shadow-[var(--el-box-shadow-light)] border-rd-2"
|
<div class="login-card__inner">
|
||||||
>
|
|
||||||
<div w-full flex flex-col items-center>
|
|
||||||
<!-- logo -->
|
<!-- logo -->
|
||||||
<el-image :src="logo" style="width: 84px" />
|
<el-image :src="logo" class="w-84px h-auto" />
|
||||||
|
|
||||||
<!-- 标题 -->
|
<!-- 标题 -->
|
||||||
<h2>
|
<h2 class="my-4">
|
||||||
<el-badge :value="`v ${defaultSettings.version}`" type="success">
|
<el-badge :value="`v ${defaultSettings.version}`" type="success">
|
||||||
{{ defaultSettings.title }}
|
{{ defaultSettings.title }}
|
||||||
</el-badge>
|
</el-badge>
|
||||||
@@ -36,10 +34,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 登录页底部版权 -->
|
<!-- 登录页底部版权 -->
|
||||||
<el-text size="small" class="py-2.5! fixed bottom-0 text-center">
|
<footer class="login-footer">
|
||||||
Copyright © 2021 - 2025 youlai.tech All Rights Reserved.
|
<el-text size="small">
|
||||||
<a href="http://beian.miit.gov.cn/" target="_blank">皖ICP备20006496号-2</a>
|
Copyright © 2021 - 2025 youlai.tech All Rights Reserved.
|
||||||
</el-text>
|
<a href="http://beian.miit.gov.cn/" target="_blank">皖ICP备20006496号-2</a>
|
||||||
|
</el-text>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -52,25 +52,21 @@ import DarkModeSwitch from "@/components/DarkModeSwitch/index.vue";
|
|||||||
|
|
||||||
type LayoutMap = "login" | "register" | "resetPwd";
|
type LayoutMap = "login" | "register" | "resetPwd";
|
||||||
|
|
||||||
const t = useI18n().t;
|
const { t } = useI18n();
|
||||||
|
const component = ref<LayoutMap>("login");
|
||||||
|
|
||||||
const component = ref<LayoutMap>("login"); // 切换显示的组件
|
|
||||||
const formComponents = {
|
const formComponents = {
|
||||||
login: defineAsyncComponent(() => import("./components/Login.vue")),
|
login: defineAsyncComponent(() => import("./components/Login.vue")),
|
||||||
register: defineAsyncComponent(() => import("./components/Register.vue")),
|
register: defineAsyncComponent(() => import("./components/Register.vue")),
|
||||||
resetPwd: defineAsyncComponent(() => import("./components/ResetPwd.vue")),
|
resetPwd: defineAsyncComponent(() => import("./components/ResetPwd.vue")),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 投票通知
|
|
||||||
const voteUrl = "https://gitee.com/activity/2025opensource?ident=I6VXEH";
|
|
||||||
// 保存通知实例,用于在组件卸载时关闭
|
|
||||||
let notificationInstance: ReturnType<typeof ElNotification> | null = null;
|
let notificationInstance: ReturnType<typeof ElNotification> | null = null;
|
||||||
|
|
||||||
// 显示投票通知
|
|
||||||
const showVoteNotification = () => {
|
const showVoteNotification = () => {
|
||||||
notificationInstance = ElNotification({
|
notificationInstance = ElNotification({
|
||||||
title: "⭐ Gitee 2025 开源评选 · 诚邀您的支持! 🙏",
|
title: "⭐ Gitee 2025 开源评选 · 诚邀您的支持! 🙏",
|
||||||
message: `我正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧!<br/><a href="${voteUrl}" target="_blank" style="color: var(--el-color-primary); text-decoration: none; font-weight: 500;">点击投票 →</a>`,
|
message: `我正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧!<br/><a href="https://gitee.com/activity/2025opensource?ident=I6VXEH" target="_blank" style="color: var(--el-color-primary); text-decoration: none; font-weight: 500;">点击投票 →</a>`,
|
||||||
type: "success",
|
type: "success",
|
||||||
position: "bottom-right",
|
position: "bottom-right",
|
||||||
duration: 0,
|
duration: 0,
|
||||||
@@ -78,14 +74,10 @@ const showVoteNotification = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 延迟显示
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeout(() => {
|
setTimeout(showVoteNotification, 500);
|
||||||
showVoteNotification();
|
|
||||||
}, 500);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 组件卸载时关闭通知
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (notificationInstance) {
|
if (notificationInstance) {
|
||||||
notificationInstance.close();
|
notificationInstance.close();
|
||||||
@@ -95,6 +87,8 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
$transition-duration: 0.3s;
|
||||||
|
$transition-offset: 30px;
|
||||||
.login-container {
|
.login-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -104,23 +98,20 @@ onBeforeUnmount(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: -1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
content: "";
|
||||||
|
background: url("@/assets/images/login-bg.svg") center center / cover;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加伪元素作为背景层
|
.login-toolbar {
|
||||||
.login-container::before {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: -1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
content: "";
|
|
||||||
background: url("@/assets/images/login-bg.svg");
|
|
||||||
background-position: center center;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar {
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
@@ -128,11 +119,9 @@ onBeforeUnmount(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
top: 10px;
|
|
||||||
right: auto;
|
right: auto;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
}
|
}
|
||||||
@@ -143,19 +132,52 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fade-slide */
|
.login-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 3rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: var(--el-box-shadow-light);
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
width: 450px;
|
||||||
|
height: 700px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 0.625rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.fade-slide-leave-active,
|
.fade-slide-leave-active,
|
||||||
.fade-slide-enter-active {
|
.fade-slide-enter-active {
|
||||||
transition: all 0.3s;
|
transition: all $transition-duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-slide-enter-from {
|
.fade-slide-enter-from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-30px);
|
transform: translateX(-$transition-offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-slide-leave-to {
|
.fade-slide-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(30px);
|
transform: translateX($transition-offset);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<!-- 部门树 -->
|
<!-- 部门树 -->
|
||||||
<el-col :lg="4" :xs="24" class="mb-[12px]">
|
<el-col :lg="4" :xs="24" class="mb-[12px]">
|
||||||
<DeptTree v-model="queryParams.deptId" @node-click="handleQuery" />
|
<UserDeptTree v-model="queryParams.deptId" @node-click="handleQuery" />
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<!-- 用户列表 -->
|
<!-- 用户列表 -->
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="pageData"
|
:data="userList"
|
||||||
border
|
border
|
||||||
stripe
|
stripe
|
||||||
highlight-current-row
|
highlight-current-row
|
||||||
@@ -111,8 +111,8 @@
|
|||||||
<el-table-column label="邮箱" align="center" prop="email" width="160" />
|
<el-table-column label="邮箱" align="center" prop="email" width="160" />
|
||||||
<el-table-column label="状态" align="center" prop="status" width="80">
|
<el-table-column label="状态" align="center" prop="status" width="80">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="scope.row.status == 1 ? 'success' : 'info'">
|
<el-tag :type="scope.row.status === CommonStatus.ENABLED ? 'success' : 'info'">
|
||||||
{{ scope.row.status == 1 ? "正常" : "禁用" }}
|
{{ scope.row.status === CommonStatus.ENABLED ? "正常" : "禁用" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -166,8 +166,8 @@
|
|||||||
|
|
||||||
<!-- 用户表单 -->
|
<!-- 用户表单 -->
|
||||||
<el-drawer
|
<el-drawer
|
||||||
v-model="dialog.visible"
|
v-model="dialogState.visible"
|
||||||
:title="dialog.title"
|
:title="dialogState.title"
|
||||||
append-to-body
|
append-to-body
|
||||||
:size="drawerSize"
|
:size="drawerSize"
|
||||||
@close="handleCloseDialog"
|
@close="handleCloseDialog"
|
||||||
@@ -225,8 +225,8 @@
|
|||||||
inline-prompt
|
inline-prompt
|
||||||
active-text="正常"
|
active-text="正常"
|
||||||
inactive-text="禁用"
|
inactive-text="禁用"
|
||||||
:active-value="1"
|
:active-value="CommonStatus.ENABLED"
|
||||||
:inactive-value="0"
|
:inactive-value="CommonStatus.DISABLED"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
|
||||||
<!-- 用户导入 -->
|
<!-- 用户导入 -->
|
||||||
<UserImport v-model="importDialogVisible" @import-success="handleQuery()" />
|
<UserImportDialog v-model="importDialogVisible" @import-success="handleQuery()" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -250,32 +250,35 @@ import { computed, onMounted, reactive, ref } from "vue";
|
|||||||
import { useDebounceFn } from "@vueuse/core";
|
import { useDebounceFn } from "@vueuse/core";
|
||||||
|
|
||||||
// ==================== 2. Element Plus ====================
|
// ==================== 2. Element Plus ====================
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox, type FormInstance } from "element-plus";
|
||||||
|
|
||||||
// ==================== 3. 类型定义 ====================
|
// ==================== 3. 类型定义 ====================
|
||||||
import type { UserForm, UserPageQuery, UserPageVO } from "@/api/system/user-api";
|
import type { UserForm, UserPageQuery, UserPageVO } from "@/api/system/user-api";
|
||||||
|
|
||||||
|
// ==================== 3.5 工具函数 ====================
|
||||||
|
import { downloadFile } from "@/utils";
|
||||||
|
import { VALIDATORS } from "@/constants";
|
||||||
// ==================== 4. API 服务 ====================
|
// ==================== 4. API 服务 ====================
|
||||||
import UserAPI from "@/api/system/user-api";
|
import UserAPI from "@/api/system/user-api";
|
||||||
import DeptAPI from "@/api/system/dept-api";
|
import DeptAPI from "@/api/system/dept-api";
|
||||||
import RoleAPI from "@/api/system/role-api";
|
import RoleAPI from "@/api/system/role-api";
|
||||||
|
|
||||||
// ==================== 5. Store ====================
|
// ==================== 5. Store ====================
|
||||||
import { useAppStore } from "@/store/modules/app-store";
|
import { useUserStore, useAppStore } from "@/store";
|
||||||
import { useUserStore } from "@/store";
|
|
||||||
|
|
||||||
// ==================== 6. Enums ====================
|
// ==================== 6. Enums ====================
|
||||||
import { DeviceEnum } from "@/enums/settings/device-enum";
|
import { DeviceEnum, DialogMode, CommonStatus } from "@/enums";
|
||||||
|
|
||||||
// ==================== 7. Composables ====================
|
// ==================== 7. Composables ====================
|
||||||
import { useAiAction, useTableSelection } from "@/composables";
|
import { useAiAction, useTableSelection } from "@/composables";
|
||||||
|
|
||||||
// ==================== 8. 组件 ====================
|
// ==================== 8. 组件 ====================
|
||||||
import DeptTree from "./components/DeptTree.vue";
|
import UserDeptTree from "./components/UserDeptTree.vue";
|
||||||
import UserImport from "./components/UserImport.vue";
|
import UserImportDialog from "./components/UserImportDialog.vue";
|
||||||
|
|
||||||
// ==================== 组件配置 ====================
|
// ==================== 组件配置 ====================
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "SystemUser",
|
name: "User",
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -286,8 +289,8 @@ const userStore = useUserStore();
|
|||||||
// ==================== 响应式状态 ====================
|
// ==================== 响应式状态 ====================
|
||||||
|
|
||||||
// DOM 引用
|
// DOM 引用
|
||||||
const queryFormRef = ref();
|
const queryFormRef = ref<FormInstance>();
|
||||||
const userFormRef = ref();
|
const userFormRef = ref<FormInstance>();
|
||||||
|
|
||||||
// 列表查询参数
|
// 列表查询参数
|
||||||
const queryParams = reactive<UserPageQuery>({
|
const queryParams = reactive<UserPageQuery>({
|
||||||
@@ -296,20 +299,24 @@ const queryParams = reactive<UserPageQuery>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 列表数据
|
// 列表数据
|
||||||
const pageData = ref<UserPageVO[]>();
|
const userList = ref<UserPageVO[]>([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
// 弹窗状态
|
// 弹窗状态
|
||||||
const dialog = reactive({
|
const dialogState = reactive({
|
||||||
visible: false,
|
visible: false,
|
||||||
title: "新增用户",
|
title: "新增用户",
|
||||||
|
mode: DialogMode.CREATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 初始表单数据
|
||||||
|
const initialFormData: UserForm = {
|
||||||
|
status: CommonStatus.ENABLED,
|
||||||
|
};
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const formData = reactive<UserForm>({
|
const formData = reactive<UserForm>({ ...initialFormData });
|
||||||
status: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 下拉选项数据
|
// 下拉选项数据
|
||||||
const deptOptions = ref<OptionType[]>();
|
const deptOptions = ref<OptionType[]>();
|
||||||
@@ -328,48 +335,12 @@ const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600
|
|||||||
// ==================== 表单验证规则 ====================
|
// ==================== 表单验证规则 ====================
|
||||||
|
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
username: [
|
username: [VALIDATORS.required("用户名不能为空")],
|
||||||
{
|
nickname: [VALIDATORS.required("用户昵称不能为空")],
|
||||||
required: true,
|
deptId: [VALIDATORS.required("所属部门不能为空")],
|
||||||
message: "用户名不能为空",
|
roleIds: [VALIDATORS.required("用户角色不能为空")],
|
||||||
trigger: "blur",
|
email: [VALIDATORS.email],
|
||||||
},
|
mobile: [VALIDATORS.mobile],
|
||||||
],
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==================== 数据加载 ====================
|
// ==================== 数据加载 ====================
|
||||||
@@ -381,7 +352,7 @@ async function fetchUserList(): Promise<void> {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const data = await UserAPI.getPage(queryParams);
|
const data = await UserAPI.getPage(queryParams);
|
||||||
pageData.value = data.list;
|
userList.value = data.list;
|
||||||
total.value = data.total;
|
total.value = data.total;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error("获取用户列表失败");
|
ElMessage.error("获取用户列表失败");
|
||||||
@@ -408,7 +379,7 @@ function handleQuery(): Promise<void> {
|
|||||||
* 重置查询条件
|
* 重置查询条件
|
||||||
*/
|
*/
|
||||||
function handleResetQuery(): void {
|
function handleResetQuery(): void {
|
||||||
queryFormRef.value.resetFields();
|
queryFormRef.value?.resetFields();
|
||||||
queryParams.deptId = undefined;
|
queryParams.deptId = undefined;
|
||||||
queryParams.createTime = undefined;
|
queryParams.createTime = undefined;
|
||||||
handleQuery();
|
handleQuery();
|
||||||
@@ -446,7 +417,7 @@ function handleResetPassword(row: UserPageVO): void {
|
|||||||
* @param id 用户ID(编辑时传入)
|
* @param id 用户ID(编辑时传入)
|
||||||
*/
|
*/
|
||||||
async function handleOpenDialog(id?: string): Promise<void> {
|
async function handleOpenDialog(id?: string): Promise<void> {
|
||||||
dialog.visible = true;
|
dialogState.visible = true;
|
||||||
|
|
||||||
// 并行加载下拉选项数据
|
// 并行加载下拉选项数据
|
||||||
try {
|
try {
|
||||||
@@ -461,7 +432,8 @@ async function handleOpenDialog(id?: string): Promise<void> {
|
|||||||
|
|
||||||
// 编辑:加载用户数据
|
// 编辑:加载用户数据
|
||||||
if (id) {
|
if (id) {
|
||||||
dialog.title = "修改用户";
|
dialogState.title = "修改用户";
|
||||||
|
dialogState.mode = DialogMode.EDIT;
|
||||||
try {
|
try {
|
||||||
const data = await UserAPI.getFormData(id);
|
const data = await UserAPI.getFormData(id);
|
||||||
Object.assign(formData, data);
|
Object.assign(formData, data);
|
||||||
@@ -471,7 +443,8 @@ async function handleOpenDialog(id?: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 新增:设置默认值
|
// 新增:设置默认值
|
||||||
dialog.title = "新增用户";
|
dialogState.title = "新增用户";
|
||||||
|
dialogState.mode = DialogMode.CREATE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,20 +452,21 @@ async function handleOpenDialog(id?: string): Promise<void> {
|
|||||||
* 关闭用户表单弹窗
|
* 关闭用户表单弹窗
|
||||||
*/
|
*/
|
||||||
function handleCloseDialog(): void {
|
function handleCloseDialog(): void {
|
||||||
dialog.visible = false;
|
dialogState.visible = false;
|
||||||
userFormRef.value.resetFields();
|
|
||||||
userFormRef.value.clearValidate();
|
|
||||||
|
|
||||||
// 重置表单数据
|
// 安全地重置表单
|
||||||
formData.id = undefined;
|
userFormRef.value?.resetFields();
|
||||||
formData.status = 1;
|
userFormRef.value?.clearValidate();
|
||||||
|
|
||||||
|
// 完全重置表单数据
|
||||||
|
Object.assign(formData, initialFormData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交用户表单(防抖)
|
* 提交用户表单(防抖)
|
||||||
*/
|
*/
|
||||||
const handleSubmit = useDebounceFn(async () => {
|
const handleSubmit = useDebounceFn(async () => {
|
||||||
const valid = await userFormRef.value.validate().catch(() => false);
|
const valid = await userFormRef.value?.validate().catch(() => false);
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
|
|
||||||
const userId = formData.id;
|
const userId = formData.id;
|
||||||
@@ -514,14 +488,14 @@ const handleSubmit = useDebounceFn(async () => {
|
|||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 300);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除用户
|
* 删除用户
|
||||||
* @param id 用户ID(单个删除时传入)
|
* @param id 用户ID(单个删除时传入)
|
||||||
*/
|
*/
|
||||||
function handleDelete(id?: string): void {
|
function handleDelete(id?: string): void {
|
||||||
const userIds = id ? id : selectedIds.value.join(",");
|
const userIds = id ?? selectedIds.value.join(",");
|
||||||
|
|
||||||
if (!userIds) {
|
if (!userIds) {
|
||||||
ElMessage.warning("请勾选删除项");
|
ElMessage.warning("请勾选删除项");
|
||||||
@@ -579,27 +553,7 @@ function handleOpenImportDialog(): void {
|
|||||||
async function handleExport(): Promise<void> {
|
async function handleExport(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const response = await UserAPI.export(queryParams);
|
const response = await UserAPI.export(queryParams);
|
||||||
const fileData = response.data;
|
downloadFile(response);
|
||||||
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);
|
|
||||||
|
|
||||||
ElMessage.success("导出成功");
|
ElMessage.success("导出成功");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error("导出失败");
|
ElMessage.error("导出失败");
|
||||||
@@ -656,9 +610,6 @@ useAiAction({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件挂载时初始化数据
|
* 组件挂载时初始化数据
|
||||||
*
|
|
||||||
* 注意:这里会先加载列表数据,如果 URL 中有 AI 参数(如搜索关键字),
|
|
||||||
* useAiAction 会在 nextTick 中再次执行搜索,这是预期行为
|
|
||||||
*/
|
*/
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
handleQuery();
|
handleQuery();
|
||||||
|
|||||||
Reference in New Issue
Block a user