mirror of
https://github.com/vbenjs/vben-admin-thin-next.git
synced 2025-02-03 02:18:40 +08:00
wip: add upload component
This commit is contained in:
parent
2b95be8013
commit
746d4a745d
@ -5,7 +5,7 @@ VITE_USE_MOCK = true
|
|||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
# Cross-domain proxy, you can configure multiple
|
# Cross-domain proxy, you can configure multiple
|
||||||
VITE_PROXY=[["/api","http://localhost:3000"]]
|
VITE_PROXY=[["/api","http://localhost:3000"],["/upload","http://localhost:3001/upload"]]
|
||||||
# VITE_PROXY=[["/api","https://vvbin.cn/test"]]
|
# VITE_PROXY=[["/api","https://vvbin.cn/test"]]
|
||||||
|
|
||||||
# Delete console
|
# Delete console
|
||||||
|
5
src/api/demo/model/uploadModel.ts
Normal file
5
src/api/demo/model/uploadModel.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface UploadApiResult {
|
||||||
|
message: string;
|
||||||
|
code: number;
|
||||||
|
url: string;
|
||||||
|
}
|
23
src/api/demo/upload.ts
Normal file
23
src/api/demo/upload.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { UploadApiResult } from './model/uploadModel';
|
||||||
|
import { defHttp } from '/@/utils/http/axios';
|
||||||
|
import { UploadFileParams } from '/@/utils/http/axios/types';
|
||||||
|
|
||||||
|
enum Api {
|
||||||
|
UPLOAD_URL = '/upload',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 上传接口
|
||||||
|
*/
|
||||||
|
export function uploadApi(
|
||||||
|
params: UploadFileParams,
|
||||||
|
onUploadProgress: (progressEvent: ProgressEvent) => void
|
||||||
|
) {
|
||||||
|
return defHttp.uploadFile<UploadApiResult>(
|
||||||
|
{
|
||||||
|
url: Api.UPLOAD_URL,
|
||||||
|
onUploadProgress,
|
||||||
|
},
|
||||||
|
params
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
export interface ActionItem {
|
export interface ActionItem {
|
||||||
on?: any;
|
on?: any;
|
||||||
|
onClick?: any;
|
||||||
label: string;
|
label: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
color?: 'success' | 'error' | 'warning';
|
color?: 'success' | 'error' | 'warning';
|
||||||
|
2
src/components/Upload/index.ts
Normal file
2
src/components/Upload/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as UploadContainer } from './src/UploadContainer.vue';
|
||||||
|
// export * from './src/types';
|
29
src/components/Upload/src/ThumnUrl.vue
Normal file
29
src/components/Upload/src/ThumnUrl.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<img v-if="fileUrl" :src="fileUrl" />
|
||||||
|
<span v-else>{{ fileType }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
fileUrl: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
fileType: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
fileName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
62
src/components/Upload/src/UploadContainer.vue
Normal file
62
src/components/Upload/src/UploadContainer.vue
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<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>
|
244
src/components/Upload/src/UploadModal.vue
Normal file
244
src/components/Upload/src/UploadModal.vue
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
v-bind="$attrs"
|
||||||
|
@register="register"
|
||||||
|
@ok="handleOk"
|
||||||
|
:closeFunc="handleCloseFunc"
|
||||||
|
:maskClosable="false"
|
||||||
|
width="800px"
|
||||||
|
title="上传组件"
|
||||||
|
wrapClassName="upload-modal"
|
||||||
|
:okButtonProps="{ disabled: isUploadingRef }"
|
||||||
|
:cancelButtonProps="{ disabled: isUploadingRef }"
|
||||||
|
>
|
||||||
|
<template #centerdFooter>
|
||||||
|
<a-button @click="handleStartUpload" color="success" :loading="isUploadingRef">
|
||||||
|
{{ isUploadingRef ? '上传中' : '开始上传' }}
|
||||||
|
</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" />
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, ref, toRef, unref } from 'vue';
|
||||||
|
import { Upload } 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 './types';
|
||||||
|
import { basicProps } from './props';
|
||||||
|
import { createTableColumns, createActionColumn } from './data';
|
||||||
|
// utils
|
||||||
|
import { checkFileType, checkImgType, getBase64WithFile } from './utils';
|
||||||
|
import { buildUUID } from '/@/utils/uuid';
|
||||||
|
import { createImgPreview } from '/@/components/Preview/index';
|
||||||
|
import { uploadApi } from '/@/api/demo/upload';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { BasicModal, Upload, BasicTable },
|
||||||
|
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 fileListRef = ref<FileItem[]>([]);
|
||||||
|
const state = reactive<{ fileList: FileItem[] }>({ fileList: [] });
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
// 上传前校验
|
||||||
|
function beforeUpload(file: File) {
|
||||||
|
const { size, name } = file;
|
||||||
|
const { maxSize } = props;
|
||||||
|
const accept = unref(getAccept);
|
||||||
|
|
||||||
|
// 设置最大值,则判断
|
||||||
|
if (maxSize && file.size / 1024 / 1024 >= maxSize) {
|
||||||
|
createMessage.error(`只能上传不超过${maxSize}MB的文件!`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置类型,则判断
|
||||||
|
if (accept.length > 0 && !checkFileType(file, accept)) {
|
||||||
|
createMessage.error!(`只能上传${accept.join(',')}格式文件`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 生成图片缩略图
|
||||||
|
if (checkImgType(file)) {
|
||||||
|
// beforeUpload,如果异步会调用自带上传方法
|
||||||
|
// file.thumbUrl = await getBase64(file);
|
||||||
|
getBase64WithFile(file).then(({ result: thumbUrl }) => {
|
||||||
|
fileListRef.value = [
|
||||||
|
...unref(fileListRef),
|
||||||
|
{
|
||||||
|
uuid: buildUUID(),
|
||||||
|
file,
|
||||||
|
thumbUrl,
|
||||||
|
size,
|
||||||
|
name,
|
||||||
|
percent: 0,
|
||||||
|
type: name.split('.').pop(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fileListRef.value = [
|
||||||
|
...unref(fileListRef),
|
||||||
|
{
|
||||||
|
uuid: buildUUID(),
|
||||||
|
|
||||||
|
file,
|
||||||
|
size,
|
||||||
|
name,
|
||||||
|
percent: 0,
|
||||||
|
type: name.split('.').pop(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 删除
|
||||||
|
function handleRemove(record: FileItem) {
|
||||||
|
const index = fileListRef.value.findIndex((item) => item.uuid === record.uuid);
|
||||||
|
index !== -1 && fileListRef.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
// 预览
|
||||||
|
function handlePreview(record: FileItem) {
|
||||||
|
const { thumbUrl = '' } = record;
|
||||||
|
createImgPreview({
|
||||||
|
imageList: [thumbUrl],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const [registerTable] = useTable({
|
||||||
|
columns: createTableColumns(),
|
||||||
|
actionColumn: createActionColumn(handleRemove, handlePreview),
|
||||||
|
pagination: false,
|
||||||
|
});
|
||||||
|
// 是否正在上传
|
||||||
|
const isUploadingRef = ref(false);
|
||||||
|
async function uploadApiByItem(item: FileItem) {
|
||||||
|
try {
|
||||||
|
item.status = UploadResultStatus.UPLOADING;
|
||||||
|
|
||||||
|
const { data } = await uploadApi(
|
||||||
|
{
|
||||||
|
file: item.file,
|
||||||
|
},
|
||||||
|
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() {
|
||||||
|
try {
|
||||||
|
isUploadingRef.value = true;
|
||||||
|
const data = await Promise.all(
|
||||||
|
unref(fileListRef).map((item) => {
|
||||||
|
return uploadApiByItem(item);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
isUploadingRef.value = false;
|
||||||
|
// 生产环境:抛出错误
|
||||||
|
const errorList = data.filter((item) => !item.success);
|
||||||
|
if (errorList.length > 0) {
|
||||||
|
throw errorList;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
isUploadingRef.value = false;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 点击保存
|
||||||
|
function handleOk() {
|
||||||
|
// TODO: 没起作用:okButtonProps={{ disabled: state.isUploading }}
|
||||||
|
if (isUploadingRef.value) {
|
||||||
|
createMessage.warning('请等待文件上传后,保存');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fileList: string[] = [];
|
||||||
|
|
||||||
|
for (const item of fileListRef.value) {
|
||||||
|
const { status, responseData } = item;
|
||||||
|
if (status === UploadResultStatus.SUCCESS && responseData) {
|
||||||
|
fileList.push(responseData.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存在一个上传成功的即可保存
|
||||||
|
|
||||||
|
if (fileList.length <= 0) {
|
||||||
|
createMessage.warning('没有上传成功的文件,无法保存');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(fileList);
|
||||||
|
emit('change', fileList);
|
||||||
|
fileListRef.value = [];
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
// 点击关闭:则所有操作不保存,包括上传的
|
||||||
|
function handleCloseFunc() {
|
||||||
|
if (!isUploadingRef.value) {
|
||||||
|
fileListRef.value = [];
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
createMessage.warning('请等待文件上传结束后操作');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
register,
|
||||||
|
closeModal,
|
||||||
|
getHelpText,
|
||||||
|
getStringAccept,
|
||||||
|
beforeUpload,
|
||||||
|
registerTable,
|
||||||
|
fileListRef,
|
||||||
|
state,
|
||||||
|
isUploadingRef,
|
||||||
|
handleStartUpload,
|
||||||
|
handleOk,
|
||||||
|
handleCloseFunc,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
// /deep/ .ant-upload-list {
|
||||||
|
// display: none;
|
||||||
|
// }
|
||||||
|
.upload-modal {
|
||||||
|
.ant-upload-list {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-wrapper .ant-spin-nested-loading {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
93
src/components/Upload/src/UploadPreviewModal.vue
Normal file
93
src/components/Upload/src/UploadPreviewModal.vue
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
wrapClassName="upload-preview-modal"
|
||||||
|
v-bind="$attrs"
|
||||||
|
width="800px"
|
||||||
|
@register="register"
|
||||||
|
title="预览"
|
||||||
|
:showOkBtn="false"
|
||||||
|
>
|
||||||
|
<BasicTable @register="registerTable" :dataSource="fileListRef" />
|
||||||
|
</BasicModal>
|
||||||
|
</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 { PreviewFileItem } from './types';
|
||||||
|
import { createImgPreview } from '/@/components/Preview/index';
|
||||||
|
import { downloadByUrl } from '/@/utils/file/FileDownload';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { BasicModal, BasicTable },
|
||||||
|
props: priviewProps,
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const [register, { closeModal }] = useModalInner();
|
||||||
|
const fileListRef = ref<PreviewFileItem[]>([]);
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
(value) => {
|
||||||
|
fileListRef.value = [];
|
||||||
|
value.forEach((item) => {
|
||||||
|
fileListRef.value = [
|
||||||
|
...unref(fileListRef),
|
||||||
|
{
|
||||||
|
url: item,
|
||||||
|
type: item.split('.').pop() || '',
|
||||||
|
name: item.split('/').pop() || '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ 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',
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
const [registerTable] = useTable({
|
||||||
|
columns: createPreviewColumns(),
|
||||||
|
pagination: false,
|
||||||
|
actionColumn: createPreviewActionColumn({ handleRemove, handlePreview, handleDownload }),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
register,
|
||||||
|
closeModal,
|
||||||
|
fileListRef,
|
||||||
|
registerTable,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
.upload-preview-modal {
|
||||||
|
.ant-upload-list {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-wrapper .ant-spin-nested-loading {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
159
src/components/Upload/src/data.tsx
Normal file
159
src/components/Upload/src/data.tsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
// import { BasicColumn, TableAction, ActionItem } from '@/components/table';
|
||||||
|
import { checkImgType, isImgTypeByName } from './utils';
|
||||||
|
// import ThumnUrl from './ThumbUrl.vue';
|
||||||
|
import { Progress } 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';
|
||||||
|
|
||||||
|
// 文件上传列表
|
||||||
|
export function createTableColumns(): BasicColumn[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
dataIndex: 'thumbUrl',
|
||||||
|
title: '图例',
|
||||||
|
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} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'name',
|
||||||
|
title: '文件名',
|
||||||
|
align: 'left',
|
||||||
|
customRender: ({ text, record }) => {
|
||||||
|
const { percent, status: uploadStatus } = (record as FileItem) || {};
|
||||||
|
let status = '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="ellipsis mb-1" title={text}>
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
<Progress percent={percent} size="small" status={status} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'size',
|
||||||
|
title: '文件大小',
|
||||||
|
width: 100,
|
||||||
|
customRender: ({ text = 0 }) => {
|
||||||
|
return text && (text / 1024).toFixed(2) + 'KB';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// dataIndex: 'type',
|
||||||
|
// title: '文件类型',
|
||||||
|
// width: 100,
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
dataIndex: 'status',
|
||||||
|
title: '状态',
|
||||||
|
width: 100,
|
||||||
|
customRender: ({ text }) => {
|
||||||
|
if (text === UploadResultStatus.SUCCESS) {
|
||||||
|
return '上传成功';
|
||||||
|
} else if (text === UploadResultStatus.ERROR) {
|
||||||
|
return '上传失败';
|
||||||
|
} else if (text === UploadResultStatus.UPLOADING) {
|
||||||
|
return '上传中';
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
export function createActionColumn(handleRemove: Function, handlePreview: Function): BasicColumn {
|
||||||
|
return {
|
||||||
|
width: 120,
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'action',
|
||||||
|
fixed: false,
|
||||||
|
customRender: ({ record }) => {
|
||||||
|
const actions: ActionItem[] = [
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
onClick: handleRemove.bind(null, record),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (checkImgType(record)) {
|
||||||
|
actions.unshift({
|
||||||
|
label: '预览',
|
||||||
|
onClick: handlePreview.bind(null, record),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return <TableAction actions={actions} />;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 文件预览列表
|
||||||
|
export function createPreviewColumns(): BasicColumn[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
dataIndex: 'url',
|
||||||
|
title: '图例',
|
||||||
|
width: 100,
|
||||||
|
customRender: ({ record }) => {
|
||||||
|
const { url, type } = (record as PreviewFileItem) || {};
|
||||||
|
return (
|
||||||
|
<span>{isImgTypeByName(url) ? <img src={url} style={{ width: '50px' }} /> : type}</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'name',
|
||||||
|
title: '文件名',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPreviewActionColumn({
|
||||||
|
handleRemove,
|
||||||
|
handlePreview,
|
||||||
|
handleDownload,
|
||||||
|
}: {
|
||||||
|
handleRemove: Function;
|
||||||
|
handlePreview: Function;
|
||||||
|
handleDownload: Function;
|
||||||
|
}): BasicColumn {
|
||||||
|
return {
|
||||||
|
width: 160,
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'action',
|
||||||
|
fixed: false,
|
||||||
|
customRender: ({ record }) => {
|
||||||
|
const { url } = (record as PreviewFileItem) || {};
|
||||||
|
|
||||||
|
const actions: ActionItem[] = [
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
onClick: handleRemove.bind(null, record),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '下载',
|
||||||
|
onClick: handleDownload.bind(null, record),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (isImgTypeByName(url)) {
|
||||||
|
actions.unshift({
|
||||||
|
label: '预览',
|
||||||
|
onClick: handlePreview.bind(null, record),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return <TableAction actions={actions} />;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
42
src/components/Upload/src/props.ts
Normal file
42
src/components/Upload/src/props.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
export const basicProps = {
|
||||||
|
helpText: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
// 文件最大多少MB
|
||||||
|
maxSize: {
|
||||||
|
type: Number as PropType<number>,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
// 最大数量的文件,0不限制
|
||||||
|
maxNumber: {
|
||||||
|
type: Number as PropType<number>,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
// 根据后缀,或者其他
|
||||||
|
accept: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadContainerProps = {
|
||||||
|
value: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
...basicProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const priviewProps = {
|
||||||
|
value: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
};
|
25
src/components/Upload/src/types.ts
Normal file
25
src/components/Upload/src/types.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { UploadApiResult } from '/@/api/demo/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;
|
||||||
|
}
|
55
src/components/Upload/src/useUpload.ts
Normal file
55
src/components/Upload/src/useUpload.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Ref, unref, computed } from 'vue';
|
||||||
|
|
||||||
|
export function useUploadType({
|
||||||
|
acceptRef,
|
||||||
|
// uploadTypeRef,
|
||||||
|
helpTextRef,
|
||||||
|
maxNumberRef,
|
||||||
|
maxSizeRef,
|
||||||
|
}: {
|
||||||
|
acceptRef: Ref<string[]>;
|
||||||
|
// uploadTypeRef: Ref<UploadTypeEnum>;
|
||||||
|
helpTextRef: Ref<string>;
|
||||||
|
maxNumberRef: Ref<number>;
|
||||||
|
maxSizeRef: Ref<number>;
|
||||||
|
}) {
|
||||||
|
// 文件类型限制
|
||||||
|
const getAccept = computed(() => {
|
||||||
|
// const uploadType = unref(uploadTypeRef);
|
||||||
|
const accept = unref(acceptRef);
|
||||||
|
if (accept && accept.length > 0) {
|
||||||
|
return accept;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
const getStringAccept = computed(() => {
|
||||||
|
return unref(getAccept)
|
||||||
|
.map((item) => `.${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(`支持${accept.join(',')}格式`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSize = unref(maxSizeRef);
|
||||||
|
if (maxSize) {
|
||||||
|
helpTexts.push(`不超过${maxSize}MB`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxNumber = unref(maxNumberRef);
|
||||||
|
if (maxNumber) {
|
||||||
|
helpTexts.push(`最多可选择${maxNumber}个文件`);
|
||||||
|
}
|
||||||
|
return helpTexts.join(',');
|
||||||
|
});
|
||||||
|
return { getAccept, getStringAccept, getHelpText };
|
||||||
|
}
|
28
src/components/Upload/src/utils.ts
Normal file
28
src/components/Upload/src/utils.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export function checkFileType(file: File, accepts: string[]) {
|
||||||
|
const newTypes = accepts.join('|');
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function checkImgType(file: File) {
|
||||||
|
return /\.(jpg|jpeg|png|gif)$/i.test(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);
|
||||||
|
});
|
||||||
|
}
|
@ -38,6 +38,10 @@ const menu: MenuModule = {
|
|||||||
path: 'strength-meter',
|
path: 'strength-meter',
|
||||||
name: '密码强度组件',
|
name: '密码强度组件',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'upload',
|
||||||
|
name: '上传组件',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'scroll',
|
path: 'scroll',
|
||||||
name: '滚动组件',
|
name: '滚动组件',
|
||||||
|
@ -170,5 +170,13 @@ export default {
|
|||||||
title: '密码强度组件',
|
title: '密码强度组件',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/upload',
|
||||||
|
name: 'UploadDemo',
|
||||||
|
component: () => import('/@/views/demo/comp/upload/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '上传组件',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
} as AppRouteModule;
|
} as AppRouteModule;
|
||||||
|
@ -5,9 +5,10 @@ import { AxiosCanceler } from './axiosCancel';
|
|||||||
import { isFunction } from '/@/utils/is';
|
import { isFunction } from '/@/utils/is';
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import type { RequestOptions, CreateAxiosOptions, Result } from './types';
|
import type { RequestOptions, CreateAxiosOptions, Result, UploadFileParams } from './types';
|
||||||
// import { ContentTypeEnum } from '/@/enums/httpEnum';
|
// import { ContentTypeEnum } from '/@/enums/httpEnum';
|
||||||
import { errorResult } from './const';
|
import { errorResult } from './const';
|
||||||
|
import { ContentTypeEnum } from '/@/enums/httpEnum';
|
||||||
|
|
||||||
export * from './axiosTransform';
|
export * from './axiosTransform';
|
||||||
|
|
||||||
@ -107,25 +108,42 @@ export class VAxios {
|
|||||||
this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
|
this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * @description: 文件上传
|
* @description: 文件上传
|
||||||
// */
|
*/
|
||||||
// uploadFiles(config: AxiosRequestConfig, params: File[]) {
|
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
|
||||||
// const formData = new FormData();
|
const formData = new window.FormData();
|
||||||
|
|
||||||
// Object.keys(params).forEach((key) => {
|
if (params.data) {
|
||||||
// formData.append(key, params[key as any]);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// return this.request({
|
formData.append(key, params.data[key]);
|
||||||
// ...config,
|
});
|
||||||
// method: 'POST',
|
}
|
||||||
// data: formData,
|
|
||||||
// headers: {
|
formData.append(params.name || 'file', params.file, params.filename);
|
||||||
// 'Content-type': ContentTypeEnum.FORM_DATA,
|
|
||||||
// },
|
return this.axiosInstance.request<T>({
|
||||||
// });
|
...config,
|
||||||
// }
|
method: 'POST',
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
'Content-type': ContentTypeEnum.FORM_DATA,
|
||||||
|
ignoreCancelToken: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: 请求方法
|
* @description: 请求方法
|
||||||
|
@ -28,3 +28,14 @@ export interface Result<T = any> {
|
|||||||
message: string;
|
message: string;
|
||||||
result: T;
|
result: T;
|
||||||
}
|
}
|
||||||
|
// multipart/form-data:上传文件
|
||||||
|
export interface UploadFileParams {
|
||||||
|
// 其他参数
|
||||||
|
data?: { [key: string]: any };
|
||||||
|
// 文件参数的接口字段名
|
||||||
|
name?: string;
|
||||||
|
// 文件
|
||||||
|
file: File | Blob;
|
||||||
|
// 文件名
|
||||||
|
filename?: string;
|
||||||
|
}
|
||||||
|
17
src/views/demo/comp/upload/index.vue
Normal file
17
src/views/demo/comp/upload/index.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-4">
|
||||||
|
<UploadContainer :maxSize="5" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { UploadContainer } from '/@/components/Upload/index';
|
||||||
|
|
||||||
|
// import { Alert } from 'ant-design-vue';
|
||||||
|
export default defineComponent({
|
||||||
|
components: { UploadContainer },
|
||||||
|
setup() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
Loading…
Reference in New Issue
Block a user