* ============================================================================
* 通用工具函数库
* ============================================================================
*/
* 模态框控制器
*/
const Modal = {
overlay: null,
currentOnConfirm: null,
* 初始化模态框
*/
init() {
this.overlay = document.createElement('div');
this.overlay.className = 'modal-overlay';
this.overlay.id = 'global-modal-overlay';
document.body.appendChild(this.overlay);
},
* 显示模态框
* @param {Object} options - 配置选项
* @param {string} options.title - 标题
* @param {string} options.content - HTML内容
* @param {Function} options.onConfirm - 确认回调
* @param {Function} options.onCancel - 取消回调
* @param {boolean} options.showCancel - 是否显示取消按钮
* @param {string} options.confirmText - 确认按钮文字
* @param {string} options.cancelText - 取消按钮文字
* @param {boolean} options.danger - 危险操作(红色按钮)
*/
show(options) {
if (!this.overlay) this.init();
const {
title = '提示',
content = '',
onConfirm = null,
onCancel = null,
showCancel = true,
confirmText = '确认',
cancelText = '取消',
danger = false
} = options;
this.currentOnConfirm = onConfirm;
const confirmBtnClass = danger ? 'btn btn-danger' : 'btn btn-primary';
this.overlay.innerHTML = `
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">${title}</h3>
<button class="modal-close" data-action="close">×</button>
</div>
<div class="modal-body">${content}</div>
<div class="modal-footer">
${showCancel ? `<button class="btn btn-secondary" data-action="cancel">${cancelText}</button>` : ''}
<button class="${confirmBtnClass}" data-action="confirm">${confirmText}</button>
</div>
</div>
`;
this.overlay.addEventListener('click', (e) => {
const action = e.target.dataset.action;
if (action === 'close' || e.target === this.overlay) {
this.hide();
if (onCancel) onCancel();
} else if (action === 'cancel') {
this.hide();
if (onCancel) onCancel();
} else if (action === 'confirm') {
let shouldClose = true;
if (this.currentOnConfirm) {
shouldClose = this.currentOnConfirm() !== false;
}
if (shouldClose) {
this.hide();
}
}
}, { once: true });
this.overlay.classList.add('active');
},
* 隐藏模态框
*/
hide() {
if (this.overlay) {
this.overlay.classList.remove('active');
}
},
* 显示确认对话框
* @param {string} message - 确认消息
* @param {Function} onConfirm - 确认回调
* @param {string} title - 标题
*/
confirm(message, onConfirm, title = '确认') {
this.show({
title,
content: `<p class="text-secondary">${message}</p>`,
onConfirm,
danger: true
});
},
* 显示提示对话框
* @param {string} message - 提示消息
* @param {Function} onConfirm - 确认回调
* @param {string} title - 标题
*/
alert(message, onConfirm = null, title = '提示') {
this.show({
title,
content: `<p class="text-secondary">${message}</p>`,
onConfirm,
showCancel: false
});
}
};
* Toast 消息提示
*/
const Toast = {
container: null,
queue: [],
isShowing: false,
* 初始化
*/
init() {
this.container = document.createElement('div');
this.container.className = 'toast-container';
document.body.appendChild(this.container);
},
* 显示提示
* @param {string} message - 消息内容
* @param {string} type - 类型: 'success' | 'error' | 'warning'
* @param {number} duration - 显示时长(ms)
*/
show(message, type = 'success', duration = 3000) {
if (!this.container) this.init();
const icons = {
success: '✓',
error: '✕',
warning: '⚠'
};
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
<span class="toast-icon">${icons[type]}</span>
<span class="toast-content">${message}</span>
`;
this.container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(120%)';
setTimeout(() => {
toast.remove();
}, 200);
}, duration);
},
success(message, duration) {
this.show(message, 'success', duration);
},
error(message, duration) {
this.show(message, 'error', duration);
},
warning(message, duration) {
this.show(message, 'warning', duration);
}
};
* 表单验证器
*/
const Validator = {
* 验证非空
* @param {*} value - 值
* @param {string} fieldName - 字段名
* @returns {Object} { valid: boolean, message: string }
*/
required(value, fieldName) {
if (!value || value.toString().trim() === '') {
return { valid: false, message: `${fieldName}不能为空` };
}
return { valid: true };
},
* 验证最小长度
* @param {string} value - 值
* @param {number} min - 最小长度
* @param {string} fieldName - 字段名
* @returns {Object} { valid: boolean, message: string }
*/
minLength(value, min, fieldName) {
if (value && value.length < min) {
return { valid: false, message: `${fieldName}至少需要${min}个字符` };
}
return { valid: true };
},
* 验证最大长度
* @param {string} value - 值
* @param {number} max - 最大长度
* @param {string} fieldName - 字段名
* @returns {Object} { valid: boolean, message: string }
*/
maxLength(value, max, fieldName) {
if (value && value.length > max) {
return { valid: false, message: `${fieldName}不能超过${max}个字符` };
}
return { valid: true };
},
* 验证URL格式
* @param {string} url - URL
* @returns {Object} { valid: boolean, message: string }
*/
url(url) {
if (!url || url.trim() === '') {
return { valid: true };
}
try {
new URL(url);
return { valid: true };
} catch {
return { valid: false, message: 'URL格式不正确' };
}
},
* 验证邮箱格式
* @param {string} email - 邮箱
* @returns {Object} { valid: boolean, message: string }
*/
email(email) {
if (!email || email.trim() === '') {
return { valid: true };
}
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!pattern.test(email)) {
return { valid: false, message: '邮箱格式不正确' };
}
return { valid: true };
},
* 验证IP地址
* @param {string} ip - IP地址
* @returns {Object} { valid: boolean, message: string }
*/
ip(ip) {
if (!ip || ip.trim() === '') {
return { valid: true };
}
const pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
if (!pattern.test(ip)) {
return { valid: false, message: 'IP地址格式不正确' };
}
const parts = ip.split('.');
if (parts.some(part => parseInt(part) > 255)) {
return { valid: false, message: 'IP地址不正确' };
}
return { valid: true };
},
* 验证端口号
* @param {number|string} port - 端口号
* @returns {Object} { valid: boolean, message: string }
*/
port(port) {
const num = parseInt(port);
if (!port || isNaN(num) || num < 1 || num > 65535) {
return { valid: false, message: '端口号必须在 1-65535 之间' };
}
return { valid: true };
},
* 验证表单
* @param {Object} rules - 验证规则 { fieldName: [{ rule, value, name }] }
* @returns {Object} { valid: boolean, errors: Object }
*/
validate(rules) {
const errors = {};
let valid = true;
for (const [field, ruleList] of Object.entries(rules)) {
for (const rule of ruleList) {
const { rule: validator, value, name } = rule;
const result = validator(value, name);
if (!result.valid) {
errors[field] = result.message;
valid = false;
break;
}
}
}
return { valid, errors };
},
* 显示表单错误
* @param {Object} errors - 错误对象
* @param {string} prefix - 字段ID前缀
*/
showErrors(errors, prefix = '') {
for (const [field, message] of Object.entries(errors)) {
const element = document.getElementById(prefix + field);
if (element) {
element.classList.add('error');
}
Toast.error(message);
}
},
* 清除表单错误
* @param {string} prefix - 字段ID前缀
*/
clearErrors(prefix = '') {
const elements = document.querySelectorAll('[id^="' + prefix + '"].error');
elements.forEach(el => el.classList.remove('error'));
}
};
* 工具函数
*/
const Utils = {
* 防抖
* @param {Function} fn - 函数
* @param {number} delay - 延迟时间(ms)
* @returns {Function} 防抖后的函数
*/
debounce(fn, delay = 300) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
},
* 节流
* @param {Function} fn - 函数
* @param {number} delay - 延迟时间(ms)
* @returns {Function} 节流后的函数
*/
throttle(fn, delay = 300) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
fn.apply(this, args);
}
};
},
* 生成唯一ID
* @returns {string} 唯一ID
*/
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
},
* 格式化日期
* @param {Date|string} date - 日期对象或字符串
* @param {string} format - 格式: 'datetime' | 'date' | 'time'
* @returns {string} 格式化后的日期字符串
*/
formatDate(date, format = 'datetime') {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.getSeconds()).padStart(2, '0');
if (format === 'date') {
return `${year}-${month}-${day}`;
}
if (format === 'time') {
return `${hours}:${minutes}:${seconds}`;
}
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
* 相对时间
* @param {Date|string} date - 日期
* @returns {string} 相对时间字符串
*/
relativeTime(date) {
const d = new Date(date);
const now = new Date();
const diff = Math.floor((now - d) / 1000);
if (diff < 60) return '刚刚';
if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`;
if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`;
if (diff < 604800) return `${Math.floor(diff / 86400)}天前`;
return this.formatDate(date, 'date');
},
* 格式化文件大小
* @param {number} bytes - 字节数
* @returns {string} 格式化后的大小
*/
formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
* 复制到剪贴板
* @param {string} text - 要复制的文本
* @returns {Promise<boolean>} 是否成功
*/
async copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
Toast.success('已复制到剪贴板');
return true;
} catch (e) {
console.error('Copy to clipboard failed:', e);
Toast.error('复制失败');
return false;
}
},
* 安全解析 JSON
* @param {string} str - JSON字符串
* @param {*} defaultValue - 默认值
* @returns {*} 解析结果
*/
safeJsonParse(str, defaultValue = null) {
try {
return JSON.parse(str);
} catch (e) {
return defaultValue;
}
},
* 搜索过滤
* @param {Array} items - 数据项数组
* @param {string} query - 搜索关键词
* @param {Array} fields - 要搜索的字段
* @returns {Array} 过滤后的数组
*/
filterItems(items, query, fields) {
if (!query || query.trim() === '') {
return items;
}
const lowerQuery = query.toLowerCase();
return items.filter(item => {
return fields.some(field => {
const value = item[field];
return value && value.toString().toLowerCase().includes(lowerQuery);
});
});
},
* 深拷贝对象
* @param {*} obj - 要拷贝的对象
* @returns {*} 拷贝后的对象
*/
deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof Array) return obj.map(item => this.deepClone(item));
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = this.deepClone(obj[key]);
}
}
return cloned;
},
* URL参数解析
* @param {string} url - URL字符串
* @returns {Object} 参数对象
*/
parseUrlParams(url = window.location.href) {
const params = {};
const queryString = url.split('?')[1];
if (!queryString) return params;
queryString.split('&').forEach(param => {
const [key, value] = param.split('=');
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
});
return params;
},
* 构建URL参数
* @param {Object} params - 参数对象
* @returns {string} URL参数字符串
*/
buildUrlParams(params) {
return Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&');
},
* 获取元素位置
* @param {HTMLElement} element - DOM元素
* @returns {Object} 位置信息
*/
getElementPosition(element) {
const rect = element.getBoundingClientRect();
return {
top: rect.top + window.pageYOffset,
left: rect.left + window.pageXOffset,
width: rect.width,
height: rect.height
};
},
* 平滑滚动到元素
* @param {HTMLElement|string} element - DOM元素或选择器
* @param {number} offset - 偏移量
*/
scrollToElement(element, offset = 0) {
const el = typeof element === 'string' ? document.querySelector(element) : element;
if (!el) return;
const position = this.getElementPosition(el);
window.scrollTo({
top: position.top - offset,
behavior: 'smooth'
});
},
* 下载文件
* @param {string} content - 文件内容
* @param {string} filename - 文件名
* @param {string} type - MIME类型
*/
downloadFile(content, filename, type = 'text/plain') {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
};
* DOM 工具
*/
const DOM = {
* 查询元素
* @param {string} selector - 选择器
* @param {HTMLElement} parent - 父元素
* @returns {HTMLElement|null} DOM元素
*/
$(selector, parent = document) {
return parent.querySelector(selector);
},
* 查询所有元素
* @param {string} selector - 选择器
* @param {HTMLElement} parent - 父元素
* @returns {NodeList} DOM元素列表
*/
$$(selector, parent = document) {
return parent.querySelectorAll(selector);
},
* 创建元素
* @param {string} tag - 标签名
* @param {Object} attributes - 属性对象
* @param {string} content - 内容
* @returns {HTMLElement} DOM元素
*/
create(tag, attributes = {}, content = '') {
const element = document.createElement(tag);
for (const [key, value] of Object.entries(attributes)) {
if (key === 'className') {
element.className = value;
} else if (key === 'style' && typeof value === 'object') {
Object.assign(element.style, value);
} else if (key.startsWith('on') && typeof value === 'function') {
element.addEventListener(key.slice(2).toLowerCase(), value);
} else {
element.setAttribute(key, value);
}
}
if (content) {
element.innerHTML = content;
}
return element;
},
* 添加类名
* @param {HTMLElement} element - DOM元素
* @param {string} className - 类名
*/
addClass(element, className) {
element.classList.add(className);
},
* 移除类名
* @param {HTMLElement} element - DOM元素
* @param {string} className - 类名
*/
removeClass(element, className) {
element.classList.remove(className);
},
* 切换类名
* @param {HTMLElement} element - DOM元素
* @param {string} className - 类名
* @param {boolean} force - 强制添加/移除
*/
toggleClass(element, className, force) {
element.classList.toggle(className, force);
},
* 设置/获取属性
* @param {HTMLElement} element - DOM元素
* @param {string} name - 属性名
* @param {*} value - 属性值
* @returns {string|undefined} 属性值
*/
attr(element, name, value) {
if (value === undefined) {
return element.getAttribute(name);
}
element.setAttribute(name, value);
}
};
window.Modal = Modal;
window.Toast = Toast;
window.Validator = Validator;
window.Utils = Utils;
window.DOM = DOM;