318 lines
7.6 KiB
JavaScript
318 lines
7.6 KiB
JavaScript
/**
|
|
* 工具函数
|
|
*/
|
|
|
|
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
|
|
}
|