Claw 项目完整结构提交
This commit is contained in:
392
Claw/client/wechat_app/utils/api.js
Normal file
392
Claw/client/wechat_app/utils/api.js
Normal file
@@ -0,0 +1,392 @@
|
||||
// API接口封装
|
||||
const API_BASE = 'https://pactgo.cn/api/v1'
|
||||
|
||||
class API {
|
||||
// 用户登录
|
||||
static async login(code) {
|
||||
return this.request('/wechat/login', 'POST', { code })
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
static async getUserInfo() {
|
||||
return this.request('/wechat/userinfo', 'GET')
|
||||
}
|
||||
|
||||
// 提交任务
|
||||
static async submitTask(taskData) {
|
||||
return this.request('/task/submit', 'POST', taskData)
|
||||
}
|
||||
|
||||
// 获取任务列表
|
||||
static async getTaskList(page = 1, limit = 20) {
|
||||
return this.request(`/task/list?page=${page}&limit=${limit}`, 'GET')
|
||||
}
|
||||
|
||||
// 获取任务详情
|
||||
static async getTaskDetail(taskId) {
|
||||
return this.request(`/task/${taskId}`, 'GET')
|
||||
}
|
||||
|
||||
// 获取任务状态
|
||||
static async getTaskStatus(taskId) {
|
||||
return this.request(`/task/${taskId}/status`, 'GET')
|
||||
}
|
||||
|
||||
// 发送聊天消息
|
||||
static async sendMessage(content, type = 'text') {
|
||||
return this.request('/chat/message', 'POST', { content, type })
|
||||
}
|
||||
|
||||
// 获取聊天记录
|
||||
static async getChatHistory(page = 1, limit = 50) {
|
||||
return this.request(`/chat/history?page=${page}&limit=${limit}`, 'GET')
|
||||
}
|
||||
|
||||
// 获取设备信息
|
||||
static async getDeviceInfo() {
|
||||
return this.request('/device/info', 'GET')
|
||||
}
|
||||
|
||||
// 更新设备状态
|
||||
static async updateDeviceStatus(status) {
|
||||
return this.request('/device/status', 'POST', { status })
|
||||
}
|
||||
|
||||
// 获取系统状态
|
||||
static async getSystemStatus() {
|
||||
return this.request('/system/status', 'GET')
|
||||
}
|
||||
|
||||
// 通用请求方法
|
||||
static async request(url, method = 'GET', data = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = wx.getStorageSync('token') || ''
|
||||
|
||||
wx.request({
|
||||
url: API_BASE + url,
|
||||
method,
|
||||
data,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : ''
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
if (res.data.success !== false) {
|
||||
resolve(res.data)
|
||||
} else {
|
||||
reject(new Error(res.data.message || '请求失败'))
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
// Token过期,重新登录
|
||||
wx.removeStorageSync('token')
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
reject(new Error('登录已过期'))
|
||||
} else {
|
||||
reject(new Error(`请求失败: ${res.statusCode}`))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('API请求失败:', err)
|
||||
wx.showToast({
|
||||
title: '网络连接失败',
|
||||
icon: 'none'
|
||||
})
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
static async uploadFile(filePath, formData = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = wx.getStorageSync('token') || ''
|
||||
|
||||
wx.uploadFile({
|
||||
url: API_BASE + '/upload',
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: formData,
|
||||
header: {
|
||||
'Authorization': token ? `Bearer ${token}` : ''
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
resolve(data)
|
||||
} catch (e) {
|
||||
reject(new Error('解析响应失败'))
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`上传失败: ${res.statusCode}`))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('文件上传失败:', err)
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket管理器
|
||||
class WebSocketManager {
|
||||
constructor() {
|
||||
this.socket = null
|
||||
this.isConnected = false
|
||||
this.reconnectTimer = null
|
||||
this.messageHandlers = new Map()
|
||||
}
|
||||
|
||||
connect(url) {
|
||||
if (this.socket) {
|
||||
this.disconnect()
|
||||
}
|
||||
|
||||
this.socket = wx.connectSocket({
|
||||
url: url,
|
||||
header: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
this.socket.onOpen(() => {
|
||||
console.log('WebSocket连接已打开')
|
||||
this.isConnected = true
|
||||
this.clearReconnectTimer()
|
||||
|
||||
// 发送认证信息
|
||||
this.send({
|
||||
type: 'auth',
|
||||
userId: wx.getStorageSync('userId') || 'anonymous',
|
||||
deviceId: wx.getSystemInfoSync().model,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
})
|
||||
|
||||
this.socket.onMessage((res) => {
|
||||
console.log('收到WebSocket消息:', res.data)
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
this.handleMessage(data)
|
||||
} catch (e) {
|
||||
console.error('解析WebSocket消息失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
this.socket.onClose(() => {
|
||||
console.log('WebSocket连接已关闭')
|
||||
this.isConnected = false
|
||||
this.socket = null
|
||||
this.scheduleReconnect(url)
|
||||
})
|
||||
|
||||
this.socket.onError((err) => {
|
||||
console.error('WebSocket连接错误:', err)
|
||||
this.isConnected = false
|
||||
this.scheduleReconnect(url)
|
||||
})
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.clearReconnectTimer()
|
||||
if (this.socket) {
|
||||
this.socket.close()
|
||||
this.socket = null
|
||||
}
|
||||
this.isConnected = false
|
||||
}
|
||||
|
||||
send(data) {
|
||||
if (this.isConnected && this.socket) {
|
||||
this.socket.send({
|
||||
data: JSON.stringify(data)
|
||||
})
|
||||
} else {
|
||||
console.warn('WebSocket未连接,无法发送消息')
|
||||
}
|
||||
}
|
||||
|
||||
onMessage(type, handler) {
|
||||
if (!this.messageHandlers.has(type)) {
|
||||
this.messageHandlers.set(type, [])
|
||||
}
|
||||
this.messageHandlers.get(type).push(handler)
|
||||
}
|
||||
|
||||
offMessage(type, handler) {
|
||||
if (this.messageHandlers.has(type)) {
|
||||
const handlers = this.messageHandlers.get(type)
|
||||
const index = handlers.indexOf(handler)
|
||||
if (index > -1) {
|
||||
handlers.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(data) {
|
||||
if (data.type && this.messageHandlers.has(data.type)) {
|
||||
const handlers = this.messageHandlers.get(data.type)
|
||||
handlers.forEach(handler => {
|
||||
try {
|
||||
handler(data)
|
||||
} catch (e) {
|
||||
console.error('消息处理错误:', e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
scheduleReconnect(url) {
|
||||
this.clearReconnectTimer()
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
console.log('尝试重新连接WebSocket...')
|
||||
this.connect(url)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
clearReconnectTimer() {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
this.reconnectTimer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const util = {
|
||||
// 格式化时间
|
||||
formatTime(date) {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
return [year, month, day].map(this.formatNumber).join('/') + ' ' +
|
||||
[hour, minute, second].map(this.formatNumber).join(':')
|
||||
},
|
||||
|
||||
formatNumber(n) {
|
||||
n = n.toString()
|
||||
return n[1] ? n : '0' + n
|
||||
},
|
||||
|
||||
// 显示加载提示
|
||||
showLoading(title = '加载中...') {
|
||||
wx.showLoading({
|
||||
title: title,
|
||||
mask: true
|
||||
})
|
||||
},
|
||||
|
||||
// 隐藏加载提示
|
||||
hideLoading() {
|
||||
wx.hideLoading()
|
||||
},
|
||||
|
||||
// 显示成功提示
|
||||
showSuccess(title = '操作成功') {
|
||||
wx.showToast({
|
||||
title: title,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
},
|
||||
|
||||
// 显示错误提示
|
||||
showError(title = '操作失败') {
|
||||
wx.showToast({
|
||||
title: title,
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
},
|
||||
|
||||
// 显示模态框
|
||||
showModal(title, content) {
|
||||
return new Promise((resolve) => {
|
||||
wx.showModal({
|
||||
title: title,
|
||||
content: content,
|
||||
success: (res) => {
|
||||
resolve(res.confirm)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 防抖函数
|
||||
debounce(func, wait) {
|
||||
let timeout
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout)
|
||||
func(...args)
|
||||
}
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(later, wait)
|
||||
}
|
||||
},
|
||||
|
||||
// 节流函数
|
||||
throttle(func, limit) {
|
||||
let inThrottle
|
||||
return function() {
|
||||
const args = arguments
|
||||
const context = this
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args)
|
||||
inThrottle = true
|
||||
setTimeout(() => inThrottle = false, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 常量定义
|
||||
const constants = {
|
||||
// API基础地址
|
||||
API_BASE: 'https://pactgo.cn/api/v1',
|
||||
|
||||
// WebSocket地址
|
||||
WEBSOCKET_URL: 'wss://pactgo.cn/ws/task',
|
||||
|
||||
// 任务状态
|
||||
TASK_STATUS: {
|
||||
PENDING: 'pending',
|
||||
PROCESSING: 'processing',
|
||||
COMPLETED: 'completed',
|
||||
FAILED: 'failed'
|
||||
},
|
||||
|
||||
// 消息类型
|
||||
MESSAGE_TYPE: {
|
||||
TEXT: 'text',
|
||||
IMAGE: 'image',
|
||||
FILE: 'file',
|
||||
SYSTEM: 'system'
|
||||
},
|
||||
|
||||
// 本地存储键名
|
||||
STORAGE_KEYS: {
|
||||
TOKEN: 'token',
|
||||
USER_INFO: 'userInfo',
|
||||
DEVICE_ID: 'deviceId',
|
||||
TASK_HISTORY: 'taskHistory'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
API,
|
||||
WebSocketManager,
|
||||
util,
|
||||
constants
|
||||
}
|
||||
210
Claw/client/wechat_app/utils/constant.js
Normal file
210
Claw/client/wechat_app/utils/constant.js
Normal file
@@ -0,0 +1,210 @@
|
||||
// 常量定义
|
||||
|
||||
// API基础地址
|
||||
const API_BASE = 'https://pactgo.cn/api/v1'
|
||||
|
||||
// WebSocket地址
|
||||
const WEBSOCKET_URL = 'wss://pactgo.cn/ws/task'
|
||||
|
||||
// 任务状态
|
||||
const TASK_STATUS = {
|
||||
PENDING: 'pending',
|
||||
PROCESSING: 'processing',
|
||||
COMPLETED: 'completed',
|
||||
FAILED: 'failed',
|
||||
CANCELLED: 'cancelled'
|
||||
}
|
||||
|
||||
// 任务类型
|
||||
const TASK_TYPE = {
|
||||
TEXT: 'text',
|
||||
IMAGE: 'image',
|
||||
FILE: 'file',
|
||||
AUDIO: 'audio',
|
||||
VIDEO: 'video'
|
||||
}
|
||||
|
||||
// 消息类型
|
||||
const MESSAGE_TYPE = {
|
||||
TEXT: 'text',
|
||||
IMAGE: 'image',
|
||||
FILE: 'file',
|
||||
SYSTEM: 'system',
|
||||
TASK: 'task'
|
||||
}
|
||||
|
||||
// 本地存储键名
|
||||
const STORAGE_KEYS = {
|
||||
TOKEN: 'token',
|
||||
USER_INFO: 'userInfo',
|
||||
DEVICE_ID: 'deviceId',
|
||||
TASK_HISTORY: 'taskHistory',
|
||||
CHAT_HISTORY: 'chatHistory',
|
||||
SYSTEM_CONFIG: 'systemConfig'
|
||||
}
|
||||
|
||||
// 网络超时时间(毫秒)
|
||||
const NETWORK_TIMEOUT = {
|
||||
REQUEST: 10000,
|
||||
UPLOAD: 30000,
|
||||
DOWNLOAD: 30000,
|
||||
WEBSOCKET: 10000
|
||||
}
|
||||
|
||||
// 重试配置
|
||||
const RETRY_CONFIG = {
|
||||
MAX_RETRIES: 3,
|
||||
RETRY_DELAY: 1000,
|
||||
BACKOFF_MULTIPLIER: 2
|
||||
}
|
||||
|
||||
// 分页配置
|
||||
const PAGINATION = {
|
||||
DEFAULT_PAGE_SIZE: 20,
|
||||
MAX_PAGE_SIZE: 100,
|
||||
PRELOAD_PAGES: 2
|
||||
}
|
||||
|
||||
// 文件上传限制
|
||||
const UPLOAD_LIMITS = {
|
||||
MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB
|
||||
MAX_IMAGE_SIZE: 5 * 1024 * 1024, // 5MB
|
||||
ALLOWED_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'text/plain', 'application/pdf']
|
||||
}
|
||||
|
||||
// WebSocket消息类型
|
||||
const WS_MESSAGE_TYPE = {
|
||||
AUTH: 'auth',
|
||||
PING: 'ping',
|
||||
PONG: 'pong',
|
||||
MESSAGE: 'message',
|
||||
TASK_STATUS: 'task_status',
|
||||
SYSTEM: 'system',
|
||||
ERROR: 'error'
|
||||
}
|
||||
|
||||
// 系统配置
|
||||
const SYSTEM_CONFIG = {
|
||||
APP_NAME: '智控未来',
|
||||
APP_VERSION: '1.0.0',
|
||||
API_VERSION: 'v1',
|
||||
SUPPORTED_FEATURES: ['chat', 'task', 'file_upload', 'websocket']
|
||||
}
|
||||
|
||||
// 错误代码
|
||||
const ERROR_CODES = {
|
||||
NETWORK_ERROR: 1000,
|
||||
AUTH_ERROR: 1001,
|
||||
API_ERROR: 1002,
|
||||
VALIDATION_ERROR: 1003,
|
||||
FILE_ERROR: 1004,
|
||||
WEBSOCKET_ERROR: 1005,
|
||||
TASK_ERROR: 1006
|
||||
}
|
||||
|
||||
// 错误消息
|
||||
const ERROR_MESSAGES = {
|
||||
[ERROR_CODES.NETWORK_ERROR]: '网络连接失败',
|
||||
[ERROR_CODES.AUTH_ERROR]: '认证失败',
|
||||
[ERROR_CODES.API_ERROR]: '接口调用失败',
|
||||
[ERROR_CODES.VALIDATION_ERROR]: '参数验证失败',
|
||||
[ERROR_CODES.FILE_ERROR]: '文件处理失败',
|
||||
[ERROR_CODES.WEBSOCKET_ERROR]: 'WebSocket连接失败',
|
||||
[ERROR_CODES.TASK_ERROR]: '任务处理失败'
|
||||
}
|
||||
|
||||
// 默认头像
|
||||
const DEFAULT_AVATAR = '/assets/images/default-avatar.png'
|
||||
|
||||
// 默认图标
|
||||
const DEFAULT_ICONS = {
|
||||
HOME: '/assets/icons/home.png',
|
||||
CHAT: '/assets/icons/chat.png',
|
||||
TASK: '/assets/icons/task.png',
|
||||
USER: '/assets/icons/user.png',
|
||||
SEND: '/assets/icons/send.png',
|
||||
UPLOAD: '/assets/icons/upload.png',
|
||||
SETTINGS: '/assets/icons/settings.png'
|
||||
}
|
||||
|
||||
// 颜色配置
|
||||
const COLORS = {
|
||||
PRIMARY: '#07c160',
|
||||
SECONDARY: '#10aeff',
|
||||
SUCCESS: '#07c160',
|
||||
WARNING: '#f0ad4e',
|
||||
ERROR: '#dd524d',
|
||||
INFO: '#10aeff',
|
||||
TEXT_PRIMARY: '#333333',
|
||||
TEXT_SECONDARY: '#666666',
|
||||
TEXT_HINT: '#999999',
|
||||
BACKGROUND: '#f5f5f5',
|
||||
WHITE: '#ffffff',
|
||||
BORDER: '#e0e0e0'
|
||||
}
|
||||
|
||||
// 字体大小
|
||||
const FONT_SIZES = {
|
||||
SMALL: '24rpx',
|
||||
NORMAL: '28rpx',
|
||||
LARGE: '32rpx',
|
||||
XLARGE: '36rpx',
|
||||
TITLE: '40rpx'
|
||||
}
|
||||
|
||||
// 间距配置
|
||||
const SPACING = {
|
||||
XS: '10rpx',
|
||||
SM: '20rpx',
|
||||
MD: '30rpx',
|
||||
LG: '40rpx',
|
||||
XL: '50rpx'
|
||||
}
|
||||
|
||||
// 圆角配置
|
||||
const BORDER_RADIUS = {
|
||||
SMALL: '5rpx',
|
||||
NORMAL: '10rpx',
|
||||
LARGE: '20rpx',
|
||||
ROUND: '50%'
|
||||
}
|
||||
|
||||
// 阴影配置
|
||||
const SHADOWS = {
|
||||
SMALL: '0 2rpx 10rpx rgba(0, 0, 0, 0.1)',
|
||||
NORMAL: '0 4rpx 20rpx rgba(0, 0, 0, 0.15)',
|
||||
LARGE: '0 8rpx 30rpx rgba(0, 0, 0, 0.2)'
|
||||
}
|
||||
|
||||
// 动画配置
|
||||
const ANIMATIONS = {
|
||||
DURATION_SHORT: 200,
|
||||
DURATION_NORMAL: 300,
|
||||
DURATION_LONG: 500,
|
||||
EASING: 'ease-in-out'
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
API_BASE,
|
||||
WEBSOCKET_URL,
|
||||
TASK_STATUS,
|
||||
TASK_TYPE,
|
||||
MESSAGE_TYPE,
|
||||
STORAGE_KEYS,
|
||||
NETWORK_TIMEOUT,
|
||||
RETRY_CONFIG,
|
||||
PAGINATION,
|
||||
UPLOAD_LIMITS,
|
||||
WS_MESSAGE_TYPE,
|
||||
SYSTEM_CONFIG,
|
||||
ERROR_CODES,
|
||||
ERROR_MESSAGES,
|
||||
DEFAULT_AVATAR,
|
||||
DEFAULT_ICONS,
|
||||
COLORS,
|
||||
FONT_SIZES,
|
||||
SPACING,
|
||||
BORDER_RADIUS,
|
||||
SHADOWS,
|
||||
ANIMATIONS
|
||||
}
|
||||
310
Claw/client/wechat_app/utils/util.js
Normal file
310
Claw/client/wechat_app/utils/util.js
Normal file
@@ -0,0 +1,310 @@
|
||||
// 通用工具函数
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param {Date} date - 日期对象
|
||||
* @returns {string} 格式化后的时间字符串
|
||||
*/
|
||||
function formatTime(date) {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
return [year, month, day].map(formatNumber).join('/') + ' ' +
|
||||
[hour, minute, second].map(formatNumber).join(':')
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数字,补零
|
||||
* @param {number} n - 数字
|
||||
* @returns {string} 格式化后的数字字符串
|
||||
*/
|
||||
function formatNumber(n) {
|
||||
n = n.toString()
|
||||
return n[1] ? n : '0' + n
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化相对时间
|
||||
* @param {number} timestamp - 时间戳
|
||||
* @returns {string} 相对时间描述
|
||||
*/
|
||||
function formatRelativeTime(timestamp) {
|
||||
const now = Date.now()
|
||||
const diff = now - timestamp
|
||||
|
||||
const minute = 60 * 1000
|
||||
const hour = 60 * minute
|
||||
const day = 24 * hour
|
||||
const week = 7 * day
|
||||
const month = 30 * 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 < week) {
|
||||
return Math.floor(diff / day) + '天前'
|
||||
} else if (diff < month) {
|
||||
return Math.floor(diff / week) + '周前'
|
||||
} else {
|
||||
return formatTime(new Date(timestamp))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param {Function} func - 要执行的函数
|
||||
* @param {number} wait - 等待时间(毫秒)
|
||||
* @returns {Function} 防抖后的函数
|
||||
*/
|
||||
function debounce(func, wait) {
|
||||
let timeout
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout)
|
||||
func(...args)
|
||||
}
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(later, wait)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {Function} func - 要执行的函数
|
||||
* @param {number} limit - 限制时间(毫秒)
|
||||
* @returns {Function} 节流后的函数
|
||||
*/
|
||||
function throttle(func, limit) {
|
||||
let inThrottle
|
||||
return function() {
|
||||
const args = arguments
|
||||
const context = this
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args)
|
||||
inThrottle = true
|
||||
setTimeout(() => inThrottle = false, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝对象
|
||||
* @param {Object} obj - 要拷贝的对象
|
||||
* @returns {Object} 拷贝后的对象
|
||||
*/
|
||||
function 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 => deepClone(item))
|
||||
if (typeof obj === 'object') {
|
||||
const cloned = {}
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
cloned[key] = deepClone(obj[key])
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断对象是否为空
|
||||
* @param {Object} obj - 要判断的对象
|
||||
* @returns {boolean} 是否为空
|
||||
*/
|
||||
function isEmpty(obj) {
|
||||
if (obj == null) return true
|
||||
if (obj.length > 0) return false
|
||||
if (obj.length === 0) return true
|
||||
if (typeof obj !== 'object') return true
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
* @returns {string} 唯一ID
|
||||
*/
|
||||
function generateUniqueId() {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机号
|
||||
* @param {string} phone - 手机号
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
function validatePhone(phone) {
|
||||
return /^1[3-9]\d{9}$/.test(phone)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱
|
||||
* @param {string} email - 邮箱
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
function validateEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证URL
|
||||
* @param {string} url - URL
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
function validateUrl(url) {
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
* @param {string} filename - 文件名
|
||||
* @returns {string} 扩展名
|
||||
*/
|
||||
function getFileExtension(filename) {
|
||||
return filename.split('.').pop().toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param {number} bytes - 字节数
|
||||
* @returns {string} 格式化后的文件大小
|
||||
*/
|
||||
function 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 {number} length - 字符串长度
|
||||
* @returns {string} 随机字符串
|
||||
*/
|
||||
function randomString(length) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
let result = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 首字母大写
|
||||
* @param {string} str - 字符串
|
||||
* @returns {string} 首字母大写的字符串
|
||||
*/
|
||||
function capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰命名转下划线
|
||||
* @param {string} str - 驼峰命名字符串
|
||||
* @returns {string} 下划线命名字符串
|
||||
*/
|
||||
function camelToSnake(str) {
|
||||
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线命名转驼峰
|
||||
* @param {string} str - 下划线命名字符串
|
||||
* @returns {string} 驼峰命名字符串
|
||||
*/
|
||||
function snakeToCamel(str) {
|
||||
return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组去重
|
||||
* @param {Array} arr - 数组
|
||||
* @returns {Array} 去重后的数组
|
||||
*/
|
||||
function uniqueArray(arr) {
|
||||
return [...new Set(arr)]
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组分组
|
||||
* @param {Array} arr - 数组
|
||||
* @param {Function} keyFn - 分组键函数
|
||||
* @returns {Object} 分组后的对象
|
||||
*/
|
||||
function groupBy(arr, keyFn) {
|
||||
return arr.reduce((groups, item) => {
|
||||
const key = keyFn(item)
|
||||
if (!groups[key]) {
|
||||
groups[key] = []
|
||||
}
|
||||
groups[key].push(item)
|
||||
return groups
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取URL参数
|
||||
* @param {string} url - URL字符串
|
||||
* @returns {Object} URL参数对象
|
||||
*/
|
||||
function getUrlParams(url) {
|
||||
const params = {}
|
||||
const urlObj = new URL(url)
|
||||
for (const [key, value] of urlObj.searchParams) {
|
||||
params[key] = value
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL参数
|
||||
* @param {Object} params - 参数对象
|
||||
* @returns {string} URL参数字符串
|
||||
*/
|
||||
function buildUrlParams(params) {
|
||||
return Object.keys(params)
|
||||
.filter(key => params[key] !== null && params[key] !== undefined)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||||
.join('&')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatTime,
|
||||
formatNumber,
|
||||
formatRelativeTime,
|
||||
debounce,
|
||||
throttle,
|
||||
deepClone,
|
||||
isEmpty,
|
||||
generateUniqueId,
|
||||
validatePhone,
|
||||
validateEmail,
|
||||
validateUrl,
|
||||
getFileExtension,
|
||||
formatFileSize,
|
||||
randomString,
|
||||
capitalize,
|
||||
camelToSnake,
|
||||
snakeToCamel,
|
||||
uniqueArray,
|
||||
groupBy,
|
||||
getUrlParams,
|
||||
buildUrlParams
|
||||
}
|
||||
Reference in New Issue
Block a user