瀏覽代碼

Update 优化文件上传组件

Yue 3 月之前
父節點
當前提交
920a786b42

+ 12 - 0
SERVER/ChickenFarmV3/vb-modules/vb-system/src/main/java/cn/vber/system/controller/system/SysOssController.java

@@ -96,6 +96,7 @@ public class SysOssController extends BaseController {
         SysOssUploadVo uploadVo = new SysOssUploadVo();
         uploadVo.setUrl(oss.getUrl());
         uploadVo.setFileName(oss.getOriginalName());
+        uploadVo.setFileId(oss.getObjectId());
         uploadVo.setOssId(oss.getOssId().toString());
         return R.ok(uploadVo);
     }
@@ -124,6 +125,17 @@ public class SysOssController extends BaseController {
         ossService.download(ossId, response);
         R.ok();
     }
+    /**
+     * 下载OSS对象
+     *
+     * @param objectId OSS对象ObjectId
+     */
+    @SaCheckPermission("system:oss:download")
+    @GetMapping("/download/o/{objectId}")
+    public void downloadByObjectId(@PathVariable String objectId, HttpServletResponse response) throws IOException {
+        ossService.downloadByObjectId(objectId, response);
+        R.ok();
+    }
 
     /**
      * 删除OSS对象存储

+ 1 - 0
SERVER/ChickenFarmV3/vb-modules/vb-system/src/main/java/cn/vber/system/domain/SysOssExt.java

@@ -72,4 +72,5 @@ public class SysOssExt implements Serializable {
      */
     private String md5;
 
+    private String objectId;
 }

+ 4 - 0
SERVER/ChickenFarmV3/vb-modules/vb-system/src/main/java/cn/vber/system/domain/vo/SysOssUploadVo.java

@@ -19,6 +19,10 @@ public class SysOssUploadVo {
      * 文件名
      */
     private String fileName;
+    /**
+     * 文件ID
+     */
+    private String fileId;
 
     /**
      * 对象存储主键

+ 7 - 1
SERVER/ChickenFarmV3/vb-modules/vb-system/src/main/java/cn/vber/system/service/ISysOssService.java

@@ -92,7 +92,13 @@ public interface ISysOssService {
      * @param ossId    OSS对象ID
      * @param response HttpServletResponse对象,用于设置响应头和向客户端发送文件内容
      */
-    void download(Long ossId, HttpServletResponse response) throws IOException;
+    void download(Long ossId, HttpServletResponse response) throws IOException;  /**
+     * 文件下载方法,支持一次性下载完整文件
+     *
+     * @param objectId    OSS对象ID
+     * @param response HttpServletResponse对象,用于设置响应头和向客户端发送文件内容
+     */
+    void downloadByObjectId(String objectId, HttpServletResponse response) throws IOException;
 
     /**
      * 删除OSS对象存储

+ 19 - 2
SERVER/ChickenFarmV3/vb-modules/vb-system/src/main/java/cn/vber/system/service/impl/SysOssServiceImpl.java

@@ -190,7 +190,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
     public SysOssVo getByObjectId(String objectId) {
         TenantHelper.enableIgnore();
         try {
-            return baseMapper.selectVoOne(Wrappers.lambdaQuery(SysOss.class).eq(SysOss::getObjectId, objectId));
+            return baseMapper.selectVoList(Wrappers.lambdaQuery(SysOss.class).eq(SysOss::getObjectId, objectId)).get(0);
         } finally {
             TenantHelper.disableIgnore();
         }
@@ -205,6 +205,21 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
     @Override
     public void download(Long ossId, HttpServletResponse response) throws IOException {
         SysOssVo sysOss = SpringUtils.getAopProxy(this).getById(ossId);
+        downloadOss(response, sysOss);
+
+    }/**
+     * 文件下载方法,支持一次性下载完整文件
+     *
+     * @param objectId    OSS对象objectId
+     * @param response HttpServletResponse对象,用于设置响应头和向客户端发送文件内容
+     */
+    @Override
+    public void downloadByObjectId(String objectId, HttpServletResponse response) throws IOException {
+        SysOssVo sysOss = SpringUtils.getAopProxy(this).getByObjectId(objectId);
+        downloadOss(response, sysOss);
+    }
+
+    private void downloadOss(HttpServletResponse response, SysOssVo sysOss) throws IOException {
         if (ObjectUtil.isNull(sysOss)) {
             throw new ServiceException("文件数据不存在!");
         }
@@ -223,9 +238,9 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
                 throw new ServiceException(e.getMessage());
             }
         }
