/** * 工具函数 */ const config = require('../config/index') /** * 格式化时间 * @param {Date|string|number} date - 日期 * @param {string} format - 格式 (默认 'YYYY-MM-DD HH:mm:ss') */ const formatTime = (date, format = 'YYYY-MM-DD HH:mm:ss') => { if (!date) return '' const d = new Date(date) if (isNaN(d.getTime())) return '' const year = d.getFullYear() const month = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') const hour = String(d.getHours()).padStart(2, '0') const minute = String(d.getMinutes()).padStart(2, '0') const second = String(d.getSeconds()).padStart(2, '0') return format .replace('YYYY', year) .replace('MM', month) .replace('DD', day) .replace('HH', hour) .replace('mm', minute) .replace('ss', second) } /** * 格式化相对时间 * @param {Date|string|number} date - 日期 */ const formatRelativeTime = (date) => { if (!date) return '' const d = new Date(date) if (isNaN(d.getTime())) return '' const now = new Date() const diff = now.getTime() - d.getTime() const seconds = Math.floor(diff / 1000) const minutes = Math.floor(seconds / 60) const hours = Math.floor(minutes / 60) const days = Math.floor(hours / 24) if (seconds < 60) return '刚刚' if (minutes < 60) return `${minutes}分钟前` if (hours < 24) return `${hours}小时前` if (days < 7) return `${days}天前` if (days < 30) return `${Math.floor(days / 7)}周前` // 超过30天显示具体日期 return formatTime(d, 'MM-DD HH:mm') } /** * 格式化数字(添加千分位) * @param {number} num - 数字 */ const formatNumber = (num) => { if (num === null || num === undefined) return '0' return Number(num).toLocaleString() } /** * 格式化金额 * @param {number} amount - 金额(分) * @param {boolean} showSymbol - 是否显示符号 */ const formatMoney = (amount, showSymbol = true) => { if (amount === null || amount === undefined) return showSymbol ? '¥0.00' : '0.00' const yuan = (amount / 100).toFixed(2) return showSymbol ? `¥${yuan}` : yuan } /** * 手机号脱敏 * @param {string} phone - 手机号 */ const maskPhone = (phone) => { if (!phone || phone.length < 11) return phone return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') } /** * 检查是否登录 */ const isLoggedIn = () => { const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) return !!token } /** * 获取存储的用户信息 */ const getUserInfo = () => { return wx.getStorageSync(config.STORAGE_KEYS.USER_INFO) || null } /** * 保存用户信息 * @param {object} userInfo - 用户信息 */ const saveUserInfo = (userInfo) => { wx.setStorageSync(config.STORAGE_KEYS.USER_INFO, userInfo) if (userInfo && userInfo.id) { wx.setStorageSync(config.STORAGE_KEYS.USER_ID, userInfo.id) } } /** * 保存登录Token * @param {string} token - Token */ const saveToken = (token) => { wx.setStorageSync(config.STORAGE_KEYS.TOKEN, token) } /** * 清除登录状态 */ const clearAuth = () => { wx.removeStorageSync(config.STORAGE_KEYS.TOKEN) wx.removeStorageSync(config.STORAGE_KEYS.REFRESH_TOKEN) wx.removeStorageSync(config.STORAGE_KEYS.USER_INFO) wx.removeStorageSync(config.STORAGE_KEYS.USER_ID) } /** * 显示加载中 * @param {string} title - 提示文字 */ const showLoading = (title = '加载中...') => { wx.showLoading({ title, mask: true }) } /** * 隐藏加载中 */ const hideLoading = () => { wx.hideLoading() } /** * 显示成功提示 * @param {string} title - 提示文字 */ const showSuccess = (title) => { wx.showToast({ title, icon: 'success' }) } /** * 显示错误提示 * @param {string} title - 提示文字 */ const showError = (title) => { wx.showToast({ title, icon: 'none' }) } /** * 显示普通提示 * @param {string} title - 提示文字 */ const showToast = (title) => { wx.showToast({ title, icon: 'none' }) } /** * 显示确认弹窗 * @param {object} options - 选项 */ const showConfirm = (options) => { return new Promise((resolve) => { wx.showModal({ title: options.title || '提示', content: options.content || '', showCancel: options.showCancel !== false, cancelText: options.cancelText || '取消', confirmText: options.confirmText || '确定', confirmColor: options.confirmColor || '#b06ab3', success: (res) => { resolve(res.confirm) } }) }) } /** * 防抖函数 * @param {Function} fn - 函数 * @param {number} delay - 延迟时间 */ const debounce = (fn, delay = 300) => { let timer = null return function (...args) { if (timer) clearTimeout(timer) timer = setTimeout(() => { fn.apply(this, args) }, delay) } } /** * 节流函数 * @param {Function} fn - 函数 * @param {number} interval - 间隔时间 */ const throttle = (fn, interval = 300) => { let lastTime = 0 return function (...args) { const now = Date.now() if (now - lastTime >= interval) { lastTime = now fn.apply(this, args) } } } /** * 深拷贝 * @param {any} obj - 对象 */ const deepClone = (obj) => { if (obj === null || typeof obj !== 'object') return obj if (obj instanceof Date) return new Date(obj) if (obj instanceof Array) return obj.map(item => deepClone(item)) const cloned = {} for (const key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = deepClone(obj[key]) } } return cloned } /** * 生成唯一ID */ const generateId = () => { return Date.now().toString(36) + Math.random().toString(36).substr(2, 9) } /** * 获取完整的图片URL * @param {string} url - 原始URL * @param {string} defaultImage - 默认图片 */ const getFullImageUrl = (url, defaultImage = '') => { if (!url) return defaultImage || '' if (url.startsWith('wxfile://')) return defaultImage || '' // 如果已经是完整 URL 且不是 localhost, 直接返回 if (url.startsWith('http') && !url.includes('localhost')) return url; // 如果是 localhost, 将其替换为正式域名 if (url.includes('localhost')) { const path = url.split(':3000')[1] || url.split('localhost')[1] return `https://ai-c.maimanji.com${path.startsWith('/') ? '' : '/'}${path}` } // 如果是 /images/ 开头的路径, 也需要拼接域名 // 以前这里直接返回了 url, 导致微信小程序尝试加载本地资源而失败 let baseUrl = 'https://ai-c.maimanji.com' if (config && config.API_BASE_URL) { try { const parsed = new URL(String(config.API_BASE_URL)) baseUrl = parsed.origin } catch (_) { baseUrl = String(config.API_BASE_URL).replace(/\/api\/?$/, '').replace(/\/+$/, '') } } let processedUrl = url if (processedUrl.startsWith('/uploads/')) { processedUrl = `/api/uploads/${processedUrl.slice('/uploads/'.length)}` } else if (processedUrl.startsWith('uploads/')) { processedUrl = `/api/uploads/${processedUrl.slice('uploads/'.length)}` } else if (/^(\/)?(avatars|characters|audio|documents|assets|interest-partners|exchange|products|temp)\//.test(processedUrl)) { processedUrl = processedUrl.startsWith('/') ? `/api/uploads${processedUrl}` : `/api/uploads/${processedUrl}` } // 确保以 / 开头 const normalizedUrl = processedUrl.startsWith('/') ? processedUrl : '/' + processedUrl return baseUrl + normalizedUrl } module.exports = { formatTime, formatRelativeTime, formatNumber, formatMoney, maskPhone, isLoggedIn, getUserInfo, saveUserInfo, saveToken, clearAuth, showLoading, hideLoading, showSuccess, showError, showToast, showConfirm, debounce, throttle, deepClone, generateId, getFullImageUrl }