Files
Mycontent/miniprogram/utils/util.js
乘风 c78a0d531f feat: enhance login modal and avatar selection process
- Updated the login modal to include a user agreement section with clickable links to the user agreement and privacy policy.
- Improved button styling and layout for better user experience.
- Refactored avatar selection process to utilize local image paths, enhancing compatibility and reliability across different environments.
- Added utility functions for resolving avatar file paths and selecting images from the gallery or camera.

This update aims to streamline user interactions during login and avatar selection, ensuring a smoother experience.
2026-05-06 17:21:10 +08:00

326 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<string>}
*/
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<string>}
*/
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,
}