-
     }
 
+
     /**
      * 上传文件到对象存储服务,并保存文件信息到数据库
      *
@@ -297,6 +312,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
         UploadResult uploadResult = storage.uploadSuffix(file, suffix);
         SysOssExt ext1 = new SysOssExt();
         ext1.setFileSize(file.length());
+        ext1.setObjectId(uploadResult.getObjectId());
         // 保存文件信息
         return buildResultEntity(originalFileName, suffix, storage.getConfigKey(), uploadResult, ext1);
     }
@@ -318,6 +334,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
         UploadResult uploadResult = storage.uploadSuffix(file, suffix);
         SysOssExt ext1 = new SysOssExt();
         ext1.setFileSize(file.length());
+        ext1.setObjectId(uploadResult.getObjectId());
         // 保存文件信息
         return buildResultEntity(originalFileName, suffix, storage.getConfigKey(), uploadResult, ext1);
     }

+ 1 - 1
UI/VB.VUE/src/components/table/VbDataTable.vue

@@ -595,7 +595,7 @@ const loadHandleBtns = () => {
 			toolbarHandleBtns.value.push(
 				...(props.customBtns ? _formatterCustomBtns(props.customBtns) : [])
 			)
-			console.log("toolbarHandleBtns", toolbarHandleBtns.value)
+			// console.log("toolbarHandleBtns", toolbarHandleBtns.value)
 		})
 	} else {
 		toolbarHandleBtns.value = props.customBtns ? _formatterCustomBtns(props.customBtns) : []

+ 0 - 152
UI/VB.VUE/src/components/upload/ImportModal copy.vue

@@ -1,152 +0,0 @@
-<script setup lang="ts">
-const props = withDefaults(
-	defineProps<{
-		options?: any
-		url?: string
-		templateUrl?: string
-		templateName?: string
-		isAlertError?: boolean
-		accept?: string
-		headers?: any
-		title?: string
-		appendToBody?: boolean
-		dataParam?: object
-	}>(),
-	{
-		title: "",
-		headers: () => {
-			return {
-				Authorization: "Bearer " + getToken(),
-				ClientId: import.meta.env.VITE_APP_CLIENT_ID
-			}
-		},
-		accept: ".xlsx, .xls",
-		templateUrl: "",
-		templateName: "",
-		isAlertError: true,
-		appendToBody: true
-	}
-)
-const params = computed(() => {
-	let str = ""
-	if (opts.value.dataParam) {
-		let data = {}
-		data =
-			typeof opts.value.dataParam === "function" ? opts.value.dataParam() : opts.value.dataParam
-		if (data && Object.keys(data).length > 0) {
-			Object.keys(data).forEach((key) => {
-				str += `&${key}=${data[key]}`
-			})
-		}
-	}
-	return str
-})
-const emits = defineEmits<{
-	(e: "modelValue:value", v: string): void
-	(e: "onProgress", event: any, file: any, fileList: any): void
-	(e: "onSuccess", response: any, file: any, fileList: any): void
-	(e: "onError", error: Error, file: any, fileList: any): void
-	(e: "onConfirm"): void
-}>()
-const uploadModalRef = ref()
-const uploadRef = ref()
-const uploadUpdateSupport = ref(true)
-const isUploading = ref(false)
-const opts = ref<any>({})
-function submitUpload() {
-	uploadRef.value.submit()
-	emits("onConfirm")
-}
-/** 文件上传中处理 */
-function handleFileUploadProgress(event: any, file: any, fileList: any) {
-	isUploading.value = true
-	message.loading()
-	emits("onProgress", event, file, fileList)
-}
-/** 文件上传成功处理 */
-function handleFileSuccess(response: any, file: any, fileList: any) {
-	isUploading.value = false
-	uploadRef.value.handleRemove(file)
-	message.alert(
-		"<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" +
-			response.msg +
-			"</div>",
-		"导入结果",
-		{ dangerouslyUseHTMLString: true }
-	)
-	message.closeLoading()
-	emits("onSuccess", response, file, fileList)
-}
-function handleFileError(error: Error, file: any, fileList: any) {
-	message.closeLoading()
-	if (opts.isAlertError) {
-		message.msgError(error.message)
-	}
-	emits("onError", error, file, fileList)
-}
-function importTemplate() {
-	download(
-		opts.value.templateUrl,
-		`${opts.value.templateName}_TEMPLATE_${new Date().getTime()}.xlsx`
-	)
-}
-function init() {
-	opts.value = props.options ? Object.assign({}, props, props.options) : props
-	console.log(opts.value, props.options)
-	if (!opts.value.url) {
-		throw new Error("uploadUrl不能为空!")
-	}
-}
-function show() {
-	uploadModalRef.value.show()
-}
-onMounted(init)
-defineExpose({ show })
-</script>
-<template>
-	<VbModal
-		v-model:modal="uploadModalRef"
-		:title="opts.title"
-		@confirm="submitUpload"
-		:append-to-body="appendToBody"
-		v-bind="$attrs">
-		<template #body>
-			<slot name="other_form"></slot>
-			<el-upload
-				ref="uploadRef"
-				:limit="1"
-				:accept="opts.accept"
-				:headers="opts.headers"
-				:action="opts.url + '?updateSupport=' + uploadUpdateSupport + params"
-				:disabled="isUploading"
-				:on-progress="handleFileUploadProgress"
-				:on-success="handleFileSuccess"
-				:on-error="handleFileError"
-				:auto-upload="false"
-				drag>
-				<el-icon class="el-icon--upload"><upload-filled /></el-icon>
-				<div class="el-upload__text">
-					将文件拖到此处,或
-					<em>点击上传</em>
-				</div>
-				<template #tip>
-					<div class="el-upload__tip text-center">
-						<div class="el-upload__tip">
-							<el-checkbox v-model="uploadUpdateSupport" />
-							是否更新已经存在的数据
-						</div>
-						<span>仅允许导入{{ opts.accept }}格式文件。</span>
-						<el-link
-							v-if="opts.templateUrl"
-							type="primary"
-							:underline="false"
-							style="font-size: 12px; vertical-align: baseline"
-							@click="importTemplate">
-							下载模板
-						</el-link>
-					</div>
-				</template>
-			</el-upload>
-		</template>
-	</VbModal>
-</template>

