1.1.58 邮寄流程上传的附件图片可以预览。

dev
yinq 5 days ago
parent acfb43ad74
commit f4d762d0d0

@ -35,11 +35,20 @@
<li v-for="(file, index) in fileList" :key="file.uid" class="el-upload-list__item ele-upload-list__item-content">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
<div class="ele-upload-list__item-content-action">
<el-link v-if="isImageFile(file)" type="primary" :underline="false" @click="handlePreview(file)"></el-link>
<el-link type="primary" :underline="false" @click="handleDownload(file)"></el-link>
<el-link type="danger" v-if="!disabled" :underline="false" @click="handleDelete(index)"></el-link>
</div>
</li>
</transition-group>
<el-image-viewer
v-if="previewVisible"
:url-list="previewUrlList"
:initial-index="previewIndex"
teleported
@close="previewVisible = false"
/>
</div>
</template>
@ -47,6 +56,7 @@
import { propTypes } from '@/utils/propTypes';
import { delOss, listByIds } from '@/api/system/oss';
import { globalHeaders } from '@/utils/request';
import { isImageFileName } from '@/utils/ossPreview';
const props = defineProps({
modelValue: {
@ -75,6 +85,9 @@ const uploadFileUrl = ref(baseUrl + '/resource/oss/upload'); // 上传文件服
const headers = ref(globalHeaders());
const fileList = ref<any[]>([]);
const previewVisible = ref(false);
const previewUrlList = ref<string[]>([]);
const previewIndex = ref(0);
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
const fileUploadRef = ref<ElUploadInstance>();
@ -183,6 +196,18 @@ const handleDownload = (file: any) => {
}
};
const isImageFile = (file: any) => isImageFileName(file.name);
const handlePreview = (file: any) => {
const imageFiles = fileList.value.filter((item) => isImageFile(item) && item.url);
previewUrlList.value = imageFiles.map((item) => item.url);
previewIndex.value = Math.max(
imageFiles.findIndex((item) => item.url === file.url),
0
);
previewVisible.value = true;
};
//
const handleDelete = (index: number) => {
const ossId = fileList.value[index].ossId;

@ -0,0 +1,164 @@
<template>
<div v-loading="loading" class="oss-file-preview">
<div v-if="imageFiles.length" class="oss-file-preview__images">
<div v-for="(file, index) in imageFiles" :key="file.ossId || index" class="oss-file-preview__item">
<el-image
:src="file.url"
fit="cover"
:style="{ width: thumbSize, height: thumbSize }"
:preview-src-list="imagePreviewList"
:initial-index="index"
preview-teleported
>
<template #error>
<div class="oss-file-preview__error">
<el-icon><picture-filled /></el-icon>
</div>
</template>
</el-image>
<span v-if="showFileName" class="oss-file-preview__name" :title="getDisplayName(file)">{{ getDisplayName(file) }}</span>
</div>
</div>
<ul v-if="otherFiles.length && !imageOnly" class="oss-file-preview__files">
<li v-for="file in otherFiles" :key="file.ossId" class="oss-file-preview__file-item">
<el-icon><document /></el-icon>
<el-link type="primary" :underline="false" @click="handleDownload(file)">{{ getDisplayName(file) }}</el-link>
</li>
</ul>
<span v-if="!loading && !fileList.length" class="oss-file-preview__empty">{{ emptyText }}</span>
</div>
</template>
<script setup lang="ts">
import { listByIds } from '@/api/system/oss';
import type { OssVO } from '@/api/system/oss/types';
import { isImageOssFile, parseOssIds } from '@/utils/ossPreview';
import { propTypes } from '@/utils/propTypes';
const props = defineProps({
/** OSS 文件 ID支持逗号分隔字符串、单个 ID 或 ID 数组 */
ossIds: {
type: [String, Number, Array],
default: ''
},
/** 缩略图尺寸 */
thumbSize: propTypes.string.def('80px'),
/** 是否仅展示图片 */
imageOnly: propTypes.bool.def(false),
/** 是否显示文件名 */
showFileName: propTypes.bool.def(false),
/** 无附件时的提示文案 */
emptyText: propTypes.string.def('暂无附件')
});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const loading = ref(false);
const fileList = ref<OssVO[]>([]);
const imageFiles = computed(() => fileList.value.filter((file) => isImageOssFile(file)));
const otherFiles = computed(() => fileList.value.filter((file) => !isImageOssFile(file)));
const imagePreviewList = computed(() => imageFiles.value.map((file) => file.url));
const getDisplayName = (file: OssVO) => file.originalName || file.fileName || '未命名文件';
const loadFiles = async (value?: string | number | Array<string | number>) => {
const ids = parseOssIds(value);
if (!ids.length) {
fileList.value = [];
return;
}
loading.value = true;
try {
const res = await listByIds(ids.join(','));
fileList.value = res.data || [];
} catch (error) {
console.error('加载附件预览失败:', error);
fileList.value = [];
} finally {
loading.value = false;
}
};
const handleDownload = (file: OssVO) => {
if (file.ossId) {
proxy?.$download.oss(file.ossId);
}
};
watch(
() => props.ossIds,
(val) => {
loadFiles(val);
},
{ immediate: true }
);
</script>
<style lang="scss" scoped>
.oss-file-preview {
min-height: 32px;
}
.oss-file-preview__images {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.oss-file-preview__item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
max-width: 120px;
}
.oss-file-preview__name {
width: 100%;
font-size: 12px;
color: #606266;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.oss-file-preview__error {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #909399;
font-size: 24px;
background: #f5f7fa;
}
.oss-file-preview__files {
margin: 8px 0 0;
padding: 0;
list-style: none;
}
.oss-file-preview__file-item {
display: flex;
align-items: center;
gap: 6px;
line-height: 28px;
color: #606266;
}
.oss-file-preview__empty {
color: #909399;
font-size: 13px;
}
:deep(.el-image) {
border-radius: 4px;
border: 1px solid #ebeef5;
cursor: pointer;
}
</style>

@ -0,0 +1,37 @@
/** 支持预览的图片扩展名 */
export const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'];
/** 根据文件名判断是否为可预览图片 */
export function isImageFileName(fileName?: string): boolean {
if (!fileName) {
return false;
}
const ext = fileName.split('.').pop()?.toLowerCase() || '';
return IMAGE_EXTENSIONS.includes(ext);
}
/** 根据 OSS 文件信息判断是否为可预览图片 */
export function isImageOssFile(file?: { fileSuffix?: string; originalName?: string; name?: string; url?: string }): boolean {
if (!file) {
return false;
}
if (file.fileSuffix) {
const suffix = file.fileSuffix.startsWith('.') ? file.fileSuffix.slice(1) : file.fileSuffix;
return IMAGE_EXTENSIONS.includes(suffix.toLowerCase());
}
return isImageFileName(file.originalName || file.name);
}
/** 从 ossId 字符串或数组中解析 ID 列表 */
export function parseOssIds(value?: string | number | Array<string | number>): string[] {
if (value === undefined || value === null || value === '') {
return [];
}
if (Array.isArray(value)) {
return value.map((item) => String(item)).filter(Boolean);
}
return String(value)
.split(',')
.map((item) => item.trim())
.filter(Boolean);
}

@ -27,7 +27,7 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="邮寄申请编号" prop="mailingApplyCode">
<el-input v-model="form.mailingApplyCode" placeholder="自动生成邮寄申请编号" readonly>
<el-input v-model="form.mailingApplyCode" placeholder="自动生成邮寄申请编号" :disabled="!isFormEditable" >
<template #append>
<el-button type="primary" @click="generateMailingApplyCode" :disabled="isCodeGenerated">生成邮寄申请编号</el-button>
</template>
@ -155,7 +155,7 @@
:disabled="!isFormEditable"
:isShowTip="true"
/>
<div style="color: #f56c6c; font-size: 12px; margin-top: 5px">提示请上传邮寄实物图片和快递单截图</div>
<div style="color: #f56c6c; font-size: 12px; margin-top: 5px">提示请上传邮寄实物图片和快递单截图图片可点击预览</div>
</el-form-item>
</el-col>
<el-col :span="12">
@ -362,6 +362,7 @@ const initFormData: CrmMailingApplyForm = {
const data = reactive<{ form: CrmMailingApplyForm; rules: any }>({
form: { ...initFormData },
rules: {
mailingApplyCode: [{ required: true, message: '邮寄申请编号不能为空', trigger: 'blur' }],
applicationDate: [{ required: true, message: '申请日期不能为空', trigger: 'blur' }],
handlerId: [{ required: true, message: '经手人不能为空', trigger: 'change' }],
province: [{ required: true, message: '省份不能为空', trigger: 'blur' }],
@ -596,7 +597,7 @@ const submitForm = async (status: string, mode: boolean) => {
try {
if (status === 'draft') {
await mailingApplyFormRef.value.validateField(['applicationDate', 'handlerId', 'province', 'weight', 'mailingType', 'mailingFee', 'itemInfo']);
await mailingApplyFormRef.value.validateField(['mailingApplyCode', 'applicationDate', 'handlerId', 'province', 'weight', 'mailingType', 'mailingFee', 'itemInfo']);
} else {
await mailingApplyFormRef.value.validate();
}

Loading…
Cancel
Save