ai-c/utils/util.js
2026-02-02 18:21:32 +08:00

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
}