+ 285 - 0
UI/VB.VUE/src/components/upload/VbPdfPreview.vue

@@ -0,0 +1,285 @@
+<script setup lang="ts">
+import Rs from "@@/services/RequestService"
+import { isExternal } from "@@/utils/validate"
+
+const props = withDefaults(
+	defineProps<{
+		src?: string
+		previewUrl?: string
+		fileId?: string
+		fileName?: string
+		fileExt?: string
+		prefixSrc?: string
+	}>(),
+	{
+		previewUrl: "resource/oss/preview",
+		downloadUrl: "resource/oss/download",
+		fileExt: "pdf",
+		prefixSrc: ""
+	}
+)
+const emits = defineEmits<{ (e: "update:modelValue", v: string): void }>()
+
+const fileUrl = ref(props.src)
+const visible = ref(false)
+const loading = ref(false)
+const fileName = ref("")
+const fileExt = ref(props.fileExt)
+
+function formatUrl(url: string) {
+	if (isExternal(url)) {
+		return url
+	}
+
+	// 确保URL以"/"结尾
+	let baseUrl = import.meta.env.VITE_APP_BASE_API
+	if (baseUrl && !baseUrl.endsWith("/")) {
+		baseUrl += "/"
+	}
+
+	// 确保prefixSrc以"/"开头,但不以"/"结尾
+	let prefix = props.prefixSrc ? props.prefixSrc : ""
+	if (prefix && !prefix.startsWith("/")) {
+		prefix = "/" + prefix
+	}
+	if (prefix && prefix.endsWith("/")) {
+		prefix = prefix.slice(0, -1)
+	}
+
+	// 确保url以"/"开头
+	if (!url.startsWith("/")) {
+		url = "/" + url
+	}
+
+	url = baseUrl + prefix + url
+	if (url.includes("?")) {
+		url += "&inline=true"
+	} else {
+		url += "?inline=true"
+	}
+	return url
+}
+
+function load(fileId?: string, src?: string) {
+	// 清理之前的对象URL
+	if (fileUrl.value && fileUrl.value.startsWith("blob:")) {
+		URL.revokeObjectURL(fileUrl.value)
+		fileUrl.value = ""
+	}
+	const targetFileId = fileId || props.fileId
+	const targetSrc = src || props.src
+
+	if (!targetSrc && !targetFileId) {
+		fileUrl.value = ""
+		return
+	}
+
+	if (props.previewUrl && targetFileId) {
+		let requestUrl = props.previewUrl
+		if (!requestUrl.endsWith("/")) {
+			requestUrl += "/"
+		}
+
+		loading.value = true
+		Rs.get({
+			url: requestUrl + targetFileId,
+			loading: false,
+			responseType: "blob"
+		})
+			.then((res: any) => {
+				loading.value = false
+				if (res.type === "application/json") {
+					return
+				}
+				const data = new Blob([res], { type: `application/${fileExt.value}` })
+				fileUrl.value = URL.createObjectURL(data)
+			})
+			.catch(() => {
+				loading.value = false
+			})
+	} else if (targetFileId) {
+		fileUrl.value = formatUrl(targetFileId)
+	} else {
+		fileUrl.value = formatUrl(targetSrc || "")
+	}
+}
+
+function init() {
+	load(props.fileId, props.src)
+}
+
+function open(options: { fileId?: string; src?: string; fileName?: string; fileExt?: string }) {
+	visible.value = true
+	if (options.fileName) {
+		if (!options.fileExt) {
+			fileExt.value = options.fileName.split(".").pop()
+		}
+		fileName.value = options.fileName
+	}
+	load(options.fileId, options.src)
+}
+
+function close() {
+	visible.value = false
+	// 清理对象URL以释放内存
+	if (fileUrl.value && fileUrl.value.startsWith("blob:")) {
+		URL.revokeObjectURL(fileUrl.value)
+		fileUrl.value = ""
+	}
+	fileName.value = props.fileName
+	fileExt.value = props.fileExt
+}
+
+onMounted(init)
+
+defineExpose({
+	open,
+	close
+})
+</script>
+
+<template>
+	<div v-if="visible" class="pdf-preview-container">
+		<div class="pdf-preview-overlay">
+			<div class="pdf-preview-header">
+				<button class="close-btn" @click="close">✕</button>
+				<div v-if="fileName" class="file-name">{{ fileName }}</div>
+			</div>
+			<div class="pdf-preview-content">
+				<div v-if="loading" class="loading-container">
+					<div class="loading-spinner"></div>
+					<p>文件加载中...</p>
+				</div>
+				<template v-else>
+					<object
+						v-if="fileUrl"
+						:data="fileUrl"
+						:type="`application/${fileExt}`"
+						class="office-object">
+						<p style="margin-top: -20%">
+							您的浏览器不支持
+							<span class="px-2 text-danger">{{ fileExt.toUpperCase() }}</span>
+							文件预览,请
+							<a :href="fileUrl" :download="fileName">下载文件</a>
+							查看。
+						</p>
+					</object>
+					<div v-else class="no-preview">无预览内容</div>
+				</template>
+			</div>
+		</div>
+	</div>
+</template>
+
+<style scoped>
+.pdf-preview-container {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100vw;
+	height: 100vh;
+	z-index: 9999;
+}
+
+.pdf-preview-overlay {
+	width: 100%;
+	height: 100%;
+	background: rgba(0, 0, 0, 0.5);
+	display: flex;
+	flex-direction: column;
+}
+
+.pdf-preview-header {
+	height: 50px;
+	padding: 10px;
+	background: white;
+	position: relative;
+	display: flex;
+	align-items: center;
+}
+
+.close-btn {
+	width: 30px;
+	height: 30px;
+	border: none;
+	background: #f2f2f2;
+	border-radius: 50%;
+	font-size: 16px;
+	cursor: pointer;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	margin-right: 10px;
+}
+
+.close-btn:hover {
+	background: #e0e0e0;
+}
+
+.file-name {
+	font-size: 16px;
+	font-weight: 600;
+	color: var(--bs-primary);
+	flex: 1;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	padding-right: 20px;
+}
+
+.pdf-preview-content {
+	flex: 1;
+	background: white;
+	padding: 10px;
+	position: relative;
+}
+
+.office-object {
+	width: 100%;
+	height: 100%;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+}
+
+.no-preview {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	height: 100%;
+	color: #999;
+	font-size: 14px;
+}
+
+.loading-container {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	background: rgba(255, 255, 255, 0.8);
+	z-index: 10;
+}
+
+.loading-spinner {
+	width: 40px;
+	height: 40px;
+	border: 4px solid #f3f3f3;
+	border-top: 4px solid #409eff;
+	border-radius: 50%;
+	animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+	0% {
+		transform: rotate(0deg);
+	}
+	100% {
+		transform: rotate(360deg);
+	}
+}
+</style>

