mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-27 15:41:32 +08:00
perf(upload): improve upload component
This commit is contained in:
@@ -24,7 +24,7 @@ export const footerProps = {
|
||||
okButtonProps: Object as PropType<any>,
|
||||
okText: {
|
||||
type: String as PropType<string>,
|
||||
default: '保存',
|
||||
default: '确认',
|
||||
},
|
||||
okType: {
|
||||
type: String as PropType<string>,
|
||||
|
@@ -44,7 +44,6 @@
|
||||
import { useFormValues } from './hooks/useFormValues';
|
||||
import useAdvanced from './hooks/useAdvanced';
|
||||
import { useFormAction } from './hooks/useFormAction';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicForm',
|
||||
components: { FormItem, Form, Row, FormAction },
|
||||
|
@@ -18,7 +18,7 @@ export default defineComponent({
|
||||
// icon size
|
||||
size: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
default: 14,
|
||||
default: 16,
|
||||
},
|
||||
prefix: {
|
||||
type: String as PropType<string>,
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import type { PropType } from 'vue';
|
||||
import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
|
||||
export const modalProps = {
|
||||
visible: Boolean as PropType<boolean>,
|
||||
// open drag
|
||||
@@ -16,7 +17,7 @@ export const modalProps = {
|
||||
},
|
||||
okText: {
|
||||
type: String as PropType<string>,
|
||||
default: '保存',
|
||||
default: '确认',
|
||||
},
|
||||
closeFunc: Function as PropType<() => Promise<boolean>>,
|
||||
};
|
||||
@@ -100,9 +101,9 @@ export const basicProps = Object.assign({}, modalProps, {
|
||||
default: 'primary',
|
||||
},
|
||||
|
||||
okButtonProps: Object as PropType<any>,
|
||||
okButtonProps: Object as PropType<ButtonProps>,
|
||||
|
||||
cancelButtonProps: Object as PropType<any>,
|
||||
cancelButtonProps: Object as PropType<ButtonProps>,
|
||||
|
||||
title: {
|
||||
type: String as PropType<string>,
|
||||
|
@@ -7,7 +7,7 @@
|
||||
import { defineComponent, watchEffect, PropType, ref, unref } from 'vue';
|
||||
import { toCanvas, QRCodeRenderersOptions, LogoType } from './qrcodePlus';
|
||||
import { toDataURL } from 'qrcode';
|
||||
import { downloadByUrl } from '/@/utils/file/FileDownload';
|
||||
import { downloadByUrl } from '/@/utils/file/download';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QrCode',
|
||||
|
@@ -4,6 +4,7 @@
|
||||
class="basic-table"
|
||||
:class="{
|
||||
'table-form-container': getBindValues.useSearchForm,
|
||||
inset: getBindValues.inset,
|
||||
}"
|
||||
>
|
||||
<BasicForm
|
||||
|
@@ -84,7 +84,7 @@ export function useDataSource(
|
||||
const { api, searchInfo, fetchSetting, beforeFetch, afterFetch, useSearchForm } = unref(
|
||||
propsRef
|
||||
);
|
||||
if (!api && !isFunction(api)) return;
|
||||
if (!api || !isFunction(api)) return;
|
||||
try {
|
||||
loadingRef.value = true;
|
||||
const { pageField, sizeField, listField, totalField } = fetchSetting || FETCH_SETTING;
|
||||
|
@@ -16,7 +16,10 @@ export const basicProps = {
|
||||
tableSetting: {
|
||||
type: Object as PropType<TableSetting>,
|
||||
},
|
||||
|
||||
inset: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
sortFn: {
|
||||
type: Function as PropType<(sortInfo: SorterResult) => any>,
|
||||
default: DEFAULT_SORT_FN,
|
||||
|
@@ -49,6 +49,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.inset {
|
||||
.ant-table-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
.ant-table {
|
||||
border: none;
|
||||
|
@@ -126,6 +126,8 @@ export interface TableSetting {
|
||||
export interface BasicTableProps<T = any> {
|
||||
// 自定义排序方法
|
||||
sortFn?: (sortInfo: SorterResult) => any;
|
||||
// 取消表格的默认padding
|
||||
inset?: boolean;
|
||||
// 显示表格设置
|
||||
showTableSetting?: boolean;
|
||||
tableSetting?: TableSetting;
|
||||
|
@@ -1,2 +1,2 @@
|
||||
export { default as UploadContainer } from './src/UploadContainer.vue';
|
||||
export { default as BasicUpload } from './src/BasicUpload.vue';
|
||||
// export * from './src/types';
|
||||
|
99
src/components/Upload/src/BasicUpload.vue
Normal file
99
src/components/Upload/src/BasicUpload.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-button-group>
|
||||
<a-button type="primary" @click="openUploadModal" preIcon="ant-design:cloud-upload-outlined">
|
||||
上传
|
||||
</a-button>
|
||||
<Tooltip placement="bottom" v-if="showPreview">
|
||||
<template #title>
|
||||
已上传
|
||||
<template v-if="fileListRef.length">{{ fileListRef.length }}</template>
|
||||
</template>
|
||||
<a-button @click="openPreviewModal">
|
||||
<Icon icon="ant-design:eye-outlined" />
|
||||
<template v-if="fileListRef.length && showPreviewNumber">
|
||||
{{ fileListRef.length }}
|
||||
</template>
|
||||
</a-button>
|
||||
</Tooltip>
|
||||
</a-button-group>
|
||||
|
||||
<UploadModal v-bind="bindValue" @register="registerUploadModal" @change="handleChange" />
|
||||
|
||||
<UploadPreviewModal
|
||||
:value="fileListRef"
|
||||
@register="registerPreviewModal"
|
||||
@list-change="handlePreviewChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, unref, computed } from 'vue';
|
||||
|
||||
import UploadModal from './UploadModal.vue';
|
||||
import UploadPreviewModal from './UploadPreviewModal.vue';
|
||||
import Icon from '/@/components/Icon';
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { useModal } from '/@/components/Modal';
|
||||
|
||||
import { uploadContainerProps } from './props';
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
export default defineComponent({
|
||||
components: { UploadModal, UploadPreviewModal, Icon, Tooltip },
|
||||
props: uploadContainerProps,
|
||||
setup(props, { emit, attrs }) {
|
||||
// 上传modal
|
||||
const [registerUploadModal, { openModal: openUploadModal }] = useModal();
|
||||
|
||||
// 预览modal
|
||||
const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
|
||||
|
||||
const fileListRef = ref<string[]>([]);
|
||||
|
||||
const showPreview = computed(() => {
|
||||
const { emptyHidePreview } = props;
|
||||
if (!emptyHidePreview) return true;
|
||||
return emptyHidePreview ? fileListRef.value.length > 0 : true;
|
||||
});
|
||||
|
||||
const bindValue = computed(() => {
|
||||
const value = { ...attrs, ...props };
|
||||
return omit(value, 'onChange');
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(value = []) => {
|
||||
fileListRef.value = value;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 上传modal保存操作
|
||||
function handleChange(urls: string[]) {
|
||||
fileListRef.value = [...unref(fileListRef), ...(urls || [])];
|
||||
emit('change', fileListRef.value);
|
||||
}
|
||||
|
||||
// 预览modal保存操作
|
||||
function handlePreviewChange(urls: string[]) {
|
||||
fileListRef.value = [...(urls || [])];
|
||||
emit('change', fileListRef.value);
|
||||
}
|
||||
|
||||
return {
|
||||
registerUploadModal,
|
||||
openUploadModal,
|
||||
handleChange,
|
||||
handlePreviewChange,
|
||||
registerPreviewModal,
|
||||
openPreviewModal,
|
||||
fileListRef,
|
||||
showPreview,
|
||||
bindValue,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
@@ -5,25 +5,22 @@
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
fileUrl: {
|
||||
type: String,
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
fileType: {
|
||||
type: String,
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-button-group>
|
||||
<a-button type="primary" @click="openUploadModal">上传</a-button>
|
||||
<a-button @click="openPreviewModal">
|
||||
<Icon icon="ant-design:eye-outlined" />
|
||||
</a-button>
|
||||
</a-button-group>
|
||||
<UploadModal v-bind="$props" @register="registerUploadModal" @change="handleChange" />
|
||||
<UploadPreviewModal
|
||||
:value="fileListRef"
|
||||
@register="registerPreviewModal"
|
||||
@change="handlePreviewChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, unref } from 'vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import UploadModal from './UploadModal.vue';
|
||||
import { uploadContainerProps } from './props';
|
||||
import UploadPreviewModal from './UploadPreviewModal.vue';
|
||||
import Icon from '/@/components/Icon/index';
|
||||
export default defineComponent({
|
||||
components: { UploadModal, UploadPreviewModal, Icon },
|
||||
props: uploadContainerProps,
|
||||
setup(props, { emit }) {
|
||||
// 上传modal
|
||||
const [registerUploadModal, { openModal: openUploadModal }] = useModal();
|
||||
// 预览modal
|
||||
const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
|
||||
|
||||
const fileListRef = ref<string[]>([]);
|
||||
watch(
|
||||
() => props.value,
|
||||
(value) => {
|
||||
fileListRef.value = [...(value || [])];
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
// 上传modal保存操作
|
||||
function handleChange(urls: string[]) {
|
||||
fileListRef.value = [...unref(fileListRef), ...(urls || [])];
|
||||
emit('change', fileListRef.value);
|
||||
}
|
||||
// 预览modal保存操作
|
||||
function handlePreviewChange(urls: string[]) {
|
||||
fileListRef.value = [...(urls || [])];
|
||||
emit('change', fileListRef.value);
|
||||
}
|
||||
return {
|
||||
registerUploadModal,
|
||||
openUploadModal,
|
||||
handleChange,
|
||||
handlePreviewChange,
|
||||
registerPreviewModal,
|
||||
openPreviewModal,
|
||||
fileListRef,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
@@ -1,31 +1,44 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
width="800px"
|
||||
title="上传"
|
||||
okText="保存"
|
||||
v-bind="$attrs"
|
||||
@register="register"
|
||||
@ok="handleOk"
|
||||
:closeFunc="handleCloseFunc"
|
||||
:maskClosable="false"
|
||||
width="800px"
|
||||
title="上传组件"
|
||||
:keyboard="false"
|
||||
wrapClassName="upload-modal"
|
||||
:okButtonProps="{ disabled: isUploadingRef }"
|
||||
:okButtonProps="getOkButtonProps"
|
||||
:cancelButtonProps="{ disabled: isUploadingRef }"
|
||||
>
|
||||
<template #centerdFooter>
|
||||
<a-button @click="handleStartUpload" color="success" :loading="isUploadingRef">
|
||||
{{ isUploadingRef ? '上传中' : '开始上传' }}
|
||||
<a-button
|
||||
@click="handleStartUpload"
|
||||
color="success"
|
||||
:disabled="!getIsSelectFile"
|
||||
:loading="isUploadingRef"
|
||||
>
|
||||
{{ getUploadBtnText }}
|
||||
</a-button>
|
||||
</template>
|
||||
<Upload :accept="getStringAccept" :multiple="multiple" :before-upload="beforeUpload">
|
||||
<a-button type="primary"> 选择文件 </a-button>
|
||||
<span class="px-2">{{ getHelpText }}</span>
|
||||
</Upload>
|
||||
<BasicTable @register="registerTable" :dataSource="fileListRef" />
|
||||
|
||||
<BasicTable @register="registerTable" :dataSource="fileListRef">
|
||||
<template #toolbar>
|
||||
<Upload :accept="getStringAccept" :multiple="multiple" :before-upload="beforeUpload">
|
||||
<a-button type="primary"> 选择文件 </a-button>
|
||||
</Upload>
|
||||
</template>
|
||||
<template #tableTitle>
|
||||
<Alert :message="getHelpText" type="info" banner></Alert>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, ref, toRef, unref } from 'vue';
|
||||
import { Upload } from 'ant-design-vue';
|
||||
import { defineComponent, reactive, ref, toRefs, unref, computed } from 'vue';
|
||||
import { Upload, Alert } from 'ant-design-vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicTable, useTable } from '/@/components/Table';
|
||||
// hooks
|
||||
@@ -39,23 +52,56 @@
|
||||
import { checkFileType, checkImgType, getBase64WithFile } from './utils';
|
||||
import { buildUUID } from '/@/utils/uuid';
|
||||
import { createImgPreview } from '/@/components/Preview/index';
|
||||
import { uploadApi } from '/@/api/demo/upload';
|
||||
import { uploadApi } from '/@/api/sys/upload';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
import { warn } from '/@/utils/log';
|
||||
|
||||
export default defineComponent({
|
||||
components: { BasicModal, Upload, BasicTable },
|
||||
components: { BasicModal, Upload, BasicTable, Alert },
|
||||
props: basicProps,
|
||||
setup(props, { emit }) {
|
||||
const [register, { closeModal }] = useModalInner();
|
||||
const { getAccept, getStringAccept, getHelpText } = useUploadType({
|
||||
acceptRef: toRef(props, 'accept'),
|
||||
helpTextRef: toRef(props, 'helpText'),
|
||||
maxNumberRef: toRef(props, 'maxNumber'),
|
||||
maxSizeRef: toRef(props, 'maxSize'),
|
||||
// 是否正在上传
|
||||
const isUploadingRef = ref(false);
|
||||
const fileListRef = ref<FileItem[]>([]);
|
||||
const state = reactive<{ fileList: FileItem[] }>({
|
||||
fileList: [],
|
||||
});
|
||||
|
||||
const [register, { closeModal }] = useModalInner();
|
||||
|
||||
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
||||
const { getAccept, getStringAccept, getHelpText } = useUploadType({
|
||||
acceptRef: accept,
|
||||
helpTextRef: helpText,
|
||||
maxNumberRef: maxNumber,
|
||||
maxSizeRef: maxSize,
|
||||
});
|
||||
|
||||
const fileListRef = ref<FileItem[]>([]);
|
||||
const state = reactive<{ fileList: FileItem[] }>({ fileList: [] });
|
||||
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 ? '上传中' : someError ? '重新上传失败文件' : '开始上传';
|
||||
});
|
||||
|
||||
// 上传前校验
|
||||
function beforeUpload(file: File) {
|
||||
const { size, name } = file;
|
||||
@@ -73,6 +119,14 @@
|
||||
createMessage.error!(`只能上传${accept.join(',')}格式文件`);
|
||||
return false;
|
||||
}
|
||||
const commonItem = {
|
||||
uuid: buildUUID(),
|
||||
file,
|
||||
size,
|
||||
name,
|
||||
percent: 0,
|
||||
type: name.split('.').pop(),
|
||||
};
|
||||
// 生成图片缩略图
|
||||
if (checkImgType(file)) {
|
||||
// beforeUpload,如果异步会调用自带上传方法
|
||||
@@ -81,29 +135,13 @@
|
||||
fileListRef.value = [
|
||||
...unref(fileListRef),
|
||||
{
|
||||
uuid: buildUUID(),
|
||||
file,
|
||||
thumbUrl,
|
||||
size,
|
||||
name,
|
||||
percent: 0,
|
||||
type: name.split('.').pop(),
|
||||
...commonItem,
|
||||
},
|
||||
];
|
||||
});
|
||||
} else {
|
||||
fileListRef.value = [
|
||||
...unref(fileListRef),
|
||||
{
|
||||
uuid: buildUUID(),
|
||||
|
||||
file,
|
||||
size,
|
||||
name,
|
||||
percent: 0,
|
||||
type: name.split('.').pop(),
|
||||
},
|
||||
];
|
||||
fileListRef.value = [...unref(fileListRef), commonItem];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -112,6 +150,7 @@
|
||||
const index = fileListRef.value.findIndex((item) => item.uuid === record.uuid);
|
||||
index !== -1 && fileListRef.value.splice(index, 1);
|
||||
}
|
||||
|
||||
// 预览
|
||||
function handlePreview(record: FileItem) {
|
||||
const { thumbUrl = '' } = record;
|
||||
@@ -119,19 +158,18 @@
|
||||
imageList: [thumbUrl],
|
||||
});
|
||||
}
|
||||
const [registerTable] = useTable({
|
||||
columns: createTableColumns(),
|
||||
actionColumn: createActionColumn(handleRemove, handlePreview),
|
||||
pagination: false,
|
||||
});
|
||||
// 是否正在上传
|
||||
const isUploadingRef = ref(false);
|
||||
|
||||
async function uploadApiByItem(item: FileItem) {
|
||||
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 uploadApi(
|
||||
{
|
||||
...(props.uploadParams || {}),
|
||||
file: item.file,
|
||||
},
|
||||
function onUploadProgress(progressEvent: ProgressEvent) {
|
||||
@@ -154,32 +192,42 @@
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 点击开始上传
|
||||
async function handleStartUpload() {
|
||||
const { maxNumber } = props;
|
||||
if (fileListRef.value.length > maxNumber) {
|
||||
return createMessage.warning(`最多只能上传${maxNumber}个文件`);
|
||||
}
|
||||
try {
|
||||
isUploadingRef.value = true;
|
||||
// 只上传不是成功状态的
|
||||
const uploadFileList =
|
||||
fileListRef.value.filter((item) => item.status !== UploadResultStatus.SUCCESS) || [];
|
||||
const data = await Promise.all(
|
||||
unref(fileListRef).map((item) => {
|
||||
uploadFileList.map((item) => {
|
||||
return uploadApiByItem(item);
|
||||
})
|
||||
);
|
||||
isUploadingRef.value = false;
|
||||
// 生产环境:抛出错误
|
||||
const errorList = data.filter((item) => !item.success);
|
||||
if (errorList.length > 0) {
|
||||
throw errorList;
|
||||
}
|
||||
const errorList = data.filter((item: any) => !item.success);
|
||||
if (errorList.length > 0) throw errorList;
|
||||
} catch (e) {
|
||||
isUploadingRef.value = false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// 点击保存
|
||||
function handleOk() {
|
||||
// TODO: 没起作用:okButtonProps={{ disabled: state.isUploading }}
|
||||
const { maxNumber } = props;
|
||||
|
||||
if (fileListRef.value.length > maxNumber) {
|
||||
return createMessage.warning(`最多只能上传${maxNumber}个文件`);
|
||||
}
|
||||
if (isUploadingRef.value) {
|
||||
createMessage.warning('请等待文件上传后,保存');
|
||||
return;
|
||||
return createMessage.warning('请等待文件上传后,保存');
|
||||
}
|
||||
const fileList: string[] = [];
|
||||
|
||||
@@ -189,18 +237,15 @@
|
||||
fileList.push(responseData.url);
|
||||
}
|
||||
}
|
||||
|
||||
// 存在一个上传成功的即可保存
|
||||
|
||||
if (fileList.length <= 0) {
|
||||
createMessage.warning('没有上传成功的文件,无法保存');
|
||||
return;
|
||||
return createMessage.warning('没有上传成功的文件,无法保存');
|
||||
}
|
||||
console.log(fileList);
|
||||
emit('change', fileList);
|
||||
fileListRef.value = [];
|
||||
closeModal();
|
||||
emit('change', fileList);
|
||||
}
|
||||
|
||||
// 点击关闭:则所有操作不保存,包括上传的
|
||||
function handleCloseFunc() {
|
||||
if (!isUploadingRef.value) {
|
||||
@@ -211,11 +256,22 @@
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const [registerTable] = useTable({
|
||||
columns: createTableColumns(),
|
||||
actionColumn: createActionColumn(handleRemove, handlePreview),
|
||||
pagination: false,
|
||||
inset: true,
|
||||
scroll: {
|
||||
y: 3000,
|
||||
},
|
||||
});
|
||||
return {
|
||||
register,
|
||||
closeModal,
|
||||
getHelpText,
|
||||
getStringAccept,
|
||||
getOkButtonProps,
|
||||
beforeUpload,
|
||||
registerTable,
|
||||
fileListRef,
|
||||
@@ -224,14 +280,13 @@
|
||||
handleStartUpload,
|
||||
handleOk,
|
||||
handleCloseFunc,
|
||||
getIsSelectFile,
|
||||
getUploadBtnText,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
// /deep/ .ant-upload-list {
|
||||
// display: none;
|
||||
// }
|
||||
.upload-modal {
|
||||
.ant-upload-list {
|
||||
display: none;
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
width="800px"
|
||||
title="预览"
|
||||
wrapClassName="upload-preview-modal"
|
||||
v-bind="$attrs"
|
||||
width="800px"
|
||||
@register="register"
|
||||
title="预览"
|
||||
:showOkBtn="false"
|
||||
>
|
||||
<BasicTable @register="registerTable" :dataSource="fileListRef" />
|
||||
@@ -12,17 +12,18 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch, ref, unref } from 'vue';
|
||||
|
||||
import { BasicTable, useTable } from '/@/components/Table';
|
||||
import { createPreviewColumns, createPreviewActionColumn } from './data';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { priviewProps } from './props';
|
||||
import { previewProps } from './props';
|
||||
import { PreviewFileItem } from './types';
|
||||
import { createImgPreview } from '/@/components/Preview/index';
|
||||
import { downloadByUrl } from '/@/utils/file/FileDownload';
|
||||
import { downloadByUrl } from '/@/utils/file/download';
|
||||
|
||||
import { createPreviewColumns, createPreviewActionColumn } from './data';
|
||||
export default defineComponent({
|
||||
components: { BasicModal, BasicTable },
|
||||
props: priviewProps,
|
||||
props: previewProps,
|
||||
setup(props, { emit }) {
|
||||
const [register, { closeModal }] = useModalInner();
|
||||
const fileListRef = ref<PreviewFileItem[]>([]);
|
||||
@@ -43,17 +44,19 @@
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 删除
|
||||
function handleRemove(record: PreviewFileItem) {
|
||||
const index = fileListRef.value.findIndex((item) => item.url === record.url);
|
||||
if (index !== -1) {
|
||||
fileListRef.value.splice(index, 1);
|
||||
emit(
|
||||
'change',
|
||||
'list-change',
|
||||
fileListRef.value.map((item) => item.url)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 预览
|
||||
function handlePreview(record: PreviewFileItem) {
|
||||
const { url = '' } = record;
|
||||
@@ -61,16 +64,19 @@
|
||||
imageList: [url],
|
||||
});
|
||||
}
|
||||
|
||||
// 下载
|
||||
function handleDownload(record: PreviewFileItem) {
|
||||
const { url = '' } = record;
|
||||
downloadByUrl({ url });
|
||||
}
|
||||
|
||||
const [registerTable] = useTable({
|
||||
columns: createPreviewColumns(),
|
||||
pagination: false,
|
||||
actionColumn: createPreviewActionColumn({ handleRemove, handlePreview, handleDownload }),
|
||||
});
|
||||
|
||||
return {
|
||||
register,
|
||||
closeModal,
|
||||
|
@@ -1,10 +1,6 @@
|
||||
// import { BasicColumn, TableAction, ActionItem } from '@/components/table';
|
||||
import { checkImgType, isImgTypeByName } from './utils';
|
||||
// import ThumnUrl from './ThumbUrl.vue';
|
||||
import { Progress } from 'ant-design-vue';
|
||||
import { Progress, Tag } from 'ant-design-vue';
|
||||
import { FileItem, PreviewFileItem, UploadResultStatus } from './types';
|
||||
// import { ElecArchivesSaveResult } from '@/api/biz/file/model/fileModel';
|
||||
// import { quryFile } from '@/api/biz/file/file';
|
||||
import { BasicColumn, ActionItem, TableAction } from '/@/components/Table/index';
|
||||
|
||||
// 文件上传列表
|
||||
@@ -16,8 +12,7 @@ export function createTableColumns(): BasicColumn[] {
|
||||
width: 100,
|
||||
customRender: ({ record }) => {
|
||||
const { thumbUrl, type } = (record as FileItem) || {};
|
||||
return <span>{thumbUrl ? <img src={thumbUrl} style={{ width: '50px' }} /> : type}</span>;
|
||||
// return <ThumnUrl fileUrl={thumbUrl} fileType={type} fileName={type} />;
|
||||
return <span>{thumbUrl ? <img style={{ maxWidth: '60px' }} src={thumbUrl} /> : type}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -26,7 +21,7 @@ export function createTableColumns(): BasicColumn[] {
|
||||
align: 'left',
|
||||
customRender: ({ text, record }) => {
|
||||
const { percent, status: uploadStatus } = (record as FileItem) || {};
|
||||
let status = 'normal';
|
||||
let status: 'normal' | 'exception' | 'active' | 'success' = 'normal';
|
||||
if (uploadStatus === UploadResultStatus.ERROR) {
|
||||
status = 'exception';
|
||||
} else if (uploadStatus === UploadResultStatus.UPLOADING) {
|
||||
@@ -63,11 +58,11 @@ export function createTableColumns(): BasicColumn[] {
|
||||
width: 100,
|
||||
customRender: ({ text }) => {
|
||||
if (text === UploadResultStatus.SUCCESS) {
|
||||
return '上传成功';
|
||||
return <Tag color="green">{() => '上传成功'}</Tag>;
|
||||
} else if (text === UploadResultStatus.ERROR) {
|
||||
return '上传失败';
|
||||
return <Tag color="red">{() => '上传失败'}</Tag>;
|
||||
} else if (text === UploadResultStatus.UPLOADING) {
|
||||
return '上传中';
|
||||
return <Tag color="blue">{() => '上传中'}</Tag>;
|
||||
}
|
||||
|
||||
return text;
|
||||
@@ -85,6 +80,7 @@ export function createActionColumn(handleRemove: Function, handlePreview: Functi
|
||||
const actions: ActionItem[] = [
|
||||
{
|
||||
label: '删除',
|
||||
color: 'error',
|
||||
onClick: handleRemove.bind(null, record),
|
||||
},
|
||||
];
|
||||
@@ -125,9 +121,9 @@ export function createPreviewActionColumn({
|
||||
handlePreview,
|
||||
handleDownload,
|
||||
}: {
|
||||
handleRemove: Function;
|
||||
handlePreview: Function;
|
||||
handleDownload: Function;
|
||||
handleRemove: Fn;
|
||||
handlePreview: Fn;
|
||||
handleDownload: Fn;
|
||||
}): BasicColumn {
|
||||
return {
|
||||
width: 160,
|
||||
@@ -135,11 +131,12 @@ export function createPreviewActionColumn({
|
||||
dataIndex: 'action',
|
||||
fixed: false,
|
||||
customRender: ({ record }) => {
|
||||
const { url } = (record as PreviewFileItem) || {};
|
||||
const { url } = (record || {}) as PreviewFileItem;
|
||||
|
||||
const actions: ActionItem[] = [
|
||||
{
|
||||
label: '删除',
|
||||
color: 'error',
|
||||
onClick: handleRemove.bind(null, record),
|
||||
},
|
||||
{
|
||||
|
@@ -10,10 +10,10 @@ export const basicProps = {
|
||||
type: Number as PropType<number>,
|
||||
default: 2,
|
||||
},
|
||||
// 最大数量的文件,0不限制
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber: {
|
||||
type: Number as PropType<number>,
|
||||
default: 0,
|
||||
default: Infinity,
|
||||
},
|
||||
// 根据后缀,或者其他
|
||||
accept: {
|
||||
@@ -21,9 +21,18 @@ export const basicProps = {
|
||||
default: () => [],
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
uploadParams: {
|
||||
type: Object as PropType<any>,
|
||||
default: {},
|
||||
},
|
||||
api: {
|
||||
type: Function as PropType<PromiseFn>,
|
||||
default: null,
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const uploadContainerProps = {
|
||||
@@ -32,9 +41,17 @@ export const uploadContainerProps = {
|
||||
default: () => [],
|
||||
},
|
||||
...basicProps,
|
||||
showPreviewNumber: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
emptyHidePreview: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const priviewProps = {
|
||||
export const previewProps = {
|
||||
value: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { UploadApiResult } from '/@/api/demo/model/uploadModel';
|
||||
import { UploadApiResult } from '/@/api/sys/model/uploadModel';
|
||||
|
||||
export enum UploadResultStatus {
|
||||
SUCCESS = 'success',
|
||||
|
@@ -42,12 +42,12 @@ export function useUploadType({
|
||||
|
||||
const maxSize = unref(maxSizeRef);
|
||||
if (maxSize) {
|
||||
helpTexts.push(`不超过${maxSize}MB`);
|
||||
helpTexts.push(`单个文件不超过${maxSize}MB`);
|
||||
}
|
||||
|
||||
const maxNumber = unref(maxNumberRef);
|
||||
if (maxNumber) {
|
||||
helpTexts.push(`最多可选择${maxNumber}个文件`);
|
||||
if (maxNumber && maxNumber !== Infinity) {
|
||||
helpTexts.push(`最多只能上传${maxNumber}个文件`);
|
||||
}
|
||||
return helpTexts.join(',');
|
||||
});
|
||||
|
@@ -3,18 +3,17 @@ export function checkFileType(file: File, accepts: string[]) {
|
||||
// const reg = /\.(jpg|jpeg|png|gif|txt|doc|docx|xls|xlsx|xml)$/i;
|
||||
const reg = new RegExp('\\.(' + newTypes + ')$', 'i');
|
||||
|
||||
if (!reg.test(file.name)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return reg.test(file.name);
|
||||
}
|
||||
|
||||
export function checkImgType(file: File) {
|
||||
return /\.(jpg|jpeg|png|gif)$/i.test(file.name);
|
||||
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;
|
||||
|
@@ -6,6 +6,11 @@
|
||||
// &.ant-btn-primary:not(.ant-btn-link) {
|
||||
// box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.08) !important;
|
||||
// }
|
||||
// &-group {
|
||||
// .ant-btn:not(:first-child) {
|
||||
// bottom: 1px;
|
||||
// }
|
||||
// }
|
||||
|
||||
&-primary {
|
||||
color: @white;
|
||||
|
@@ -16,4 +16,4 @@
|
||||
@page-loading-z-index: 10000;
|
||||
|
||||
// left-menu
|
||||
@app-menu-item-height: 46px;
|
||||
@app-menu-item-height: 44px;
|
||||
|
@@ -4,6 +4,9 @@ const menu: MenuModule = {
|
||||
menu: {
|
||||
name: '组件',
|
||||
path: '/comp',
|
||||
tag: {
|
||||
dot: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'basic',
|
||||
@@ -38,10 +41,13 @@ const menu: MenuModule = {
|
||||
path: 'strength-meter',
|
||||
name: '密码强度组件',
|
||||
},
|
||||
// {
|
||||
// path: 'upload',
|
||||
// name: '上传组件',
|
||||
// },
|
||||
{
|
||||
path: 'upload',
|
||||
name: '上传组件',
|
||||
tag: {
|
||||
content: 'new',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'scroll',
|
||||
name: '滚动组件',
|
||||
|
8
src/types/global.d.ts
vendored
8
src/types/global.d.ts
vendored
@@ -1,5 +1,9 @@
|
||||
declare interface Fn<T = any> {
|
||||
(...arg: T[]): T;
|
||||
declare interface Fn<T = any, R = T> {
|
||||
(...arg: T[]): R;
|
||||
}
|
||||
|
||||
declare interface PromiseFn<T = any, R = T> {
|
||||
(...arg: T[]): Promise<R>;
|
||||
}
|
||||
|
||||
// 任意对象
|
||||
|
@@ -1,5 +1,25 @@
|
||||
import { dataURLtoBlob } from './stream';
|
||||
import { dataURLtoBlob, urlToBase64 } from './stream';
|
||||
|
||||
/**
|
||||
* Download online pictures
|
||||
* @param url
|
||||
* @param filename
|
||||
* @param mime
|
||||
* @param bom
|
||||
*/
|
||||
export function downloadByOnlineUrl(url: string, filename: string, mime?: string, bom?: BlobPart) {
|
||||
urlToBase64(url).then((base64) => {
|
||||
downloadByBase64(base64, filename, mime, bom);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download pictures based on base64
|
||||
* @param buf
|
||||
* @param filename
|
||||
* @param mime
|
||||
* @param bom
|
||||
*/
|
||||
export function downloadByBase64(buf: string, filename: string, mime?: string, bom?: BlobPart) {
|
||||
const base64Buf = dataURLtoBlob(buf);
|
||||
downloadByData(base64Buf, filename, mime, bom);
|
@@ -13,3 +13,29 @@ export function dataURLtoBlob(base64Buf: string): Blob {
|
||||
}
|
||||
return new Blob([u8arr], { type: mime });
|
||||
}
|
||||
|
||||
/**
|
||||
* img url to base64
|
||||
* @param url
|
||||
*/
|
||||
export function urlToBase64(url: string, mineType?: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let canvas = document.createElement('CANVAS') as Nullable<HTMLCanvasElement>;
|
||||
const ctx = canvas!.getContext('2d');
|
||||
|
||||
const img = new Image();
|
||||
img.crossOrigin = '';
|
||||
img.onload = function () {
|
||||
if (!canvas || !ctx) {
|
||||
return reject();
|
||||
}
|
||||
canvas.height = img.height;
|
||||
canvas.width = img.width;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const dataURL = canvas.toDataURL(mineType || 'image/png');
|
||||
canvas = null;
|
||||
resolve(dataURL);
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
@@ -118,11 +118,8 @@ export class VAxios {
|
||||
Object.keys(params.data).forEach((key) => {
|
||||
if (!params.data) return;
|
||||
const value = params.data[key];
|
||||
// support key-value array data
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => {
|
||||
// { list: [ 11, 22 ] }
|
||||
// formData.append('list[]', 11);
|
||||
formData.append(`${key}[]`, item);
|
||||
});
|
||||
return;
|
||||
|
@@ -160,20 +160,18 @@ const transform: AxiosTransform = {
|
||||
try {
|
||||
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
|
||||
createMessage.error('接口请求超时,请刷新页面重试!');
|
||||
return;
|
||||
}
|
||||
if (err && err.includes('Network Error')) {
|
||||
createErrorModal({
|
||||
title: '网络异常',
|
||||
content: '请检查您的网络连接是否正常!',
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
checkStatus(error.response && error.response.status, msg);
|
||||
return error;
|
||||
return Promise.reject(error);
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -38,4 +38,5 @@ export interface UploadFileParams {
|
||||
file: File | Blob;
|
||||
// 文件名
|
||||
filename?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
@@ -1,17 +1,60 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<UploadContainer :maxSize="5" />
|
||||
<a-alert message="基础示例" class="my-5"></a-alert>
|
||||
<BasicUpload :maxSize="20" :maxNumber="10" @change="handleChange" :api="uploadApi" />
|
||||
|
||||
<a-alert message="嵌入表单,加入表单校验" class="my-5"></a-alert>
|
||||
|
||||
<BasicForm @register="register" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { UploadContainer } from '/@/components/Upload/index';
|
||||
import { defineComponent, h } from 'vue';
|
||||
import { BasicUpload } from '/@/components/Upload';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
|
||||
|
||||
import { uploadApi } from '/@/api/sys/upload';
|
||||
// import { Alert } from 'ant-design-vue';
|
||||
|
||||
const schemas: FormSchema[] = [
|
||||
{
|
||||
field: 'field1',
|
||||
component: 'Input',
|
||||
label: '字段1',
|
||||
colProps: {
|
||||
span: 8,
|
||||
},
|
||||
rules: [{ required: true, type: 'array', message: '请选择上传文件' }],
|
||||
render: ({ model, field }) => {
|
||||
return h(BasicUpload, {
|
||||
value: model[field],
|
||||
api: uploadApi,
|
||||
onChange: (val: string[]) => {
|
||||
model[field] = val;
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
export default defineComponent({
|
||||
components: { UploadContainer },
|
||||
components: { BasicUpload, BasicForm },
|
||||
setup() {
|
||||
return {};
|
||||
const { createMessage } = useMessage();
|
||||
const [register] = useForm({
|
||||
labelWidth: 120,
|
||||
schemas,
|
||||
actionColOptions: {
|
||||
span: 16,
|
||||
},
|
||||
});
|
||||
return {
|
||||
handleChange: (list: string[]) => {
|
||||
createMessage.info(`已上传文件${JSON.stringify(list)}`);
|
||||
},
|
||||
uploadApi,
|
||||
register,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@@ -8,11 +8,21 @@
|
||||
|
||||
<a-alert message="base64流下载" />
|
||||
<a-button type="primary" class="my-4" @click="handleDownloadByBase64"> base64流下载 </a-button>
|
||||
|
||||
<a-alert message="图片Url下载,如果有跨域问题,需要处理图片跨域" />
|
||||
<a-button type="primary" class="my-4" @click="handleDownloadByOnlineUrl">
|
||||
图片Url下载
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { downloadByUrl, downloadByData, downloadByBase64 } from '/@/utils/file/FileDownload';
|
||||
import {
|
||||
downloadByUrl,
|
||||
downloadByData,
|
||||
downloadByBase64,
|
||||
downloadByOnlineUrl,
|
||||
} from '/@/utils/file/download';
|
||||
import imgBase64 from './imgBase64';
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
@@ -24,15 +34,28 @@
|
||||
url: 'https://codeload.github.com/anncwb/vue-vben-admin-doc/zip/master',
|
||||
target: '_self',
|
||||
});
|
||||
|
||||
downloadByUrl({
|
||||
url: 'https://vebn.oss-cn-beijing.aliyuncs.com/vben/logo.png',
|
||||
target: '_self',
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadByBase64() {
|
||||
downloadByBase64(imgBase64, 'logo.png');
|
||||
}
|
||||
|
||||
function handleDownloadByOnlineUrl() {
|
||||
downloadByOnlineUrl(
|
||||
'https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5944817f47b8408e9f1442ece49d68ca~tplv-k3u1fbpfcp-watermark.image',
|
||||
'logo.png'
|
||||
);
|
||||
}
|
||||
return {
|
||||
handleDownloadByUrl,
|
||||
handleDownByData,
|
||||
handleDownloadByBase64,
|
||||
handleDownloadByOnlineUrl,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user