Add new components for login and comment functionality

This commit is contained in:
wangqiao
2025-08-17 20:15:33 +08:00
parent 99df1d1f81
commit 07b4d3de99
37 changed files with 4744 additions and 263 deletions

View 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)
// // 此时文件已上传至OSSfileUrl即为访问地址
// 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>