Add new components for login and comment functionality
This commit is contained in:
418
components/kl-uploader/index.vue
Normal file
418
components/kl-uploader/index.vue
Normal file
@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<div class="upload-file">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
v-loading="loading"
|
||||
v-bind="$attrs"
|
||||
:file-list="fileList"
|
||||
drag
|
||||
:disabled="disabled"
|
||||
:multiple="limit > 1"
|
||||
:on-error="handleUploadError"
|
||||
:on-change="handleChange"
|
||||
:auto-upload="false"
|
||||
:show-file-list="listType === 'picture-card' ? true : false"
|
||||
:list-type="listType"
|
||||
:on-preview="handlePictureCardPreview"
|
||||
:on-remove="handleRemove"
|
||||
>
|
||||
<slot>
|
||||
<el-button type="primary" :disabled="disabled">选择文件</el-button>
|
||||
</slot>
|
||||
<template v-if="showTip" #tip>
|
||||
<div class="m-t-10px leading-24px">
|
||||
<slot name="des">
|
||||
<div>
|
||||
<!-- <span>附件内容说明:</span> -->
|
||||
<span class="text-[#999999]">{{ tips }}</span>
|
||||
</div>
|
||||
</slot>
|
||||
<!-- <slot name="tips">
|
||||
<div>
|
||||
<span>请上传 大小不超过</span>
|
||||
<span class="color-#FA5940 m-l-2px m-r-2px">{{ size }}MB</span>
|
||||
<span>格式为</span>
|
||||
<span class="color-#FA5940 m-l-2px m-r-2px">{{ fileType.join('/') }}</span>
|
||||
<span>的文件</span>
|
||||
</div>
|
||||
</slot> -->
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="listType === 'picture-card'" #file="{ file }">
|
||||
<div v-if="!handelFileType(file.name)" class="custom-preview-card">
|
||||
<div class="file-type-icon cursor-pointer" @click="handlePictureCardDown(file)">
|
||||
<el-icon :size="32"><Document /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div v-if="loading" class="text-12px color-#999">{{ loadingtext }}</div>
|
||||
|
||||
<transition-group v-if="listType !== 'picture-card'" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
|
||||
<li v-for="(file, index) in fileList" :key="file.uid" class="el-upload-list__item ele-upload-list__item-content">
|
||||
<div class="ele-upload-list__item-content upload-item">
|
||||
<div class="curpor-pointer" @click="handlePictureCardDown(file)">
|
||||
<span class="el-icon-document cursor-pointer pl-9px">{{ file.name }} </span>
|
||||
</div>
|
||||
<div class="ele-upload-list__item-content-action">
|
||||
<el-link :underline="false" type="danger" :disabled="disabled" @click="handleDelete(index)">删除 </el-link>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</transition-group>
|
||||
|
||||
<el-image-viewer v-if="dialogVisible" :url-list="[dialogImageUrl]" @close="dialogVisible = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { uploadV2, creatFile } from '@/api/common/index'
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import type { UploadUserFile, UploadInstance } from 'element-plus'
|
||||
import { accDiv } from '@/utils/utils'
|
||||
// import CryptoJS from 'crypto-js' // 引入 crypto-js
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const props = defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 文件个数限制 */
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
/** 文件大小限制(M) */
|
||||
size: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
/** 文件类型 */
|
||||
fileType: {
|
||||
type: Array as PropType<string[]>,
|
||||
// default: () => ['png', 'jpg', 'jpeg', 'doc', 'xls', 'ppt', 'pdf', 'txt'],
|
||||
default: () => [],
|
||||
},
|
||||
/** 上传地址 */
|
||||
uploadUrl: {
|
||||
type: String,
|
||||
default: '/prod-api/app-api/infra/file/presigned-url',
|
||||
},
|
||||
/** 提示文案 */
|
||||
tips: {
|
||||
type: String,
|
||||
default: '提供佐证变更有效的证明文件(邮件/聊天截图/情况说明)',
|
||||
},
|
||||
/** 提示文案 */
|
||||
showTip: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/** 是否预览cad */
|
||||
cadShow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 组件类型 */
|
||||
listType: {
|
||||
type: String as PropType<'text' | 'picture' | 'picture-card'>,
|
||||
default: 'text', // picture" | "text" | "picture-card
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['validate', 'preview'])
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const fileList = defineModel<any>('fileList', {
|
||||
default: [],
|
||||
})
|
||||
|
||||
const fileChange = ref()
|
||||
const handleChange = async (uploadFile: UploadUserFile) => {
|
||||
if (uploadFile.status !== 'ready') return
|
||||
/** 上传前校验 */
|
||||
if (handleBeforeUpload(uploadFile)) {
|
||||
fileChange.value = uploadFile
|
||||
// 新增:清除上传组件内部缓存
|
||||
uploadRef.value?.handleRemove(fileChange.value) // 关键:主动触发上传组件的remove方法
|
||||
return
|
||||
}
|
||||
/** 上传 */
|
||||
await handleUploadFile(uploadFile)
|
||||
}
|
||||
/** 上传前校验 */
|
||||
const handleBeforeUpload = (rawFile: UploadUserFile) => {
|
||||
if (fileList.value.length + 1 > props.limit) {
|
||||
handleExceed()
|
||||
return true
|
||||
}
|
||||
if (props.size > 0 && rawFile.size && +accDiv(accDiv(rawFile.size, 1024), 1024) > props.size) {
|
||||
ElMessage.error(`文件大小不能超过${props.size}M`)
|
||||
return true
|
||||
}
|
||||
const ext = rawFile.name.split('.').pop()?.toLowerCase()
|
||||
if (ext && props.fileType?.length && !props.fileType.includes(ext)) {
|
||||
ElMessage.error(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`)
|
||||
return true
|
||||
}
|
||||
}
|
||||
const handleExceed = () => {
|
||||
ElMessage.error(`最多只能上传 ${props.limit} 个文件`)
|
||||
}
|
||||
const handleUploadError = (err: any) => {
|
||||
ElMessage.error('文件上传失败:' + err.message)
|
||||
}
|
||||
|
||||
/** 上传 */
|
||||
const loading = ref(false)
|
||||
const loadingtext = ref('上传中...')
|
||||
const uploadUrlV2 = ref('')
|
||||
const fileUrl = ref('')
|
||||
const configId = ref('')
|
||||
const path = ref('')
|
||||
const getUploadConfig = async (options: UploadUserFile) => {
|
||||
path.value = `${options.uid}/${dayjs(new Date()).format('YYYY/MM/DD/HH/mm/ss')}/${options.name}`
|
||||
try {
|
||||
const response = await uploadV2(props.uploadUrl, { path: path.value })
|
||||
if (response.code !== 0) return ElMessage.error(response.message)
|
||||
uploadUrlV2.value = response.data.uploadUrl
|
||||
fileUrl.value = response.data.url
|
||||
configId.value = response.data.configId
|
||||
} catch (err) {
|
||||
loading.value = false
|
||||
console.error('获取上传配置失败:', err)
|
||||
alert('获取上传参数失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
/** 上传到服务器 */
|
||||
const UploadFilled = async (options: UploadUserFile) => {
|
||||
try {
|
||||
const res = await creatFile({
|
||||
configId: Number(configId.value),
|
||||
path: path.value,
|
||||
name: options.name,
|
||||
url: fileUrl.value,
|
||||
size: options.size as number,
|
||||
})
|
||||
if (res.code !== 0) return ElMessage.error(res.message)
|
||||
ElMessage.success('上传成功')
|
||||
const newFile = {
|
||||
name: options?.name,
|
||||
url: fileUrl.value,
|
||||
size: options?.size,
|
||||
uid: options.uid,
|
||||
title: options?.name,
|
||||
fileId: res.data,
|
||||
}
|
||||
// 更新文件列表
|
||||
fileList.value = [...fileList.value, newFile]
|
||||
emit('validate')
|
||||
} catch (error) {
|
||||
console.error('获取上传配置失败:', error)
|
||||
alert('creatFile上传失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** fetch 监听不了进度 需要改成xhr */
|
||||
const uploadWithProgress = (url: string, file: any, onProgress: (percent: number) => void) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('PUT', url, true)
|
||||
xhr.setRequestHeader('Content-Type', file.raw.type)
|
||||
|
||||
xhr.upload.onprogress = function (event) {
|
||||
if (event.lengthComputable) {
|
||||
const percent = Math.round((event.loaded / event.total) * 100)
|
||||
onProgress(percent)
|
||||
}
|
||||
}
|
||||
|
||||
xhr.onload = function () {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(xhr.response)
|
||||
} else {
|
||||
reject(new Error('上传失败'))
|
||||
}
|
||||
}
|
||||
|
||||
xhr.onerror = function () {
|
||||
reject(new Error('网络错误'))
|
||||
}
|
||||
|
||||
xhr.send(file.raw)
|
||||
})
|
||||
}
|
||||
const handleUploadFile = async (options: UploadUserFile) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await getUploadConfig(options)
|
||||
// 使用PUT或POST方法上传(根据后端要求)
|
||||
// const response = await fetch(uploadUrlV2.value, {
|
||||
// method: 'PUT', // 或 POST,根据后端预签名URL的规则
|
||||
// body: options.raw,
|
||||
// headers: {
|
||||
// 'Content-Type': (options.raw as Blob).type, // 设置文件类型
|
||||
// },
|
||||
// // onUploadProgress: (progressEvent: ProgressEvent) => {
|
||||
// // const percentCompleted = Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
||||
// // loadingtext.value = `上传中...${percentCompleted}%`
|
||||
// // },
|
||||
// })
|
||||
|
||||
// if (response.ok) {
|
||||
// await UploadFilled(options)
|
||||
// // 此时文件已上传至OSS,fileUrl即为访问地址
|
||||
// console.log('文件访问URL:', fileUrl.value)
|
||||
// } else {
|
||||
// loading.value = false
|
||||
// throw new Error('上传文件失败')
|
||||
// }
|
||||
const response = await uploadWithProgress(uploadUrlV2.value, options, (percent) => {
|
||||
loadingtext.value = `上传中...${percent}%`
|
||||
})
|
||||
if (response) {
|
||||
// 错误处理
|
||||
loading.value = false
|
||||
return ElMessage.error(options.name + '上传错误')
|
||||
}
|
||||
await UploadFilled(options)
|
||||
} catch (error) {
|
||||
loading.value = false
|
||||
ElMessage.error(options.name + '上传错误')
|
||||
}
|
||||
}
|
||||
|
||||
/*** 删除 */
|
||||
const handleDelete = (index: number) => {
|
||||
fileList.value.splice(index, 1)
|
||||
emit('validate')
|
||||
}
|
||||
|
||||
const dialogImageUrl = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const handleRemove = (uploadFile: any, uploadFiles: any) => {
|
||||
console.log(uploadFile, uploadFiles)
|
||||
fileList.value = fileList.value.filter((item: any) => item.uid !== uploadFile.uid)
|
||||
emit('validate')
|
||||
}
|
||||
|
||||
const handlePictureCardPreview = (uploadFile: any) => {
|
||||
dialogImageUrl.value = uploadFile.url
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handlePictureCardDown = (uploadFile: any) => {
|
||||
if (props.cadShow) {
|
||||
emit('preview', uploadFile)
|
||||
} else {
|
||||
window.open(uploadFile.url)
|
||||
}
|
||||
}
|
||||
// 生成 MD5 唯一标识符
|
||||
// const generateMd5 = () => {
|
||||
// const timestamp = new Date().getTime() // 获取当前时间戳
|
||||
// const randomString = Math.random().toString(36).substring(2) // 生成随机字符串
|
||||
// const data = `${timestamp}${randomString}` // 组合时间戳和随机字符串
|
||||
// return CryptoJS.MD5(data).toString() // 生成 MD5 哈希值
|
||||
// }
|
||||
|
||||
// 判断是否是图片
|
||||
const handelFileType = (fileName: string) => {
|
||||
const ext = fileName.split('.').pop()?.toLowerCase() || ''
|
||||
return ['png', 'jpg', 'jpeg'].includes(ext)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-preview-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.file-type-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -40%);
|
||||
}
|
||||
}
|
||||
.upload-file {
|
||||
::v-deep(.el-upload-dragger) {
|
||||
border: none !important;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-file-list .el-upload-list__item {
|
||||
line-height: 2;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
min-width: 361px;
|
||||
}
|
||||
|
||||
.upload-file-list .ele-upload-list__item-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.upload-item {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 5px;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.ele-upload-list__item-content-action .el-link {
|
||||
margin-right: 10px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.el-upload-list--picture-card .el-upload-list__item {
|
||||
height: 81px !important;
|
||||
}
|
||||
|
||||
// ::v-deep(.el-upload--picture-card) {
|
||||
// height: 81px !important;
|
||||
// }
|
||||
|
||||
// ::v-deep(.el-upload-list--picture-card) {
|
||||
// .el-upload-list__item {
|
||||
// height: 82px !important;
|
||||
// }
|
||||
// }
|
||||
|
||||
::v-deep(.el-upload-list--picture-card) {
|
||||
.el-upload-list__item {
|
||||
width: 161px;
|
||||
height: 81px;
|
||||
// margin: 0 8px 8px 0;
|
||||
}
|
||||
.el-upload--picture-card {
|
||||
width: 161px;
|
||||
height: 81px;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.el-loading-spinner) {
|
||||
.circular {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
margin-top: 5px !important;
|
||||
|
||||
.path {
|
||||
stroke-width: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.el-loading-spinner i) {
|
||||
font-size: 32px !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user