初始版本提交
This commit is contained in:
4
src/components/Upload/index.ts
Normal file
4
src/components/Upload/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { withInstall } from '/@/utils';
|
||||
import basicUpload from './src/BasicUpload.vue';
|
||||
|
||||
export const BasicUpload = withInstall(basicUpload);
|
||||
146
src/components/Upload/src/BasicUpload.vue
Normal file
146
src/components/Upload/src/BasicUpload.vue
Normal file
@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div>
|
||||
<Space>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="openUploadModal"
|
||||
:disabled="bindValue.disabled"
|
||||
preIcon="carbon:cloud-upload"
|
||||
>
|
||||
上传
|
||||
</a-button>
|
||||
<Tooltip placement="bottom" v-if="showPreview">
|
||||
<template #title>
|
||||
{{ t('component.upload.uploaded') }}
|
||||
<template v-if="fileList.length">
|
||||
{{ fileList.length }}
|
||||
</template>
|
||||
</template>
|
||||
<a-button :disabled="bindValue.disabled" @click="openPreviewModal">
|
||||
<Icon icon="bi:eye" />
|
||||
<template v-if="fileList.length && showPreviewNumber">
|
||||
{{ fileList.length }}
|
||||
</template>
|
||||
</a-button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
<UploadModal
|
||||
v-bind="bindValue"
|
||||
:previewFileList="fileList"
|
||||
:folderId="folderId"
|
||||
@register="registerUploadModal"
|
||||
@change="handleChange"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
|
||||
<UploadPreviewModal
|
||||
:value="fileList"
|
||||
:file-names="fileNameList"
|
||||
@register="registerPreviewModal"
|
||||
@list-change="handlePreviewChange"
|
||||
@delete="handlePreviewDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, unref, computed } from 'vue';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { Tooltip, Space } from 'ant-design-vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { uploadContainerProps } from './props';
|
||||
import { omit } from 'lodash-es';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import UploadModal from './UploadModal.vue';
|
||||
import UploadPreviewModal from './UploadPreviewModal.vue';
|
||||
import { getFileList } from '/@/api/system/file';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicUpload',
|
||||
components: { UploadModal, Space, UploadPreviewModal, Icon, Tooltip },
|
||||
props: uploadContainerProps,
|
||||
emits: ['change', 'delete', 'preview-delete', 'update:value'],
|
||||
|
||||
setup(props, { emit, attrs }) {
|
||||
console.log('props.value1111', props.api);
|
||||
const { t } = useI18n();
|
||||
// 上传modal
|
||||
const [registerUploadModal, { openModal: openUploadModal }] = useModal();
|
||||
|
||||
// 预览modal
|
||||
const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
|
||||
|
||||
const fileList = ref<string[]>([]);
|
||||
const fileNameList = ref<string[]>([]);
|
||||
|
||||
const folderId = computed(() => props.value);
|
||||
const showPreview = computed(() => {
|
||||
const { emptyHidePreview } = props;
|
||||
if (!emptyHidePreview) return true;
|
||||
return emptyHidePreview ? fileList.value.length > 0 : true;
|
||||
});
|
||||
|
||||
const bindValue = computed(() => {
|
||||
const value = { ...attrs, ...props };
|
||||
return omit(value, 'onChange');
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
async (value) => {
|
||||
//如果没有传入参数 默认不再请求文件列表
|
||||
if (value && value.length > 0) {
|
||||
const list = await getFileList({ folderId: value });
|
||||
fileList.value = list.map((item) => item.fileUrl);
|
||||
fileNameList.value = list.map((item) => item.fileName);
|
||||
} else {
|
||||
fileList.value = [];
|
||||
}
|
||||
emit('update:value', value);
|
||||
emit('change', value);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 上传modal保存操作
|
||||
function handleChange(urls: string[], folderId: string, fileNames: string[]) {
|
||||
fileList.value = [...unref(fileList), ...(urls || [])];
|
||||
fileNameList.value = [...unref(fileNameList), ...(fileNames || [])];
|
||||
emit('update:value', folderId);
|
||||
emit('change', folderId);
|
||||
}
|
||||
|
||||
// 预览modal保存操作
|
||||
function handlePreviewChange(urls: string[], folderId: string, fileNames: string[]) {
|
||||
fileList.value = [...(urls || [])];
|
||||
fileNameList.value = [...(fileNames || [])];
|
||||
emit('update:value', folderId);
|
||||
emit('change', folderId);
|
||||
}
|
||||
|
||||
function handleDelete(record: Recordable) {
|
||||
emit('delete', record);
|
||||
}
|
||||
|
||||
function handlePreviewDelete(url: string) {
|
||||
emit('preview-delete', url);
|
||||
}
|
||||
|
||||
return {
|
||||
registerUploadModal,
|
||||
openUploadModal,
|
||||
handleChange,
|
||||
handlePreviewChange,
|
||||
registerPreviewModal,
|
||||
openPreviewModal,
|
||||
fileList,
|
||||
fileNameList,
|
||||
showPreview,
|
||||
bindValue,
|
||||
handleDelete,
|
||||
handlePreviewDelete,
|
||||
folderId,
|
||||
t,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
104
src/components/Upload/src/FileList.vue
Normal file
104
src/components/Upload/src/FileList.vue
Normal file
@ -0,0 +1,104 @@
|
||||
<script lang="tsx">
|
||||
import { defineComponent, CSSProperties, watch, nextTick } from 'vue';
|
||||
import { fileListProps } from './props';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
import { useModalContext } from '/@/components/Modal/src/hooks/useModalContext';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileList',
|
||||
props: fileListProps,
|
||||
setup(props) {
|
||||
const modalFn = useModalContext();
|
||||
watch(
|
||||
() => props.dataSource,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
modalFn?.redoModalHeight?.();
|
||||
});
|
||||
},
|
||||
);
|
||||
return () => {
|
||||
const { columns, actionColumn, dataSource } = props;
|
||||
const columnList = [...columns, actionColumn];
|
||||
return (
|
||||
<table class="file-table">
|
||||
<colgroup>
|
||||
{columnList.map((item) => {
|
||||
const { width = 0, dataIndex } = item;
|
||||
const style: CSSProperties = {
|
||||
width: `${width}px`,
|
||||
minWidth: `${width}px`,
|
||||
};
|
||||
return <col style={width ? style : {}} key={dataIndex} />;
|
||||
})}
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr class="file-table-tr">
|
||||
{columnList.map((item) => {
|
||||
const { title = '', align = 'center', dataIndex } = item;
|
||||
return (
|
||||
<th class={['file-table-th', align]} key={dataIndex}>
|
||||
{title}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dataSource.map((record = {}, index) => {
|
||||
return (
|
||||
<tr class="file-table-tr" key={`${index + record.name || ''}`}>
|
||||
{columnList.map((item) => {
|
||||
const { dataIndex = '', customRender, align = 'center' } = item;
|
||||
const render = customRender && isFunction(customRender);
|
||||
return (
|
||||
<td class={['file-table-td', align]} key={dataIndex}>
|
||||
{render
|
||||
? customRender?.({ text: record[dataIndex], record })
|
||||
: record[dataIndex]}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.file-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&-th,
|
||||
&-td {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: @background-color-light;
|
||||
}
|
||||
|
||||
table,
|
||||
td,
|
||||
th {
|
||||
border: 1px solid @border-color-base;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
29
src/components/Upload/src/ThumbUrl.vue
Normal file
29
src/components/Upload/src/ThumbUrl.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<span class="thumb">
|
||||
<Image v-if="fileUrl" :src="fileUrl" :width="104" />
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { Image } from 'ant-design-vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { Image },
|
||||
props: {
|
||||
fileUrl: propTypes.string.def(''),
|
||||
fileName: propTypes.string.def(''),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.thumb {
|
||||
img {
|
||||
position: static;
|
||||
display: block;
|
||||
cursor: zoom-in;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
342
src/components/Upload/src/UploadModal.vue
Normal file
342
src/components/Upload/src/UploadModal.vue
Normal file
@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
width="800px"
|
||||
:title="t('component.upload.upload')"
|
||||
:okText="t('component.upload.save')"
|
||||
v-bind="$attrs"
|
||||
@register="register"
|
||||
@ok="handleOk"
|
||||
:closeFunc="handleCloseFunc"
|
||||
:maskClosable="false"
|
||||
:keyboard="false"
|
||||
class="upload-modal"
|
||||
:okButtonProps="getOkButtonProps"
|
||||
:cancelButtonProps="{ disabled: isUploadingRef }"
|
||||
>
|
||||
<template #centerFooter>
|
||||
<a-button
|
||||
@click="handleStartUpload"
|
||||
color="success"
|
||||
:disabled="!getIsSelectFile"
|
||||
:loading="isUploadingRef"
|
||||
>
|
||||
{{ getUploadBtnText }}
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<div class="upload-modal-toolbar">
|
||||
<Alert :message="getHelpText" type="info" banner class="upload-modal-toolbar__text" />
|
||||
|
||||
<Upload
|
||||
:accept="getStringAccept"
|
||||
:multiple="multiple"
|
||||
:before-upload="beforeUpload"
|
||||
:show-upload-list="false"
|
||||
class="upload-modal-toolbar__btn"
|
||||
>
|
||||
<a-button type="primary"> 选择文件 </a-button>
|
||||
</Upload>
|
||||
</div>
|
||||
<FileList :dataSource="fileListRef" :columns="columns" :actionColumn="actionColumn" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, ref, toRefs, unref, computed, PropType, watch } from 'vue';
|
||||
import { Upload, Alert } from 'ant-design-vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
// import { BasicTable, useTable } from '/@/components/Table';
|
||||
// hooks
|
||||
import { useUploadType } from './useUpload';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
// types
|
||||
import { FileItem, UploadResultStatus } from './typing';
|
||||
import { basicProps } from './props';
|
||||
import { createTableColumns, createActionColumn } from './data';
|
||||
// utils
|
||||
import { checkImgType, getBase64WithFile } from './helper';
|
||||
import { buildSnowFlakeId, buildUUID } from '/@/utils/uuid';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
import { warn } from '/@/utils/log';
|
||||
import FileList from './FileList.vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: { BasicModal, Upload, Alert, FileList },
|
||||
props: {
|
||||
...basicProps,
|
||||
previewFileList: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
folderId: {
|
||||
type: String as PropType<string>,
|
||||
default: () => '',
|
||||
},
|
||||
},
|
||||
emits: ['change', 'register', 'delete'],
|
||||
setup(props, { emit }) {
|
||||
const state = reactive<{ fileList: FileItem[] }>({
|
||||
fileList: [],
|
||||
});
|
||||
|
||||
// 是否正在上传
|
||||
const isUploadingRef = ref(false);
|
||||
const fileListRef = ref<FileItem[]>([]);
|
||||
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
||||
|
||||
const folderIdRef = ref<string>(props.folderId);
|
||||
|
||||
watch(
|
||||
() => props.folderId,
|
||||
() => {
|
||||
folderIdRef.value = props.folderId;
|
||||
},
|
||||
);
|
||||
|
||||
const { t } = useI18n();
|
||||
const [register, { closeModal }] = useModalInner();
|
||||
|
||||
const { getStringAccept, getHelpText } = useUploadType({
|
||||
acceptRef: accept,
|
||||
helpTextRef: helpText,
|
||||
maxNumberRef: maxNumber,
|
||||
maxSizeRef: maxSize,
|
||||
});
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
const getIsSelectFile = computed(() => {
|
||||
return (
|
||||
fileListRef.value.length > 0 &&
|
||||
!fileListRef.value.every((item) => item.status === UploadResultStatus.SUCCESS)
|
||||
);
|
||||
});
|
||||
|
||||
const getOkButtonProps = computed(() => {
|
||||
const someSuccess = fileListRef.value.some(
|
||||
(item) => item.status === UploadResultStatus.SUCCESS,
|
||||
);
|
||||
return {
|
||||
disabled: isUploadingRef.value || fileListRef.value.length === 0 || !someSuccess,
|
||||
};
|
||||
});
|
||||
|
||||
const getUploadBtnText = computed(() => {
|
||||
const someError = fileListRef.value.some(
|
||||
(item) => item.status === UploadResultStatus.ERROR,
|
||||
);
|
||||
return isUploadingRef.value
|
||||
? t('component.upload.uploading')
|
||||
: someError
|
||||
? t('component.upload.reUploadFailed')
|
||||
: t('component.upload.startUpload');
|
||||
});
|
||||
|
||||
// 上传前校验
|
||||
function beforeUpload(file: File) {
|
||||
const { size, name } = file;
|
||||
const { maxSize } = props;
|
||||
// 设置最大值,则判断
|
||||
if (maxSize && file.size / 1024 / 1024 >= maxSize) {
|
||||
createMessage.error(t('component.upload.maxSizeMultiple', [maxSize]));
|
||||
return false;
|
||||
}
|
||||
|
||||
const commonItem = {
|
||||
uuid: buildUUID(),
|
||||
file,
|
||||
size,
|
||||
name,
|
||||
percent: 0,
|
||||
type: name.split('.').pop(),
|
||||
};
|
||||
// 生成图片缩略图
|
||||
if (checkImgType(file)) {
|
||||
// beforeUpload,如果异步会调用自带上传方法
|
||||
// file.thumbUrl = await getBase64(file);
|
||||
getBase64WithFile(file).then(({ result: thumbUrl }) => {
|
||||
fileListRef.value = [
|
||||
...unref(fileListRef),
|
||||
{
|
||||
thumbUrl,
|
||||
...commonItem,
|
||||
},
|
||||
];
|
||||
});
|
||||
} else {
|
||||
fileListRef.value = [...unref(fileListRef), commonItem];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 删除
|
||||
function handleRemove(record: FileItem) {
|
||||
const index = fileListRef.value.findIndex((item) => item.uuid === record.uuid);
|
||||
index !== -1 && fileListRef.value.splice(index, 1);
|
||||
emit('delete', record);
|
||||
}
|
||||
|
||||
// 预览
|
||||
// function handlePreview(record: FileItem) {
|
||||
// const { thumbUrl = '' } = record;
|
||||
// createImgPreview({
|
||||
// imageList: [thumbUrl],
|
||||
// });
|
||||
// }
|
||||
|
||||
async function uploadApiByItem(item: FileItem, folderId: string) {
|
||||
const { api } = props;
|
||||
if (!api || !isFunction(api)) {
|
||||
return warn('upload api must exist and be a function');
|
||||
}
|
||||
try {
|
||||
item.status = UploadResultStatus.UPLOADING;
|
||||
const { data } = await props.api?.(
|
||||
{
|
||||
data: {
|
||||
...(props.uploadParams || {}),
|
||||
folderId,
|
||||
},
|
||||
file: item.file,
|
||||
name: props.name,
|
||||
filename: props.filename,
|
||||
},
|
||||
function onUploadProgress(progressEvent: ProgressEvent) {
|
||||
const complete = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
|
||||
item.percent = complete;
|
||||
},
|
||||
);
|
||||
item.status = UploadResultStatus.SUCCESS;
|
||||
item.responseData = data;
|
||||
return {
|
||||
success: true,
|
||||
error: null,
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
item.status = UploadResultStatus.ERROR;
|
||||
return {
|
||||
success: false,
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 点击开始上传
|
||||
async function handleStartUpload() {
|
||||
const { maxNumber } = props;
|
||||
if ((fileListRef.value.length + props.previewFileList?.length ?? 0) > maxNumber) {
|
||||
return createMessage.warning(t('component.upload.maxNumber', [maxNumber]));
|
||||
}
|
||||
try {
|
||||
isUploadingRef.value = true;
|
||||
// 只上传不是成功状态的
|
||||
const uploadFileList =
|
||||
fileListRef.value.filter((item) => item.status !== UploadResultStatus.SUCCESS) || [];
|
||||
|
||||
//判断表单是否传入folderId 如果没有 默认生成雪花id 用于后端文件存储
|
||||
if (!unref(folderIdRef)) {
|
||||
folderIdRef.value = buildSnowFlakeId();
|
||||
}
|
||||
|
||||
const data = await Promise.all(
|
||||
uploadFileList.map((item) => {
|
||||
return uploadApiByItem(item, folderIdRef.value);
|
||||
}),
|
||||
);
|
||||
isUploadingRef.value = false;
|
||||
// 生产环境:抛出错误
|
||||
const errorList = data.filter((item: any) => !item.success);
|
||||
if (errorList.length > 0) throw errorList;
|
||||
} catch (e) {
|
||||
isUploadingRef.value = false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// 点击保存
|
||||
function handleOk() {
|
||||
const { maxNumber } = props;
|
||||
|
||||
if (fileListRef.value.length > maxNumber) {
|
||||
return createMessage.warning(t('component.upload.maxNumber', [maxNumber]));
|
||||
}
|
||||
if (isUploadingRef.value) {
|
||||
return createMessage.warning(t('component.upload.saveWarn'));
|
||||
}
|
||||
const fileList: string[] = []; //文件地址
|
||||
const fileNameList: string[] = []; //文件名
|
||||
|
||||
for (const item of fileListRef.value) {
|
||||
const { status, responseData, name } = item;
|
||||
if (status === UploadResultStatus.SUCCESS && responseData) {
|
||||
fileList.push(responseData.data);
|
||||
fileNameList.push(name);
|
||||
}
|
||||
}
|
||||
// 存在一个上传成功的即可保存
|
||||
if (fileList.length <= 0) {
|
||||
return createMessage.warning(t('component.upload.saveError'));
|
||||
}
|
||||
fileListRef.value = [];
|
||||
closeModal();
|
||||
emit('change', fileList, folderIdRef.value, fileNameList);
|
||||
}
|
||||
|
||||
// 点击关闭:则所有操作不保存,包括上传的
|
||||
async function handleCloseFunc() {
|
||||
if (!isUploadingRef.value) {
|
||||
fileListRef.value = [];
|
||||
return true;
|
||||
} else {
|
||||
createMessage.warning(t('component.upload.uploadWait'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
columns: createTableColumns() as any[],
|
||||
actionColumn: createActionColumn(handleRemove) as any,
|
||||
register,
|
||||
closeModal,
|
||||
getHelpText,
|
||||
getStringAccept,
|
||||
getOkButtonProps,
|
||||
beforeUpload,
|
||||
// registerTable,
|
||||
fileListRef,
|
||||
state,
|
||||
isUploadingRef,
|
||||
handleStartUpload,
|
||||
handleOk,
|
||||
handleCloseFunc,
|
||||
getIsSelectFile,
|
||||
getUploadBtnText,
|
||||
t,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.upload-modal {
|
||||
.ant-upload-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-spin-nested-loading {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&__btn {
|
||||
margin-left: 8px;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
99
src/components/Upload/src/UploadPreviewModal.vue
Normal file
99
src/components/Upload/src/UploadPreviewModal.vue
Normal file
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
width="800px"
|
||||
:title="t('component.upload.preview')"
|
||||
class="upload-preview-modal"
|
||||
v-bind="$attrs"
|
||||
@register="register"
|
||||
:showOkBtn="false"
|
||||
>
|
||||
<FileList :dataSource="fileListRef" :columns="columns" :actionColumn="actionColumn" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch, ref } from 'vue';
|
||||
// import { BasicTable, useTable } from '/@/components/Table';
|
||||
import FileList from './FileList.vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { previewProps } from './props';
|
||||
import { PreviewFileItem } from './typing';
|
||||
import { downloadByUrl } from '/@/utils/file/download';
|
||||
import { createPreviewColumns, createPreviewActionColumn } from './data';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { isArray } from '/@/utils/is';
|
||||
|
||||
export default defineComponent({
|
||||
components: { BasicModal, FileList },
|
||||
props: previewProps,
|
||||
emits: ['list-change', 'register', 'delete'],
|
||||
setup(props, { emit }) {
|
||||
const [register, { closeModal }] = useModalInner();
|
||||
const { t } = useI18n();
|
||||
|
||||
const fileListRef = ref<PreviewFileItem[]>([]);
|
||||
watch(
|
||||
() => props.value,
|
||||
(value) => {
|
||||
if (!isArray(value)) value = [];
|
||||
fileListRef.value = value
|
||||
.filter((item) => !!item)
|
||||
.map((item, index) => {
|
||||
return {
|
||||
url: item,
|
||||
type: item.split('.').pop() || '',
|
||||
name: props.fileNames[index],
|
||||
};
|
||||
});
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 删除
|
||||
function handleRemove(record: PreviewFileItem) {
|
||||
const index = fileListRef.value.findIndex((item) => item.url === record.url);
|
||||
if (index !== -1) {
|
||||
const removed = fileListRef.value.splice(index, 1);
|
||||
emit('delete', removed[0].url);
|
||||
emit(
|
||||
'list-change',
|
||||
fileListRef.value.map((item) => item.url),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// // 预览
|
||||
// function handlePreview(record: PreviewFileItem) {
|
||||
// const { url = '' } = record;
|
||||
// createImgPreview({
|
||||
// imageList: [url],
|
||||
// });
|
||||
// }
|
||||
|
||||
// 下载
|
||||
function handleDownload(record: PreviewFileItem) {
|
||||
const { url = '' } = record;
|
||||
downloadByUrl({ url });
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
register,
|
||||
closeModal,
|
||||
fileListRef,
|
||||
columns: createPreviewColumns() as any[],
|
||||
actionColumn: createPreviewActionColumn({ handleRemove, handleDownload }) as any,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.upload-preview-modal {
|
||||
.ant-upload-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-spin-nested-loading {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
153
src/components/Upload/src/data.tsx
Normal file
153
src/components/Upload/src/data.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import type { BasicColumn, ActionItem } from '/@/components/Table';
|
||||
import { FileItem, PreviewFileItem, UploadResultStatus } from './typing';
|
||||
import {
|
||||
// checkImgType,
|
||||
isImgTypeByName,
|
||||
} from './helper';
|
||||
import { Progress, Tag } from 'ant-design-vue';
|
||||
import TableAction from '/@/components/Table/src/components/TableAction.vue';
|
||||
import ThumbUrl from './ThumbUrl.vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// 文件上传列表
|
||||
export function createTableColumns(): BasicColumn[] {
|
||||
return [
|
||||
{
|
||||
dataIndex: 'thumbUrl',
|
||||
title: t('略缩图'),
|
||||
width: 100,
|
||||
customRender: ({ record }) => {
|
||||
const { thumbUrl } = (record as FileItem) || {};
|
||||
return thumbUrl && <ThumbUrl fileUrl={thumbUrl} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: t('文件名'),
|
||||
align: 'left',
|
||||
customRender: ({ text, record }) => {
|
||||
const { percent, status: uploadStatus } = (record as FileItem) || {};
|
||||
let status: 'normal' | 'exception' | 'active' | 'success' = 'normal';
|
||||
if (uploadStatus === UploadResultStatus.ERROR) {
|
||||
status = 'exception';
|
||||
} else if (uploadStatus === UploadResultStatus.UPLOADING) {
|
||||
status = 'active';
|
||||
} else if (uploadStatus === UploadResultStatus.SUCCESS) {
|
||||
status = 'success';
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
<p class="truncate mb-1" title={text}>
|
||||
{text}
|
||||
</p>
|
||||
<Progress percent={percent} size="small" status={status} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'size',
|
||||
title: t('文件大小'),
|
||||
width: 100,
|
||||
customRender: ({ text = 0 }) => {
|
||||
return text && (text / 1024).toFixed(2) + 'KB';
|
||||
},
|
||||
},
|
||||
// {
|
||||
// dataIndex: 'type',
|
||||
// title: '文件类型',
|
||||
// width: 100,
|
||||
// },
|
||||
{
|
||||
dataIndex: 'status',
|
||||
title: t('状态'),
|
||||
width: 100,
|
||||
customRender: ({ text }) => {
|
||||
if (text === UploadResultStatus.SUCCESS) {
|
||||
return <Tag color="green">{() => t('上传成功')}</Tag>;
|
||||
} else if (text === UploadResultStatus.ERROR) {
|
||||
return <Tag color="red">{() => t('上传失败')}</Tag>;
|
||||
} else if (text === UploadResultStatus.UPLOADING) {
|
||||
return <Tag color="blue">{() => t('上传中')}</Tag>;
|
||||
}
|
||||
|
||||
return text;
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
export function createActionColumn(handleRemove: Function): BasicColumn {
|
||||
return {
|
||||
width: 120,
|
||||
title: t('操作'),
|
||||
dataIndex: 'action',
|
||||
fixed: false,
|
||||
customRender: ({ record }) => {
|
||||
const actions: ActionItem[] = [
|
||||
{
|
||||
label: t('删除'),
|
||||
color: 'error',
|
||||
onClick: handleRemove.bind(null, record),
|
||||
},
|
||||
];
|
||||
// if (checkImgType(record)) {
|
||||
// actions.unshift({
|
||||
// label: t('component.upload.preview'),
|
||||
// onClick: handlePreview.bind(null, record),
|
||||
// });
|
||||
// }
|
||||
return <TableAction actions={actions} outside={true} />;
|
||||
},
|
||||
};
|
||||
}
|
||||
// 文件预览列表
|
||||
export function createPreviewColumns(): BasicColumn[] {
|
||||
return [
|
||||
{
|
||||
dataIndex: 'url',
|
||||
title: t('略缩图'),
|
||||
width: 100,
|
||||
customRender: ({ record }) => {
|
||||
const { url } = (record as PreviewFileItem) || {};
|
||||
return isImgTypeByName(url) && <ThumbUrl fileUrl={url} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: t('文件名'),
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function createPreviewActionColumn({
|
||||
handleRemove,
|
||||
handleDownload,
|
||||
}: {
|
||||
handleRemove: Fn;
|
||||
handleDownload: Fn;
|
||||
}): BasicColumn {
|
||||
return {
|
||||
width: 160,
|
||||
title: t('操作'),
|
||||
dataIndex: 'action',
|
||||
fixed: false,
|
||||
customRender: ({ record }) => {
|
||||
const actions: ActionItem[] = [
|
||||
{
|
||||
label: t('删除'),
|
||||
color: 'error',
|
||||
onClick: handleRemove.bind(null, record),
|
||||
},
|
||||
{
|
||||
label: t('下载'),
|
||||
onClick: handleDownload.bind(null, record),
|
||||
},
|
||||
];
|
||||
|
||||
return <TableAction actions={actions} outside={true} />;
|
||||
},
|
||||
};
|
||||
}
|
||||
31
src/components/Upload/src/helper.ts
Normal file
31
src/components/Upload/src/helper.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { testRegLength } from '/@/utils/event/design';
|
||||
|
||||
export function checkFileType(file: File, accepts: string[]) {
|
||||
const newTypes = accepts.join('|');
|
||||
testRegLength(newTypes);
|
||||
|
||||
// const reg = /\.(jpg|jpeg|png|gif|txt|doc|docx|xls|xlsx|xml)$/i;
|
||||
const reg = new RegExp('\\.(' + newTypes + ')$', 'i');
|
||||
|
||||
return reg.test(file.name);
|
||||
}
|
||||
|
||||
export function checkImgType(file: File) {
|
||||
return isImgTypeByName(file.name);
|
||||
}
|
||||
|
||||
export function isImgTypeByName(name: string) {
|
||||
return /\.(jpg|jpeg|png|gif)$/i.test(name);
|
||||
}
|
||||
|
||||
export function getBase64WithFile(file: File) {
|
||||
return new Promise<{
|
||||
result: string;
|
||||
file: File;
|
||||
}>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve({ result: reader.result as string, file });
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
}
|
||||
87
src/components/Upload/src/props.ts
Normal file
87
src/components/Upload/src/props.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import type { PropType } from 'vue';
|
||||
import { FileBasicColumn } from './typing';
|
||||
|
||||
export const basicProps = {
|
||||
helpText: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
// 文件最大多少MB
|
||||
maxSize: {
|
||||
type: Number as PropType<number>,
|
||||
default: 2,
|
||||
},
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber: {
|
||||
type: Number as PropType<number>,
|
||||
default: Infinity,
|
||||
},
|
||||
// 根据后缀,或者其他
|
||||
accept: {
|
||||
type: [Array as PropType<string[]>, String as PropType<string>],
|
||||
default: () => [] || '',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
uploadParams: {
|
||||
type: Object as PropType<any>,
|
||||
default: {},
|
||||
},
|
||||
api: {
|
||||
type: Function as PropType<PromiseFn>,
|
||||
default: null,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String as PropType<string>,
|
||||
default: 'file',
|
||||
},
|
||||
filename: {
|
||||
type: String as PropType<string>,
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const uploadContainerProps = {
|
||||
value: {
|
||||
type: String as PropType<string>,
|
||||
default: () => '',
|
||||
},
|
||||
...basicProps,
|
||||
showPreviewNumber: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
emptyHidePreview: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const previewProps = {
|
||||
value: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
fileNames: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
};
|
||||
|
||||
export const fileListProps = {
|
||||
columns: {
|
||||
type: [Array] as PropType<FileBasicColumn[]>,
|
||||
default: null,
|
||||
},
|
||||
actionColumn: {
|
||||
type: Object as PropType<FileBasicColumn>,
|
||||
default: null,
|
||||
},
|
||||
dataSource: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
55
src/components/Upload/src/typing.ts
Normal file
55
src/components/Upload/src/typing.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { UploadApiResult } from '/@/api/sys/model/uploadModel';
|
||||
|
||||
export enum UploadResultStatus {
|
||||
SUCCESS = 'success',
|
||||
ERROR = 'error',
|
||||
UPLOADING = 'uploading',
|
||||
}
|
||||
|
||||
export interface FileItem {
|
||||
thumbUrl?: string;
|
||||
name: string;
|
||||
size: string | number;
|
||||
type?: string;
|
||||
percent: number;
|
||||
file: File;
|
||||
status?: UploadResultStatus;
|
||||
responseData?: UploadApiResult;
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export interface PreviewFileItem {
|
||||
url: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface FileBasicColumn {
|
||||
/**
|
||||
* Renderer of the table cell. The return value should be a VNode, or an object for colSpan/rowSpan config
|
||||
* @type Function | ScopedSlot
|
||||
*/
|
||||
customRender?: Function;
|
||||
/**
|
||||
* Title of this column
|
||||
* @type any (string | slot)
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Width of this column
|
||||
* @type string | number
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* Display field of the data record, could be set like a.b.c
|
||||
* @type string
|
||||
*/
|
||||
dataIndex: string;
|
||||
/**
|
||||
* specify how content is aligned
|
||||
* @default 'left'
|
||||
* @type string
|
||||
*/
|
||||
align?: 'left' | 'right' | 'center';
|
||||
}
|
||||
62
src/components/Upload/src/useUpload.ts
Normal file
62
src/components/Upload/src/useUpload.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Ref, unref, computed } from 'vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
const { t } = useI18n();
|
||||
export function useUploadType({
|
||||
acceptRef,
|
||||
helpTextRef,
|
||||
maxNumberRef,
|
||||
maxSizeRef,
|
||||
}: {
|
||||
acceptRef: Ref<string[] | string>;
|
||||
helpTextRef: Ref<string>;
|
||||
maxNumberRef: Ref<number>;
|
||||
maxSizeRef: Ref<number>;
|
||||
}) {
|
||||
// 文件类型限制
|
||||
const getAccept = computed(() => {
|
||||
const accept = unref(acceptRef);
|
||||
if (accept && accept.length > 0) {
|
||||
return Array.isArray(accept) ? accept : accept.split(',');
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const getStringAccept = computed(() => {
|
||||
return unref(getAccept)
|
||||
.map((item) => {
|
||||
if (item.indexOf('/') > 0 || item.startsWith('.')) {
|
||||
return item;
|
||||
} else {
|
||||
return `.${item}`;
|
||||
}
|
||||
})
|
||||
.join(',');
|
||||
});
|
||||
|
||||
// 支持jpg、jpeg、png格式,不超过2M,最多可选择10张图片,。
|
||||
const getHelpText = computed(() => {
|
||||
const helpText = unref(helpTextRef);
|
||||
if (helpText) {
|
||||
return helpText;
|
||||
}
|
||||
const helpTexts: string[] = [];
|
||||
|
||||
const accept = unref(acceptRef);
|
||||
if (accept.length > 0) {
|
||||
helpTexts.push(
|
||||
t('component.upload.accept', Array.isArray(accept) ? [accept.join(',')] : [accept]),
|
||||
);
|
||||
}
|
||||
|
||||
const maxSize = unref(maxSizeRef);
|
||||
if (maxSize) {
|
||||
helpTexts.push(t('component.upload.maxSize', [maxSize]));
|
||||
}
|
||||
|
||||
const maxNumber = unref(maxNumberRef);
|
||||
if (maxNumber && maxNumber !== Infinity) {
|
||||
helpTexts.push(t('component.upload.maxNumber', [maxNumber]));
|
||||
}
|
||||
return helpTexts.join(',');
|
||||
});
|
||||
return { getAccept, getStringAccept, getHelpText };
|
||||
}
|
||||
Reference in New Issue
Block a user