u
This commit is contained in:
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)
|
||||
}
|
||||
Reference in new issue
Block a user