a
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* 通用工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param {Date|string|number} timestamp 时间戳或日期字符串
|
||||
* @param {string} format 格式,默认:yyyy-MM-dd HH:mm:ss
|
||||
* @returns {string} 格式化后的时间字符串
|
||||
*/
|
||||
export const formatTime = (timestamp, format = 'yyyy-MM-dd HH:mm:ss') => {
|
||||
if (!timestamp) return ''
|
||||
|
||||
const date = new Date(timestamp)
|
||||
if (isNaN(date.getTime())) return ''
|
||||
|
||||
const map = {
|
||||
'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()
|
||||
}
|
||||
|
||||
if (/(y+)/.test(format)) {
|
||||
format = format.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
|
||||
}
|
||||
|
||||
for (const k in map) {
|
||||
if (new RegExp('(' + k + ')').test(format)) {
|
||||
format = format.replace(RegExp.$1,
|
||||
RegExp.$1.length === 1 ? map[k] : ('00' + map[k]).substr(('' + map[k]).length))
|
||||
}
|
||||
}
|
||||
|
||||
return format
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化相对时间(如:3分钟前)
|
||||
* @param {Date|string|number} timestamp 时间戳或日期字符串
|
||||
* @returns {string} 相对时间字符串
|
||||
*/
|
||||
export const formatRelativeTime = (timestamp) => {
|
||||
if (!timestamp) return ''
|
||||
|
||||
const date = new Date(timestamp)
|
||||
if (isNaN(date.getTime())) return ''
|
||||
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
|
||||
const minute = 60 * 1000
|
||||
const hour = 60 * minute
|
||||
const day = 24 * hour
|
||||
const month = 30 * day
|
||||
const year = 365 * day
|
||||
|
||||
if (diff < minute) {
|
||||
return '刚刚'
|
||||
} else if (diff < hour) {
|
||||
return `${Math.floor(diff / minute)}分钟前`
|
||||
} else if (diff < day) {
|
||||
return `${Math.floor(diff / hour)}小时前`
|
||||
} else if (diff < month) {
|
||||
return `${Math.floor(diff / day)}天前`
|
||||
} else if (diff < year) {
|
||||
return `${Math.floor(diff / month)}个月前`
|
||||
} else {
|
||||
return `${Math.floor(diff / year)}年前`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param {Function} func 要执行的函数
|
||||
* @param {number} wait 等待时间(毫秒)
|
||||
* @param {boolean} immediate 是否立即执行
|
||||
* @returns {Function} 防抖后的函数
|
||||
*/
|
||||
export const debounce = (func, wait = 300, immediate = false) => {
|
||||
let timeout
|
||||
|
||||
return function executedFunction(...args) {
|
||||
const context = this
|
||||
|
||||
const later = () => {
|
||||
timeout = null
|
||||
if (!immediate) func.apply(context, args)
|
||||
}
|
||||
|
||||
const callNow = immediate && !timeout
|
||||
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(later, wait)
|
||||
|
||||
if (callNow) func.apply(context, args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {Function} func 要执行的函数
|
||||
* @param {number} limit 限制时间(毫秒)
|
||||
* @returns {Function} 节流后的函数
|
||||
*/
|
||||
export const throttle = (func, limit = 300) => {
|
||||
let inThrottle
|
||||
let lastFunc
|
||||
let lastRan
|
||||
|
||||
return function executedFunction(...args) {
|
||||
const context = this
|
||||
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args)
|
||||
lastRan = Date.now()
|
||||
inThrottle = true
|
||||
} else {
|
||||
clearTimeout(lastFunc)
|
||||
lastFunc = setTimeout(() => {
|
||||
if (Date.now() - lastRan >= limit) {
|
||||
func.apply(context, args)
|
||||
lastRan = Date.now()
|
||||
}
|
||||
}, limit - (Date.now() - lastRan))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度克隆对象
|
||||
* @param {any} obj 要克隆的对象
|
||||
* @returns {any} 克隆后的对象
|
||||
*/
|
||||
export const deepClone = (obj) => {
|
||||
if (obj === null || typeof obj !== 'object') return obj
|
||||
|
||||
if (obj instanceof Date) return new Date(obj.getTime())
|
||||
if (obj instanceof RegExp) return new RegExp(obj)
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
const arrCopy = []
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
arrCopy[i] = deepClone(obj[i])
|
||||
}
|
||||
return arrCopy
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const objCopy = {}
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
objCopy[key] = deepClone(obj[key])
|
||||
}
|
||||
}
|
||||
return objCopy
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机ID
|
||||
* @param {number} length ID长度
|
||||
* @returns {string} 随机ID
|
||||
*/
|
||||
export const generateId = (length = 8) => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
let result = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为空(null、undefined、空字符串、空数组、空对象)
|
||||
* @param {any} value 要检查的值
|
||||
* @returns {boolean} 是否为空
|
||||
*/
|
||||
export const isEmpty = (value) => {
|
||||
if (value === null || value === undefined) return true
|
||||
if (typeof value === 'string' && value.trim() === '') return true
|
||||
if (Array.isArray(value) && value.length === 0) return true
|
||||
if (typeof value === 'object' && Object.keys(value).length === 0) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取嵌套对象属性
|
||||
* @param {Object} obj 对象
|
||||
* @param {string} path 属性路径,如:'a.b.c'
|
||||
* @param {any} defaultValue 默认值
|
||||
* @returns {any} 属性值或默认值
|
||||
*/
|
||||
export const getSafe = (obj, path, defaultValue = null) => {
|
||||
if (!obj || typeof obj !== 'object') return defaultValue
|
||||
|
||||
const keys = path.split('.')
|
||||
let result = obj
|
||||
|
||||
for (const key of keys) {
|
||||
if (result === null || result === undefined || typeof result !== 'object') {
|
||||
return defaultValue
|
||||
}
|
||||
result = result[key]
|
||||
}
|
||||
|
||||
return result === undefined ? defaultValue : result
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param {number} bytes 字节数
|
||||
* @returns {string} 格式化后的文件大小
|
||||
*/
|
||||
export const 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} 复制结果
|
||||
*/
|
||||
export const copyToClipboard = (text) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (uni.setClipboardData) {
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => resolve(true),
|
||||
fail: (err) => reject(err)
|
||||
})
|
||||
} else {
|
||||
// 兼容H5环境
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.opacity = '0'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
successful ? resolve(true) : reject(new Error('复制失败'))
|
||||
} catch (err) {
|
||||
document.body.removeChild(textarea)
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 统一请求工具
|
||||
* 封装uni.request,提供统一的错误处理和拦截器
|
||||
*/
|
||||
|
||||
import { TIMEOUT, HEADERS } from '../config/api.config.js'
|
||||
|
||||
// 请求拦截器
|
||||
const requestInterceptor = (config) => {
|
||||
// 可以在这里添加token等认证信息
|
||||
const token = uni.getStorageSync('token')
|
||||
if (token) {
|
||||
config.header = {
|
||||
...config.header,
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
// 显示加载提示
|
||||
if (config.showLoading !== false) {
|
||||
uni.showLoading({
|
||||
title: config.loadingText || '加载中...',
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// 响应拦截器
|
||||
const responseInterceptor = (response, config) => {
|
||||
// 隐藏加载提示
|
||||
if (config.showLoading !== false) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
const { statusCode, data } = response
|
||||
|
||||
if (statusCode === 200) {
|
||||
// 根据wanandroid API的返回格式处理
|
||||
if (data.errorCode === 0) {
|
||||
return Promise.resolve(data.data || data)
|
||||
} else {
|
||||
// API业务错误
|
||||
uni.showToast({
|
||||
title: data.errorMsg || '请求失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
return Promise.reject(new Error(data.errorMsg || '请求失败'))
|
||||
}
|
||||
} else {
|
||||
// HTTP错误
|
||||
uni.showToast({
|
||||
title: `请求失败: ${statusCode}`,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
return Promise.reject(new Error(`HTTP错误: ${statusCode}`))
|
||||
}
|
||||
}
|
||||
|
||||
// 错误拦截器
|
||||
const errorInterceptor = (error, config) => {
|
||||
// 隐藏加载提示
|
||||
if (config.showLoading !== false) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
console.error('请求错误:', error)
|
||||
|
||||
let errorMessage = '网络请求失败'
|
||||
if (error.errMsg) {
|
||||
if (error.errMsg.includes('timeout')) {
|
||||
errorMessage = '请求超时,请检查网络'
|
||||
} else if (error.errMsg.includes('fail')) {
|
||||
errorMessage = '网络连接失败'
|
||||
}
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一请求方法
|
||||
* @param {Object} config 请求配置
|
||||
* @returns {Promise} 请求结果
|
||||
*/
|
||||
export const request = (config) => {
|
||||
const defaultConfig = {
|
||||
url: '',
|
||||
method: 'GET',
|
||||
data: {},
|
||||
header: HEADERS,
|
||||
timeout: TIMEOUT,
|
||||
showLoading: true,
|
||||
loadingText: '加载中...'
|
||||
}
|
||||
|
||||
const mergedConfig = { ...defaultConfig, ...config }
|
||||
|
||||
// 执行请求拦截器
|
||||
const finalConfig = requestInterceptor(mergedConfig)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: finalConfig.url,
|
||||
method: finalConfig.method,
|
||||
data: finalConfig.data,
|
||||
header: finalConfig.header,
|
||||
timeout: finalConfig.timeout,
|
||||
success: (response) => {
|
||||
responseInterceptor(response, finalConfig)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
},
|
||||
fail: (error) => {
|
||||
errorInterceptor(error, finalConfig)
|
||||
.then(reject)
|
||||
.catch(reject)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET请求
|
||||
* @param {string} url 请求地址
|
||||
* @param {Object} params 请求参数
|
||||
* @param {Object} config 其他配置
|
||||
* @returns {Promise} 请求结果
|
||||
*/
|
||||
export const get = (url, params = {}, config = {}) => {
|
||||
return request({
|
||||
url,
|
||||
method: 'GET',
|
||||
data: params,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
* @param {string} url 请求地址
|
||||
* @param {Object} data 请求数据
|
||||
* @param {Object} config 其他配置
|
||||
* @returns {Promise} 请求结果
|
||||
*/
|
||||
export const post = (url, data = {}, config = {}) => {
|
||||
return request({
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT请求
|
||||
* @param {string} url 请求地址
|
||||
* @param {Object} data 请求数据
|
||||
* @param {Object} config 其他配置
|
||||
* @returns {Promise} 请求结果
|
||||
*/
|
||||
export const put = (url, data = {}, config = {}) => {
|
||||
return request({
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE请求
|
||||
* @param {string} url 请求地址
|
||||
* @param {Object} params 请求参数
|
||||
* @param {Object} config 其他配置
|
||||
* @returns {Promise} 请求结果
|
||||
*/
|
||||
export const del = (url, params = {}, config = {}) => {
|
||||
return request({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
data: params,
|
||||
...config
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user