+ 62 - 66
UI/VB.VUE/src/components/upload/VbUpload.vue

@@ -2,7 +2,7 @@
 const props = withDefaults(
 	defineProps<{
 		modelValue: string | any[]
-		prefixUrl?: string
+		previewPrefixUrl?: string
 		uploadType?: "image" | "file"
 		uploadUrl?: string
 		limit?: number // 图片数量限制
@@ -12,7 +12,8 @@ const props = withDefaults(
 	}>(),
 	{
 		uploadType: "image",
-		prefixUrl: "/resource/oss/",
+		uploadUrl: "resource/oss/upload",
+		previewPrefixUrl: "resource/oss",
 		limit: 5,
 		fileSize: 5,
 		fileType: () => ["png", "jpg", "jpeg"],
@@ -30,8 +31,10 @@ const uploadList = ref<any[]>([])
 const showViewer = ref(false)
 const viewerInitialIndex = ref(0)
 const baseUrl = import.meta.env.VITE_APP_BASE_API
-const uploadImgUrl = ref(
-	import.meta.env.VITE_APP_BASE_API + (props.uploadUrl ? props.uploadUrl : "/resource/oss/upload")
+const uploadImgFileUrl = ref(
+	import.meta.env.VITE_APP_BASE_API +
+		"/" +
+		(props.uploadUrl ? props.uploadUrl : "resource/oss/upload")
 ) // 上传的图片服务器地址
 const headers = ref({
 	Authorization: "Bearer " + getToken(),
@@ -92,20 +95,22 @@ function handleUploadError() {
 
 // 上传成功回调
 function handleUploadSuccess(res: any, file: any) {
-	//console.log("RES", res, file)
+	console.log("RES", res, file)
 	if (res.code === 200 && res.data.url) {
-		let url = `${baseUrl}/${
-			props.prefixUrl
-				? props.prefixUrl.lastIndexOf("/") == props.prefixUrl.length - 1
-					? props.prefixUrl
-					: props.prefixUrl + "/"
-				: ""
-		}${res.data.url}`
+		let url = `${baseUrl}/${props.previewPrefixUrl ? props.previewPrefixUrl : ""}/${res.data.url}`
+		url = url
+			.replace("https://", "$$$$")
+			.replace("http://", "@@@@")
+			.replace(/\/\//g, "/")
+			.replace("$$$$", "https://")
+			.replace("@@@@", "http://")
+		console.log("uploadList", res.data)
 		uploadList.value.push({
-			id: res.data.ossId,
+			url: url,
 			name: res.data.url,
-			realUrl: url,
-			url: file.url
+			fileName: res.data.fileName,
+			fileId: res.data.fileId,
+			ossId: res.data.ossId
 		})
 		uploadedSuccessfully()
 	} else {
@@ -156,22 +161,13 @@ function handlePictureCardPreview(file: any) {
 	showViewer.value = true
 }
 
-// 获取文件名称
-function getFileName(name: string) {
-	if (name.lastIndexOf("/") > -1) {
-		return name.slice(name.lastIndexOf("/") + 1)
-	} else {
-		return ""
-	}
-}
-
 // 对象转成指定字符串分隔
 function listToString(list: any[], separator?: string) {
 	let str = ""
 	separator = separator ?? ","
 	list.forEach((v) => {
 		if (undefined !== v.url && v.url.indexOf("blob:") !== 0) {
-			str += v.name + separator
+			str += `${v.fileId}$${v.fileName}$${v.ossId}` + separator
 		}
 	})
 	return str != "" ? str.substring(0, str.length - 1) : ""
@@ -179,67 +175,59 @@ function listToString(list: any[], separator?: string) {
 
 function initFile(files: any) {
 	if (files) {
+		let temp = 0
 		// 首先将值转为数组
 		let list = Array.isArray(files) ? files : (files as string)?.split(",")
 		// 然后将数组转为对象数组
-		list = list.map((item) => {
+		fileList.value = list.map((item) => {
 			if (typeof item === "string") {
-				if (!item.includes(baseUrl)) {
-					let url = `${baseUrl}/${
-						props.prefixUrl
-							? props.prefixUrl.lastIndexOf("/") == props.prefixUrl.length - 1
-								? props.prefixUrl
-								: props.prefixUrl + "/"
-							: ""
-					}${item}`
-					item = {
-						name: item,
-						realUrl: url
-					}
+				const itemArr = item.split("$")
+				if (itemArr.length == 1) {
+					itemArr.push("01.pdf")
+				}
+				if (itemArr.length == 2) {
+					itemArr.push("0")
+				}
+				item = {
+					fileId: itemArr[0],
+					fileName: itemArr[1],
+					ossId: itemArr[2],
+					index: temp
+				}
+				if (!itemArr[0].includes(baseUrl)) {
+					let url = `${baseUrl}/${props.previewPrefixUrl ? props.previewPrefixUrl : ""}/${itemArr[0]}`
+					url = url
+						.replace("https://", "$$$$")
+						.replace("http://", "@@@@")
+						.replace(/\/\//g, "/")
+						.replace("$$$$", "https://")
+						.replace("@@@@", "http://")
+					item.url = url
+					item.name = itemArr[0]
 				} else {
-					item = { name: item, url: item }
+					item.url = itemArr[0]
+					item.name = itemArr[0]
 				}
+				item.uid = item.uid || new Date().getTime() + temp++
 			}
 			return item
 		})
-		fileList.value = []
-		list.forEach((v: any) => {
-			if (v.url) {
-				fileList.value.push({
-					name: v.name,
-					realUrl: v.realUrl,
-					url: v.url
-				})
-			} else {
-				RequestService.get({ url: v.realUrl, loading: false, responseType: "blob" }).then((res) => {
-					if (res.status === 200) {
-						const blobUrl = URL.createObjectURL(res.data)
-						fileList.value.push({
-							name: v.name,
-							url: blobUrl,
-							realUrl: v.realUrl
-						})
-					}
-				})
-			}
-		})
 	} else {
 		fileList.value = []
 	}
 }
 
-function init() {
-	initFile(props.modelValue)
-}
+function init() {}
 
 onMounted(init)
+watch(() => props.modelValue, initFile)
 </script>
 <template>
 	<div class="component-upload-image">
 		<el-upload
 			multiple
 			ref="imageUploadRef"
-			:action="uploadImgUrl"
+			:action="uploadImgFileUrl"
 			:list-type="isImage ? 'picture-card' : undefined"
 			:file-list="fileList"
 			:limit="limit"
@@ -291,8 +279,8 @@ onMounted(init)
 				:key="file.uid"
 				class="el-upload-list__item ele-upload-list__item-content"
 				v-for="(file, index) in fileList">
-				<el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
-					<span class="el-icon-document">{{ getFileName(file.name) }}</span>
+				<el-link :href="`${file.url}`" :underline="false" target="_blank">
+					<span class="el-icon-document">{{ file.fileName }}</span>
 				</el-link>
 				<div class="ele-upload-list__item-content-action">
 					<el-link :underline="false" @click="handleDeleteFile(index)" type="danger">删除</el-link>
@@ -307,6 +295,14 @@ onMounted(init)
 :deep(.hide .el-upload--picture-card) {
 	display: none;
 }
+:deep(.el-link) {
+	width: 100%;
+	justify-content: flex-start;
+	.el-link__inner {
+		padding: 0 10px;
+		color: var(--bs-primary);
+	}
+}
 
 .upload-file-uploader {
 	margin-bottom: 5px;