- 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.
326 lines
8.0 KiB
JavaScript
326 lines
8.0 KiB
JavaScript
/**
|
||
* 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,
|
||
}
|