Files
aiot-platform-ui/apps/web-ele/src/components/upload/image-upload.vue
lrl 73bddd119e style: (web-ele)优化多个组件的样式和布局
-调整图像上传组件的样式,优化上传区域和处理按钮的布局
- 改进魔方编辑器组件的样式,统一边框和对齐方式
-统一垂直按钮组组件的样式,修复按钮圆角问题
2025-08-05 15:34:25 +08:00

419 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import type {
UploadFile,
UploadProgressEvent,
UploadRequestOptions,
} from 'element-plus';
import type { AxiosResponse } from '@vben/request';
import type { UploadListType } from './typing';
import type { AxiosProgressEvent } from '#/api/infra/file';
import { nextTick, ref, toRefs, watch } from 'vue';
import { CloudUpload } from '@vben/icons';
import { $t } from '@vben/locales';
import { isFunction, isObject, isString } from '@vben/utils';
import { ElMessage, ElUpload } from 'element-plus';
import { checkImgType, defaultImageAccepts } from './helper';
import { UploadResultStatus } from './typing';
import { useUpload, useUploadType } from './use-upload';
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
const props = withDefaults(
defineProps<{
// 根据后缀,或者其他
accept?: string[];
api?: (
file: File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
// 组件边框圆角
borderradius?: string;
// 上传的目录
directory?: string;
disabled?: boolean;
// 上传框高度
height?: number | string;
helpText?: string;
listType?: UploadListType;
// 最大数量的文件Infinity不限制
maxNumber?: number;
// 文件最大多少MB
maxSize?: number;
modelValue?: string | string[];
// 是否支持多选
multiple?: boolean;
// support xxx.xxx.xx
resultField?: string;
// 是否显示下面的描述
showDescription?: boolean;
// 上传框宽度
width?: number | string;
}>(),
{
modelValue: () => [],
directory: undefined,
disabled: false,
listType: 'picture-card',
helpText: '',
maxSize: 2,
maxNumber: 1,
accept: () => defaultImageAccepts,
multiple: false,
api: undefined,
resultField: '',
showDescription: true,
width: '',
height: '',
borderradius: '8px',
},
);
const emit = defineEmits(['change', 'update:modelValue', 'delete']);
const { accept, helpText, maxNumber, maxSize, width, height, borderradius } =
toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
acceptRef: accept,
helpTextRef: helpText,
maxNumberRef: maxNumber,
maxSizeRef: maxSize,
});
const fileList = ref<UploadFile[]>([]);
const isLtMsg = ref<boolean>(true); // 文件大小错误提示
const isActMsg = ref<boolean>(true); // 文件类型错误提示
const isFirstRender = ref<boolean>(true); // 是否第一次渲染
watch(
() => props.modelValue,
async (v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
return;
}
let value: string | string[] = [];
if (v) {
if (Array.isArray(v)) {
value = v;
} else {
value.push(v);
}
fileList.value = value
.map((item, i) => {
if (item && isString(item)) {
return {
uid: -i,
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: UploadResultStatus.SUCCESS,
url: item,
} as UploadFile;
} else if (item && isObject(item)) {
const file = item as Record<string, any>;
return {
uid: file.uid || -i,
name: file.name || '',
status: UploadResultStatus.SUCCESS,
url: file.url,
} as UploadFile;
}
return null;
})
.filter(Boolean) as UploadFile[];
}
if (!isFirstRender.value) {
emit('change', value);
isFirstRender.value = false;
}
},
{
immediate: true,
deep: true,
},
);
function getBase64<T extends ArrayBuffer | null | string>(file: File) {
return new Promise<T>((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => {
resolve(reader.result as T);
});
reader.addEventListener('error', (error) => reject(error));
});
}
const handlePreview = async (file: UploadFile) => {
if (!file.url) {
const preview = await getBase64<string>(file.raw!);
window.open(preview || '');
return;
}
window.open(file.url);
};
const handleRemove = async (file: UploadFile) => {
if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
index !== -1 && fileList.value.splice(index, 1);
const value = getValue();
isInnerOperate.value = true;
emit('update:modelValue', value);
emit('change', value);
emit('delete', file);
}
};
const beforeUpload = async (file: File) => {
const { maxSize, accept } = props;
const isAct = checkImgType(file, accept);
if (!isAct) {
ElMessage.error($t('ui.upload.acceptUpload', [accept]));
isActMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000);
return false;
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
ElMessage.error($t('ui.upload.maxSizeMultiple', [maxSize]));
isLtMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000);
return false;
}
return true;
};
async function customRequest(options: UploadRequestOptions) {
let { api } = props;
if (!api || !isFunction(api)) {
api = useUpload(props.directory).httpRequest;
}
try {
// 上传文件
const progressEvent: AxiosProgressEvent = (e) => {
const percent = Math.trunc((e.loaded / e.total!) * 100);
options.onProgress!({
percent,
total: e.total || 0,
loaded: e.loaded || 0,
lengthComputable: true,
} as unknown as UploadProgressEvent);
};
const res = await api?.(options.file, progressEvent);
options.onSuccess!(res);
ElMessage.success($t('ui.upload.uploadSuccess'));
// 更新文件
const value = getValue();
isInnerOperate.value = true;
emit('update:modelValue', value);
emit('change', value);
} catch (error: any) {
console.error(error);
options.onError!(error);
}
}
function getValue() {
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.SUCCESS)
.map((item: any) => {
if (item?.response && props?.resultField) {
return item?.response;
}
return item?.response?.url || item?.response;
});
// add by 芋艿:【特殊】单个文件的情况,获取首个元素,保证返回的是 String 类型
if (props.maxNumber === 1) {
return list.length > 0 ? list[0] : '';
}
return list;
}
// 编辑按钮:触发文件选择
const triggerEdit = () => {
if (props.disabled) return;
// 只查找当前 upload-box 下的 input
nextTick(() => {
const uploadBox = document.querySelector('.upload-box');
if (uploadBox) {
const input = uploadBox.querySelector(
'input[type="file"]',
) as HTMLInputElement | null;
if (input) input.click();
}
});
};
</script>
<template>
<div
class="upload-box"
:style="{
width: width || '150px',
height: height || '150px',
borderRadius: borderradius,
}"
>
<template
v-if="
fileList.length > 0 &&
fileList[0] &&
fileList[0].status === UploadResultStatus.SUCCESS
"
>
<div class="upload-image-wrapper">
<img :src="fileList[0].url" class="upload-image" />
<div class="upload-handle">
<div class="handle-icon" @click="handlePreview(fileList[0]!)">
<i class="el-icon el-icon-zoom-in"></i>
<span>详情</span>
</div>
<div v-if="!disabled" class="handle-icon" @click="triggerEdit">
<i class="el-icon el-icon-edit"></i>
<span>编辑</span>
</div>
<div
v-if="!disabled"
class="handle-icon"
@click="handleRemove(fileList[0]!)"
>
<i class="el-icon el-icon-delete"></i>
<span>删除</span>
</div>
</div>
</div>
</template>
<template v-else>
<ElUpload
v-bind="$attrs"
v-model:file-list="fileList"
:accept="getStringAccept"
:before-upload="beforeUpload"
:http-request="customRequest"
:disabled="disabled"
:list-type="listType"
:limit="maxNumber"
:multiple="multiple"
:on-preview="handlePreview"
:on-remove="handleRemove"
class="upload"
:style="{
width: width || '150px',
height: height || '150px',
borderRadius: borderradius,
}"
>
<div class="upload-content flex flex-col items-center justify-center">
<CloudUpload />
<div class="mt-2">{{ $t('ui.upload.imgUpload') }}</div>
</div>
</ElUpload>
</template>
<div v-if="showDescription" class="mt-2 text-xs text-gray-500">
{{ getStringAccept }}
</div>
</div>
</template>
<style lang="scss" scoped>
.upload-box {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
background: #fafafa;
border: 1px dashed var(--el-border-color-darker);
transition: border-color 0.2s;
.upload {
display: flex;
align-items: center;
justify-content: center;
width: 100% !important;
height: 100% !important;
background: transparent;
border: none !important;
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
cursor: pointer;
}
.upload-image-wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow: hidden;
background: #fff;
border-radius: inherit;
}
.upload-image {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
border-radius: inherit;
}
.upload-handle {
position: absolute;
top: 0;
right: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
cursor: pointer;
background: rgb(0 0 0 / 50%);
opacity: 0;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
.handle-icon {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0 8px;
font-size: 18px;
color: #fff;
span {
margin-top: 2px;
font-size: 12px;
}
}
}
.upload-image-wrapper:hover .upload-handle {
opacity: 1;
}
}
</style>