/** * Soul创业实验 - 工具函数 */ // 格式化时间 const 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(':')}` } const formatNumber = n => { n = n.toString() return n[1] ? n : `0${n}` } // 格式化日期 const formatDate = date => { const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() return `${year}-${formatNumber(month)}-${formatNumber(day)}` } // 格式化金额 const formatMoney = (amount, decimals = 2) => { return Number(amount).toFixed(decimals) } /** 「我的」等页统计数字展示:非法值→0;≥1 万可缩写为「x万」 */ const formatStatNum = (n) => { const x = Number(n) if (Number.isNaN(x) || !Number.isFinite(x)) return '0' const v = Math.floor(x) if (v >= 10000) { const w = v / 10000 const s = w >= 10 ? String(Math.floor(w)) : String(Math.round(w * 10) / 10).replace(/\.0$/, '') return s + '万' } return String(v) } // 防抖函数 const debounce = (fn, delay = 300) => { let timer = null return function (...args) { if (timer) clearTimeout(timer) timer = setTimeout(() => { fn.apply(this, args) }, delay) } } // 节流函数 const throttle = (fn, delay = 300) => { let last = 0 return function (...args) { const now = Date.now() if (now - last >= delay) { fn.apply(this, args) last = now } } } // 生成唯一ID const generateId = () => { return 'id_' + Date.now().toString(36) + Math.random().toString(36).substr(2) } // 检查手机号格式 const isValidPhone = phone => { return /^1[3-9]\d{9}$/.test(phone) } // 检查微信号格式 const isValidWechat = wechat => { return wechat && wechat.length >= 6 && wechat.length <= 20 } // 深拷贝 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)) if (obj instanceof Object) { const copy = {} Object.keys(obj).forEach(key => { copy[key] = deepClone(obj[key]) }) return copy } } // 获取URL参数 const getQueryParams = url => { const params = {} const queryString = url.split('?')[1] if (queryString) { queryString.split('&').forEach(pair => { const [key, value] = pair.split('=') params[decodeURIComponent(key)] = decodeURIComponent(value || '') }) } return params } // 存储操作 const storage = { get(key) { try { return wx.getStorageSync(key) } catch (e) { console.error('获取存储失败:', e) return null } }, set(key, value) { try { wx.setStorageSync(key, value) return true } catch (e) { console.error('设置存储失败:', e) return false } }, remove(key) { try { wx.removeStorageSync(key) return true } catch (e) { console.error('删除存储失败:', e) return false } }, clear() { try { wx.clearStorageSync() return true } catch (e) { console.error('清除存储失败:', e) return false } } } // 显示Toast const showToast = (title, icon = 'none', duration = 2000) => { wx.showToast({ title, icon, duration }) } // 显示Loading const showLoading = (title = '加载中...') => { wx.showLoading({ title, mask: true }) } // 隐藏Loading const hideLoading = () => { wx.hideLoading() } // 修复图片 URL 中 protocol 缺少冒号的问题(如 "https//..." → "https://...") const normalizeImageUrl = (url) => { if (!url || typeof url !== 'string') return '' let s = url.trim() if (!s) return '' s = s.replace(/^(https?)\/\//, '$1://') return s } // 显示确认框 const showConfirm = (title, content) => { return new Promise((resolve) => { wx.showModal({ title, content, success: res => resolve(res.confirm) }) }) } /** * 从头像 URL 提取路径部分(不含域名),用于保存到后端 * 例如:https://xxx.com/uploads/avatars/1.jpg → /uploads/avatars/1.jpg * @param {string} url - 完整 URL 或路径 * @returns {string} */ const toAvatarPath = url => { if (!url || typeof url !== 'string') return url || '' const idx = url.indexOf('/uploads/') if (idx >= 0) return url.substring(idx) if (url.startsWith('/')) return url return url } /** * chooseAvatar 返回的 avatarUrl 常为 http(s)://tmp/... ,渲染层可用的临时路径,但 wx.uploadFile 需本地文件路径。 * 约定:网络地址先 wx.downloadFile 得到 tempFilePath;已是 wxfile:// 则直接上传。 * * @param {string} avatarUrl * @returns {Promise} */ function resolveChooseAvatarFilePath(avatarUrl) { return new Promise((resolve, reject) => { if (!avatarUrl || typeof avatarUrl !== 'string') { reject(new Error('无效的头像路径')) return } const trimmed = avatarUrl.trim() if (!trimmed) { reject(new Error('无效的头像路径')) return } if (trimmed.startsWith('wxfile://')) { resolve(trimmed) return } if (/^https?:\/\//i.test(trimmed)) { wx.downloadFile({ url: trimmed, success: res => { if (res.statusCode === 200 && res.tempFilePath) resolve(res.tempFilePath) else { wx.getImageInfo({ src: trimmed, success: img => { if (img.path) resolve(img.path) else reject(new Error('图片获取失败')) }, fail: () => reject(new Error('图片获取失败')), }) } }, fail: () => { wx.getImageInfo({ src: trimmed, success: img => { if (img.path) resolve(img.path) else reject(new Error('图片获取失败')) }, fail: () => reject(new Error('图片获取失败')), }) }, }) return } resolve(trimmed) }) } /** * 相册/相机选头像用本地路径(供 wx.uploadFile),避开开发者工具里 chooseAvatar → http://tmp 渲染失败。 * 优先 chooseMedia,低版本兜底 chooseImage。 * @returns {Promise} */ function pickLocalAvatarImagePath() { return new Promise((resolve, reject) => { const fail = err => { const em = (err && (err.errMsg || err.message)) || '' const s = String(em) if (s.includes('cancel') || s.includes('取消')) reject(Object.assign(err || {}, { cancelled: true })) else reject(err || new Error('选择图片失败')) } if (typeof wx.chooseMedia === 'function') { wx.chooseMedia({ count: 1, mediaType: ['image'], sourceType: ['album', 'camera'], sizeType: ['compressed'], success: res => { const files = res.tempFiles || [] const p = files[0] && files[0].tempFilePath if (p) resolve(p) else reject(new Error('未选择图片')) }, fail, }) return } if (typeof wx.chooseImage === 'function') { wx.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: res => { const arr = res.tempFilePaths || [] if (arr[0]) resolve(arr[0]) else reject(new Error('未选择图片')) }, fail, }) return } reject(new Error('当前环境不支持选择图片')) }) } module.exports = { formatTime, formatDate, formatMoney, formatStatNum, formatNumber, debounce, throttle, generateId, isValidPhone, isValidWechat, deepClone, getQueryParams, normalizeImageUrl, storage, showToast, showLoading, hideLoading, showConfirm, toAvatarPath, resolveChooseAvatarFilePath, pickLocalAvatarImagePath, }