Files
template-MP/common/utils/tool.js
2025-11-05 16:20:06 +08:00

668 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const baseUrl = import.meta.env.VITE_BASE_URL
const assetsUrl = import.meta.env.VITE_ASSETSURL
/**
* 工具类 - 提供常用的工具方法
* @class Tool
*/
class Tool {
constructor() {
// 图标类型映射
this.ICON_TYPES = {
NONE: 0,
SUCCESS: 1,
LOADING: 2,
}
// 字体加载状态缓存
this.loadedFonts = new Set()
}
/**
* 文字轻提示
* @param {string} str 提示文字
* @param {number} [icon=0] 提示icon (0: none, 1: success, 2: loading)
* @param {number} [duration=1500] 提示时间(毫秒)
*/
alert(str, icon = this.ICON_TYPES.NONE, duration = 1500) {
return new Promise((resolve, reject) => {
if (!str && str !== 0) {
console.warn('alert方法需要提供提示文字')
return
}
const iconMap = {
[this.ICON_TYPES.NONE]: 'none',
[this.ICON_TYPES.SUCCESS]: 'success',
[this.ICON_TYPES.LOADING]: 'loading',
}
uni.showToast({
title: String(str),
icon: iconMap[icon] || 'none',
mask: true,
duration,
success: () => {
setTimeout(resolve, duration)
},
fail: reject,
})
})
}
/**
* 显示loading加载
* @param {string} [title=' '] 加载文案
* @param {boolean} [mask=true] 是否显示遮罩
*/
loading(title = ' ', mask = true) {
uni.showLoading({ title, mask })
}
/**
* 关闭loading提示框
*/
hideLoading() {
uni.hideLoading()
}
/**
* 统一处理URL格式确保以/开头
* @param {string} url 页面地址
* @returns {string} 格式化后的URL
* @private
*/
_formatUrl(url) {
if (!url || typeof url !== 'string') {
throw new Error('URL必须是字符串')
}
return url.startsWith('/') ? url : `/${url}`
}
/**
* 可返回跳转(导航到新页面)
* @param {string} url 页面地址
*/
navigateTo(url) {
const formattedUrl = this._formatUrl(url)
uni.navigateTo({
url: formattedUrl,
fail: err => {
console.warn('navigateTo失败尝试switchTab:', err)
uni.switchTab({ url: formattedUrl })
},
})
}
/**
* 不可返回跳转(重定向到新页面)
* @param {string} url 页面地址
*/
redirectTo(url) {
uni.redirectTo({ url: this._formatUrl(url) })
}
/**
* 清除页面栈跳转(重新启动到新页面)
* @param {string} url 页面地址
*/
reLaunch(url) {
uni.reLaunch({ url: this._formatUrl(url) })
}
/**
* 跳转tabBar页
* @param {string} url 页面地址
*/
switchTab(url) {
uni.switchTab({ url: this._formatUrl(url) })
}
/**
* 返回上一页面或指定页面
* @param {number} [delta=1] 返回的页面数
* @param {string} [fallbackUrl='/pages/index/index'] 无上一页时的回退地址
*/
navigateBack(delta = 1, fallbackUrl = '/pages/index/index') {
const pages = getCurrentPages()
if (pages.length <= 1) {
console.warn('无上一页,使用回退地址')
uni.reLaunch({ url: fallbackUrl })
} else {
uni.navigateBack({ delta })
}
}
/**
* 操作本地缓存
* @param {string} key 缓存键值
* @param {any} [value] 缓存数据,不传则为读取
* @returns {any|undefined} 读取操作时返回数据
*/
storage(key, value) {
if (typeof key !== 'string') {
throw new Error('key必须是字符串')
}
// 设置操作
if (value !== undefined && value !== null) {
uni.setStorageSync(key, value)
return
}
// 读取操作
if (key !== '#') {
return uni.getStorageSync(key)
}
// 特殊操作
if (key === '#') {
uni.clearStorageSync()
}
}
/**
* 删除指定缓存
* @param {string} key 要删除的缓存键
*/
removeStorage(key) {
if (typeof key !== 'string') {
throw new Error('key必须是字符串')
}
uni.removeStorageSync(key)
}
/**
* 获取缓存信息
* @returns {Object} 缓存信息
*/
getStorageInfo() {
return uni.getStorageInfoSync()
}
/**
* 复制文本到剪贴板
* @param {string} data 要复制的文本
* @returns {Promise<boolean>} 复制是否成功
*/
async copy(data) {
if (!data && data !== 0) {
this.alert('暂无内容')
return false
}
try {
await new Promise((resolve, reject) => {
uni.setClipboardData({
data: String(data),
success: resolve,
fail: reject,
})
})
this.alert('复制成功')
return true
} catch (error) {
console.error('复制失败:', error)
this.alert('复制失败,请重试')
return false
}
}
/**
* 导入外部字体
* @param {string} fontName 字体文件名(不含路径)
* @returns {Promise<boolean>} 字体加载是否成功
*/
async loadFont(fontName) {
if (!fontName || typeof fontName !== 'string') {
throw new Error('字体名称必须是字符串')
}
// 检查是否已加载过
if (this.loadedFonts.has(fontName)) {
return true
}
try {
const fontFamily = fontName.replace(/\.[^/.]+$/, '') // 移除文件扩展名
await new Promise((resolve, reject) => {
uni.loadFontFace({
family: fontFamily,
source: `url(${assetsUrl}${fontName})`,
global: true,
success: resolve,
fail: reject,
})
})
this.loadedFonts.add(fontName)
return true
} catch (error) {
console.error(`字体加载失败: ${fontName}`, error)
return false
}
}
/**
* 保存图片到相册
* @param {string} url 图片URL
* @returns {Promise<boolean>} 保存是否成功
*/
async saveImageToPhotos(url) {
if (!url) {
this.alert('图片地址不能为空')
return false
}
try {
// 检查权限
const { authSetting } = await new Promise((resolve, reject) => {
uni.getSetting({
success: resolve,
fail: reject,
})
})
if (!authSetting['scope.writePhotosAlbum']) {
// 请求权限
await new Promise((resolve, reject) => {
uni.authorize({
scope: 'scope.writePhotosAlbum',
success: resolve,
fail: reject,
})
})
}
// 获取图片信息
const { path } = await new Promise((resolve, reject) => {
uni.getImageInfo({
src: url,
success: resolve,
fail: reject,
})
})
// 保存到相册
await new Promise((resolve, reject) => {
uni.saveImageToPhotosAlbum({
filePath: path,
success: resolve,
fail: reject,
})
})
this.alert('已保存到相册')
return true
} catch (error) {
console.error('保存图片失败:', error)
if (error.errMsg && error.errMsg.includes('auth')) {
// 权限相关错误
await new Promise(resolve => {
uni.showModal({
title: '保存失败',
content: '请开启访问手机相册权限',
showCancel: false,
success: resolve,
})
})
uni.openSetting()
} else {
this.alert('保存失败,请重试')
}
return false
}
}
/**
* 防抖函数
* @param {Function} func 要防抖的函数
* @param {number} wait 延迟时间(ms)
* @param {boolean} immediate 是否立即执行
* @returns {Function} 防抖后的函数
*/
debounce(func, wait, immediate = false) {
let timeout
return function (...args) {
const later = () => {
timeout = null
if (!immediate) func.apply(this, args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(this, args)
}
}
/**
* 节流函数
* @param {Function} func 要节流的函数
* @param {number} wait 延迟时间(ms)
* @returns {Function} 节流后的函数
*/
throttle(func, wait) {
let timeout
let previous = 0
return function (...args) {
const now = Date.now()
const remaining = wait - (now - previous)
const context = this
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
func.apply(context, args)
} else if (!timeout) {
timeout = setTimeout(() => {
previous = Date.now()
timeout = null
func.apply(context, args)
}, remaining)
}
}
}
/**
* 日期格式化
* @param {Date|string|number} date 日期对象、字符串或时间戳
* @param {string} fmt 格式字符串, 默认为 'YYYY-MM-DD HH:mm:ss'
* @returns {string} 格式化后的日期字符串
*/
formatDate(date, fmt = 'YYYY-MM-DD HH:mm:ss') {
// 如果传入的是时间戳转换为Date对象
if (typeof date === 'number') {
date = new Date(date)
} else if (typeof date === 'string') {
// 如果传入的是字符串尝试转换为Date对象
date = new Date(date)
}
if (!(date instanceof Date) || isNaN(date.getTime())) {
throw new Error('Invalid date')
}
const o = {
'Y+': date.getFullYear(),
'M+': date.getMonth() + 1,
'D+': date.getDate(),
'H+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds(),
'q+': Math.floor((date.getMonth() + 3) / 3),
S: date.getMilliseconds(),
}
for (let k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
let str = o[k] + ''
if (k === 'S') {
fmt = fmt.replace(RegExp.$1, str.padStart(3, '0'))
} else {
fmt = fmt.replace(RegExp.$1, str.length === 1 ? '0' + str : str)
}
}
}
return fmt
}
/**
* 深拷贝函数
* @param {*} obj 要深拷贝的对象
* @returns {*} 拷贝后的对象
*/
deepClone(obj) {
// 处理null、undefined和原始类型
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 clonedObj = {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = this.deepClone(obj[key])
}
}
return clonedObj
}
/**
* 表单验证工具
* @class Validator
*/
getValidator() {
const rules = {
/**
* 必填验证
* @param {*} value 值
* @param {boolean} required 是否必填
* @returns {boolean} 验证结果
*/
required(value, required = true) {
if (!required) return true
return value !== undefined && value !== null && value !== ''
},
/**
* 邮箱验证
* @param {string} value 值
* @returns {boolean} 验证结果
*/
email(value) {
if (!value) return true
const reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return reg.test(value)
},
/**
* 手机号验证
* @param {string} value 值
* @returns {boolean} 验证结果
*/
mobile(value) {
if (!value) return true
const reg = /^1[3-9]\d{9}$/
return reg.test(value)
},
/**
* 最小长度验证
* @param {string} value 值
* @param {number} length 最小长度
* @returns {boolean} 验证结果
*/
minLength(value, length) {
if (!value) return true
return value.length >= length
},
/**
* 最大长度验证
* @param {string} value 值
* @param {number} length 最大长度
* @returns {boolean} 验证结果
*/
maxLength(value, length) {
if (!value) return true
return value.length <= length
},
/**
* 数值范围验证
* @param {number} value 值
* @param {number} min 最小值
* @param {number} max 最大值
* @returns {boolean} 验证结果
*/
range(value, min, max) {
if (value === undefined || value === null) return true
return value >= min && value <= max
},
}
return {
/**
* 验证单个字段
* @param {*} value 值
* @param {Object} rule 规则
* @returns {string|null} 错误信息
*/
validateField(value, rule) {
// 必填验证
if (rule.required !== undefined && !rules.required(value, rule.required)) {
return rule.message || '此字段为必填项'
}
// 如果值为空且非必填,跳过其他验证
if (value === undefined || value === null || value === '') {
return null
}
// 其他验证规则
if (rule.type && rules[rule.type]) {
if (!rules[rule.type](value, rule.param1, rule.param2)) {
return rule.message || '字段格式不正确'
}
}
return null
},
/**
* 验证整个表单
* @param {Object} data 表单数据
* @param {Object} rules 验证规则
* @returns {Object} 验证结果 { isValid: boolean, errors: Object }
*/
validate(data, rules) {
const errors = {}
let isValid = true
for (let field in rules) {
const error = this.validateField(data[field], rules[field])
if (error) {
errors[field] = error
isValid = false
}
}
return { isValid, errors }
},
}
}
/**
* 微信支付
* @param {Object} paymentData 支付参数
* @returns {Promise<Object>} 支付结果
*/
requestPayment(paymentData) {
return new Promise((resolve, reject) => {
uni.requestPayment({
provider: 'wxpay',
...paymentData,
success: resolve,
fail: reject,
})
})
}
/**
* 文件上传
* @param {string} filePath 文件路径
* @returns {Promise<Object>} 上传结果
*/
upload(filePath) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: `${baseUrl}file/upload`,
fileType: 'image',
header: {
Authorization: `Bearer ${this.storage('token')}`,
},
filePath,
name: 'file',
success: ({ data }) => {
resolve(JSON.parse(data))
},
fail: error => {
reject(error)
},
})
})
}
/**
* 暗黑模式切换
* @param {boolean} isDark 是否启用暗黑模式
*/
toggleDarkMode(isDark) {
try {
if (isDark) {
// 启用暗黑模式
document.documentElement.setAttribute('data-theme', 'dark')
uni.setStorageSync('theme', 'dark')
} else {
// 启用亮色模式
document.documentElement.setAttribute('data-theme', 'light')
uni.setStorageSync('theme', 'light')
}
} catch (error) {
console.error('切换主题失败:', error)
}
}
/**
* 获取当前主题模式
* @returns {string} 当前主题 ('light' | 'dark')
*/
getCurrentTheme() {
try {
// 优先使用用户设置的主题
const savedTheme = uni.getStorageSync('theme')
if (savedTheme) {
return savedTheme
}
// 检查系统主题
const systemInfo = uni.getSystemInfoSync()
if (systemInfo.theme) {
return systemInfo.theme
}
// 默认返回亮色主题
return 'light'
} catch (error) {
console.error('获取主题失败:', error)
return 'light'
}
}
/**
* 权限检查
* @param {string} permission 权限标识
* @returns {boolean} 是否拥有权限
*/
hasPermission(permission) {
try {
// 获取用户权限列表
const permissions = uni.getStorageSync('permissions') || []
return permissions.includes(permission)
} catch (error) {
console.error('权限检查失败:', error)
return false
}
}
/**
* 设置用户权限
* @param {Array<string>} permissions 权限列表
*/
setPermissions(permissions) {
try {
uni.setStorageSync('permissions', Array.isArray(permissions) ? permissions : [])
} catch (error) {
console.error('设置权限失败:', error)
}
}
/**
* 获取用户所有权限
* @returns {Array<string>} 权限列表
*/
getUserPermissions() {
try {
return uni.getStorageSync('permissions') || []
} catch (error) {
console.error('获取权限失败:', error)
return []
}
}
/**
* 检查用户是否有任意权限
* @param {Array<string>} permissions 权限列表
* @returns {boolean} 是否有任意权限
*/
hasAnyPermission(permissions) {
if (!Array.isArray(permissions)) {
return false
}
const userPermissions = this.getUserPermissions()
return permissions.some(permission => userPermissions.includes(permission))
}
/**
* 检查用户是否拥有所有权限
* @param {Array<string>} permissions 权限列表
* @returns {boolean} 是否拥有所有权限
*/
hasAllPermissions(permissions) {
if (!Array.isArray(permissions)) {
return false
}
const userPermissions = this.getUserPermissions()
return permissions.every(permission => userPermissions.includes(permission))
}
/**
* 清除用户权限
*/
clearPermissions() {
try {
uni.removeStorageSync('permissions')
} catch (error) {
console.error('清除权限失败:', error)
}
}
}
// 创建单例并导出
export default new Tool()