This commit is contained in:
oneao committed 2026-04-24 17:28:11 +08:00
1 parent 07e0001b87
commit 46cbb2f50d
543 files changed
+32576 -4231

No files matched your search

@@ -0,0 +1,254 @@
import { ElMessage } from 'element-plus'
import { closeUploadProgress, showUploadProgress, updateUploadProgress } from '@/components/upload-progress/index'
import { addFileApi, getAliyunKeyApi } from '@/request/api/base'
export interface UploadImageResponse {
name: string
size: string
url: string
blob: Blob
}
export function uploadImage(options: {
quality?: number
mimeType?: string
} = {}): Promise<UploadImageResponse> {
const { quality = 0.8, mimeType = 'image/webp' } = options
return new Promise((resolve, reject) => {
if (typeof document === 'undefined') {
reject(new Error('Require browser environment'))
return
}
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.style.display = 'none'
const processImage = (file: File): Promise<UploadImageResponse> => {
return new Promise((resolve, reject) => {
const img = new Image()
const objectURL = URL.createObjectURL(file)
img.onload = () => {
try {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx)
throw new Error('Canvas init failed')
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
URL.revokeObjectURL(objectURL)
canvas.toBlob(
(blob) => {
if (!blob) {
reject(new Error('Compression failed'))
return
}
resolve({ name: file.name, size: blob.size.toString(), url: URL.createObjectURL(blob), blob })
},
mimeType,
quality,
)
}
catch (error) {
reject(error)
}
}
img.onerror = () => {
URL.revokeObjectURL(objectURL)
reject(new Error('Image load failed'))
}
img.src = objectURL
})
}
input.onchange = async (event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (!file) {
reject(new Error('No file selected'))
return
}
try {
if (!file.type.startsWith('image/')) {
throw new Error('Invalid image file')
}
const result = await processImage(file)
resolve(result)
}
catch (error) {
reject(error)
}
finally {
document.body.removeChild(input)
}
}
document.body.appendChild(input)
input.click()
})
}
export interface UploadVideoResponse {
name: string
size: string
url: string
blob: Blob
}
export function uploadVideo(): Promise<UploadVideoResponse> {
return new Promise((resolve, reject) => {
if (typeof document === 'undefined') {
reject(new Error('Require browser environment'))
return
}
const input = document.createElement('input')
input.type = 'file'
input.accept = 'video/*'
input.style.display = 'none'
input.onchange = (event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (!file) {
reject(new Error('No file selected'))
return
}
if (!file.type.startsWith('video/')) {
reject(new Error('Invalid video file'))
return
}
resolve({
name: file.name,
size: file.size.toString(),
url: URL.createObjectURL(file),
blob: file,
})
document.body.removeChild(input)
}
document.body.appendChild(input)
input.click()
})
}
export interface UploadToAliyunResponse {
url: string
fileName: string
fileSize: string
}
export function uploadToAliyun(blob: Blob, fileName: string, fileSize: string, showProgress: boolean = true, onProgress?: (percent: number) => void): Promise<UploadToAliyunResponse> {
return new Promise((resolve, reject) => {
// 在开始上传时显示上传进度
if (showProgress) {
showUploadProgress('uploading')
}
getAliyunKeyApi()
.then(async (res) => {
try {
const aliyunKey = res.data
const formData = new FormData()
const fileExt = fileName.includes('.') ? fileName.slice(fileName.lastIndexOf('.')) : ''
const uniqueName = `sl_${Date.now()}_${Math.random().toString(36).slice(2)}${fileExt}`
formData.append('key', `${aliyunKey.dir}${uniqueName}`)
formData.append('OSSAccessKeyId', aliyunKey.accessid)
formData.append('policy', aliyunKey.policy)
formData.append('Signature', aliyunKey.signature)
formData.append('file', blob, uniqueName)
const xhr = new XMLHttpRequest()
xhr.open('POST', aliyunKey.host, true)
// 监听上传进度
xhr.upload.onprogress = (event) => {
if (event.lengthComputable && onProgress) {
const percent = (event.loaded / event.total) * 100
onProgress(Number.parseFloat(percent.toFixed(2))) // 保留两位小数
if (showProgress) {
updateUploadProgress(percent) // 更新进度条
}
}
}
xhr.onload = async () => {
if (xhr.status === 204 || xhr.status === 200) {
const allFilePath = `${aliyunKey.host}/${aliyunKey.dir}${uniqueName}`
await addFileApi({
fileName,
fileSize,
fileMapName: allFilePath,
})
if (showProgress) {
updateUploadProgress(100) // 上传完毕
ElMessage.success('上传成功')
setTimeout(() => {
closeUploadProgress() // 关闭上传进度条
}, 500)
}
resolve({
url: allFilePath,
fileName,
fileSize,
})
}
else {
if (showProgress) {
updateUploadProgress(0, 'failed') // 上传失败
ElMessage.error('上传失败,请稍后再试')
setTimeout(() => {
closeUploadProgress() // 关闭上传进度条
}, 500)
}
reject(new Error(`OSS上传失败: ${xhr.responseText}`))
}
}
xhr.onerror = () => {
if (showProgress) {
updateUploadProgress(0, 'failed') // 上传失败
ElMessage.error('上传失败,请稍后再试')
setTimeout(() => {
closeUploadProgress() // 关闭上传进度条
}, 500)
}
reject(new Error(`OSS上传异常: ${xhr.responseText}`))
}
xhr.send(formData)
}
catch (error) {
if (showProgress) {
updateUploadProgress(0, 'failed') // 上传失败
ElMessage.error('上传失败,请稍后再试')
setTimeout(() => {
closeUploadProgress() // 关闭上传进度条
}, 500)
}
reject(error)
}
})
.catch(reject)
})
}
@@ -0,0 +1,56 @@
import { h } from 'vue'
import SvgIcon from '@/components/SvgIcon.vue'
/**
* 图标渲染
* - 用于vue的render函数
*/
export function useIconRender() {
interface IconConfig {
/**
* 图标名称(iconify图标的名称)
* - 例如:mdi-account 或者 mdi:account
*/
icon?: string
/** 图标颜色 */
color?: string
/** 图标大小 */
fontSize?: number
}
interface IconStyle {
color?: string
fontSize?: string
}
/**
* 图标渲染
* @param config
* @property icon - 图标名称(iconify图标的名称), 例如:mdi-account 或者 mdi:account
* @property localIcon - 本地svg图标文件名(assets/svg文件夹下)
* @property color - 图标颜色
* @property fontSize - 图标大小
*/
const iconRender = (config: IconConfig) => {
const { color, fontSize, icon } = config
const style: IconStyle = {}
if (color) {
style.color = color
}
if (fontSize) {
style.fontSize = `${fontSize}px`
}
if (!icon) {
window.console.warn('没有传递图标名称,请确保给icon或localIcon传递有效值!')
}
return () => h(SvgIcon, { icon, style })
}
return {
iconRender,
}
}
@@ -0,0 +1,167 @@
import dayjs from 'dayjs'
export function formatPostTime(input?: dayjs.ConfigType): string {
if (!input)
return ''
const time = dayjs(input)
if (!time.isValid())
return ''
const now = dayjs()
const diffSeconds = now.diff(time, 'second')
if (diffSeconds < 0) {
// 未来时间兜底
return time.format('YYYY年MM月DD日')
}
// 1 分钟内
if (diffSeconds < 60) {
return '刚刚'
}
const diffMinutes = now.diff(time, 'minute')
if (diffMinutes < 60) {
return `${diffMinutes}分钟前`
}
const diffHours = now.diff(time, 'hour')
if (diffHours < 24) {
return `${diffHours}小时前`
}
const diffDays = now.diff(time, 'day')
if (diffDays < 14) {
return `${diffDays}天前`
}
// 本年
if (time.year() === now.year()) {
return time.format('MM月DD日')
}
// 非本年
return time.format('YYYY年MM月DD日')
}
export function formatCommentTime(input?: dayjs.ConfigType): string {
if (!input)
return ''
const time = dayjs(input)
if (!time.isValid())
return ''
const now = dayjs()
const diffSeconds = now.diff(time, 'second')
// 未来时间兜底
if (diffSeconds < 0) {
return time.format('YYYY-MM-DD HH:mm')
}
// 1 分钟内
if (diffSeconds < 60) {
return '刚刚'
}
const diffMinutes = now.diff(time, 'minute')
if (diffMinutes < 60) {
return `${diffMinutes}分钟前`
}
const diffHours = now.diff(time, 'hour')
if (diffHours < 24) {
return `${diffHours}小时前`
}
const diffDays = now.diff(time, 'day')
if (diffDays < 7) {
return `${diffDays}天前`
}
// 本年
if (time.year() === now.year()) {
return time.format('MM-DD HH:mm')
}
// 非本年
return time.format('YYYY-MM-DD HH:mm')
}
type DateFormat
= | 'YYYY'
| 'MM'
| 'DD'
| 'YYYY-MM'
| 'YYYY-MM-DD'
| 'YYYY-MM-DD HH:mm'
| 'YYYY-MM-DD HH:mm:ss'
| 'YYYY年MM月DD日'
export function formatDate(
input?: dayjs.ConfigType,
format: DateFormat = 'YYYY-MM-DD',
): string {
if (!input)
return ''
const date = dayjs(input)
if (!date.isValid())
return ''
return date.format(format)
}
/**
* 格式化聊天时间
* @param timestamp - 毫秒级时间戳
* @param mode - 显示模式:'list' 为列表,'chat' 为聊天记录
* @returns 格式化后的时间字符串
*/
export function formatImTime(timestamp: number | null | undefined, mode: 'list' | 'chat' = 'list'): string {
if (!timestamp)
return ''
const now = new Date()
const date = new Date(timestamp)
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
// 去除时分秒的日期
const nowMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const dateMidnight = new Date(date.getFullYear(), date.getMonth(), date.getDate())
const diffDays = Math.floor((nowMidnight.getTime() - dateMidnight.getTime()) / (1000 * 3600 * 24))
// 1️⃣ 当天
if (diffDays === 0)
return `${hour}:${minute}`
// 2️⃣ 昨天
if (diffDays === 1)
return `昨天 ${hour}:${minute}`
// 3️⃣ 一周内(周一到周日)
const weekStart = new Date(nowMidnight.getTime() - ((now.getDay() || 7) - 1) * 24 * 3600 * 1000)
if (dateMidnight >= weekStart) {
return mode === 'chat'
? `${weekDays[date.getDay()]} ${hour}:${minute}`
: weekDays[date.getDay()]
}
// 4️⃣ 本年或跨年(聊天记录模式:统一显示 YYYY/MM/DD HH:mm)
const ymd = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
if (mode === 'chat')
return `${ymd} ${hour}:${minute}`
// 列表模式区分本年/跨年
if (date.getFullYear() === now.getFullYear())
return `${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
return ymd
}
@@ -0,0 +1,7 @@
export function generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
@@ -0,0 +1,23 @@
/**
* 校验邮箱格式
* @param value 待校验的邮箱字符串
* @returns true 格式正确,false 格式错误
*/
export function validateEmail(value: string): boolean {
if (!value)
return false // 如果为空,不校验格式,可按需求改成 false
const emailReg = /^[\w.%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i
return emailReg.test(value)
}
/**
* 校验中国手机号格式(支持 +86 开头)
* @param value 待校验的手机号字符串
* @returns true 格式正确,false 格式错误
*/
export function validatePhone(value: string): boolean {
if (!value)
return false // 如果为空,不校验格式,可按需求改成 false
const phoneReg = /^(?:\+?86)?1(?:3\d{3}|5[^4\D]\d{2}|8\d{3}|7(?:[0-35-9]\d{2}|4(?:0\d|1[0-2]|9\d))|9[0-35-9]\d{2}|6[2567]\d{2}|4(?:(?:10|4[01])\d{3}|[68]\d{4}|[579]\d{2}))\d{6}$/
return phoneReg.test(value)
}