提交 661db0c7 编写于 作者: V vben

perf(upload): improve upload component

上级 a161bfa8
......@@ -3,6 +3,11 @@
### ✨ Features
- 新增 base64 文件流下载
- 优化上传组件及示例
### 🎫 Chores
- 更新 antdv 到`2.0.0-rc.1`
## 2.0.0-rc.10 (2020-11-13)
......
......@@ -226,10 +226,10 @@ yarn clean:lib # Delete node_modules, supported window
- [x] Data import and export
- [x] Global error handling
- [x] Rich text component
- [x] Upload component
## Developing features
- [ ] Upload component
- [ ] Theme configuration
- [ ] Dark theme
- [ ] Build CDN
......
......@@ -228,10 +228,10 @@ yarn clean:lib # 删除node_modules,兼容window系统
- [x] 系统性能优化
- [x] 全局错误处理
- [x] 富文本组件
- [x] 上传组件
## 正在开发的功能
- [ ] 上传组件
- [ ] 主题配置
- [ ] 黑暗主题
- [ ] 打包 CDN
......
......@@ -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>,
......
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;
......
export { default as UploadContainer } from './src/UploadContainer.vue';
export { default as BasicUpload } from './src/BasicUpload.vue';
// export * from './src/types';
<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 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="$props" @register="registerUploadModal" @change="handleChange" />
<UploadModal v-bind="bindValue" @register="registerUploadModal" @change="handleChange" />
<UploadPreviewModal
:value="fileListRef"
@register="registerPreviewModal"
@change="handlePreviewChange"
@list-change="handlePreviewChange"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch, unref } from 'vue';
import { useModal } from '/@/components/Modal';
import { defineComponent, ref, watch, unref, computed } from 'vue';
import UploadModal from './UploadModal.vue';
import { uploadContainerProps } from './props';
import UploadPreviewModal from './UploadPreviewModal.vue';
import Icon from '/@/components/Icon/index';
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 },
components: { UploadModal, UploadPreviewModal, Icon, Tooltip },
props: uploadContainerProps,
setup(props, { emit }) {
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 || [])];
(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,
......@@ -56,6 +91,8 @@
registerPreviewModal,
openPreviewModal,
fileListRef,
showPreview,
bindValue,
};
},
});
......
......@@ -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>
<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 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: toRef(props, 'accept'),
helpTextRef: toRef(props, 'helpText'),
maxNumberRef: toRef(props, 'maxNumber'),
maxSizeRef: toRef(props, 'maxSize'),
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;
......
<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,
......
// 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: () => [],
......
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: '滚动组件',
......
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>;
}
// 任意对象
......
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;
}
<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,
};
},
});
......
......@@ -1050,10 +1050,10 @@
resolved "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.0.0-rc.2.tgz#c4a95ddc06ca9b9496df03604e66fdefb39f4c4b"
integrity sha512-BybEHU5/I9EQ0CcwKAqmreZ2bMnAXrqLCTptAc6vPetHMbrXdZfejP5mt57e/8PNSt/qE7BHniU5PCYA+PGIHw==
"@iconify/json@^1.1.254":
version "1.1.256"
resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.256.tgz#0f138d421ab12faca2fdd49aaf4fbc0122db08e3"
integrity sha512-CeLKbKL3lvq8afhR3LEyaBqXZDC52fgU0Ij3LbTRCwPUsumLNzhXA7MzN/f0JDYfXm9LShkfpgMcm00wQaANgg==
"@iconify/json@^1.1.258":
version "1.1.258"
resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.258.tgz#392064ae8fd4c6d542c21bb4d0d57d5860f38abb"
integrity sha512-x5DKhRrg8v1NWmClWa8zA80gWQ9xevivsUAF4s8CyAl/ZplBsEE1funKuuVcIKjexyE1UXb7uFWrUKt1fB5n1A==
"@koa/cors@^3.1.0":
version "3.1.0"
......@@ -1316,10 +1316,10 @@
"@types/qs" "*"
"@types/serve-static" "*"
"@types/fs-extra@^9.0.2":
version "9.0.3"
resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.3.tgz#9996e5cce993508c32325380b429f04a1327523e"
integrity sha512-NKdGoXLTFTRED3ENcfCsH8+ekV4gbsysanx2OPbstXVV6fZMgUCqTxubs6I9r7pbOJbFgVq1rpFtLURjKCZWUw==
"@types/fs-extra@^9.0.4":
version "9.0.4"
resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.4.tgz#12553138cf0438db9a31cdc8b0a3aa9332eb67aa"
integrity sha512-50GO5ez44lxK5MDH90DYHFFfqxH7+fTqEEnvguQRzJ/tY9qFrMSHLiYHite+F3SNmf7+LHC1eMXojuD+E3Qcyg==
dependencies:
"@types/node" "*"
......@@ -1725,18 +1725,18 @@
vscode-languageserver-textdocument "^1.0.1"
vscode-uri "^2.1.2"
"@vueuse/core@^4.0.0-beta.40":
version "4.0.0-beta.40"
resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.0-beta.40.tgz#7efdc15c1b994647dff7ae65c0ca573d96ce9b28"
integrity sha512-FOTOUrXAAp0NOmy8hMlP1HpUhnB8LeRJZDOEUl/A9gKMDwWvPTEvxKsDAIzSa4s7I0MapVzfeP3soNCNfl9+vQ==
"@vueuse/core@^4.0.0-beta.41":
version "4.0.0-beta.41"
resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.0-beta.41.tgz#0058aed5ade75ae2866283498009ad5172cbae84"
integrity sha512-CgUih65PzYScorm1S4F93e6XXm+qxA8GrRLOSB1kXaqtP6vXedwkBxKkNEYNACx4reL4VEHqM/BrM6FajXkQUg==
dependencies:
"@vueuse/shared" "4.0.0-beta.40"
"@vueuse/shared" "4.0.0-beta.41"
vue-demi latest
"@vueuse/shared@4.0.0-beta.40":
version "4.0.0-beta.40"
resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.0-beta.40.tgz#76e9b52228159e7ec88df2c8f4eea8fce1a42ec3"
integrity sha512-Ay71viUTXs0XX2hQ04kEExhpsCrw3KankBMP7euorsPjuQmIZjUA4NNOb45UAudg+uF5HXLpgWLvwb4cMOLHnQ==
"@vueuse/shared@4.0.0-beta.41":
version "4.0.0-beta.41"
resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.0-beta.41.tgz#395782ea2e580f1fc9488d25c89bd09f70170b25"
integrity sha512-dqnuEPPC3OUJ6L6rhMiOCuPWIR698DtdwOydwCZBISsG2V6gZ2QFND6xtRwLib6/lhUMYVYPwIz3hPjlx7BIzw==
dependencies:
vue-demi latest
......@@ -1850,10 +1850,10 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
ant-design-vue@^2.0.0-beta.15:
version "2.0.0-beta.15"
resolved "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-2.0.0-beta.15.tgz#3c787dabb70a33885d0e751e58f9a5610ed06134"
integrity sha512-OxZy+ZYU3LauIL4Rhqwy441K/iD++Cit6upnQy5+LVUrX0PSObPqPqMWVpncbAmJJYTEz88gkvgGeYqBdzouWA==
ant-design-vue@^2.0.0-rc.1:
version "2.0.0-rc.1"
resolved "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-2.0.0-rc.1.tgz#2ef02475f3aa4c1474f2fe3cf44a52c34787be02"
integrity sha512-iKXkFtTHarvLHV7LWmYh6g/Cmkv+xK+vS621A1Qvg37Z6lCGg3K9BGAizmklAYzOTiPz0Ltt63eSiNqYMGh52g==
dependencies:
"@ant-design-vue/use" "^0.0.1-0"
"@ant-design/icons-vue" "^5.1.5"
......@@ -8109,10 +8109,10 @@ vary@^1.1.2:
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
vditor@^3.6.0:
version "3.6.1"
resolved "https://registry.npmjs.org/vditor/-/vditor-3.6.1.tgz#b0b510f23d0cf0e5d8b3d36924e40400de96f692"
integrity sha512-83GdGLIWrV1x04aK8DO9aZidqQfmuGXXUbxSCuQxRla+T9KfnFRmJwfqIxXQm8h+4jUBCFL38e8PqLa3fBOf9w==
vditor@^3.6.2:
version "3.6.2"
resolved "https://registry.npmjs.org/vditor/-/vditor-3.6.2.tgz#ee6011efa3ec563c6356ed82efbf2e00ba2e35c6"
integrity sha512-HPHHun5+IXmYGMKDWcUD83VfP1Qfncz7DmaIKoWpluJgE8ve7s+4RbFBcaEpYPXYzIuL2UTHoMnIjmTPbenOCA==
dependencies:
diff-match-patch "^1.0.5"
......@@ -8272,10 +8272,10 @@ vue-i18n@^9.0.0-beta.6:
dependencies:
source-map "^0.6.1"
vue-router@^4.0.0-rc.2:
version "4.0.0-rc.2"
resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.0-rc.2.tgz#8545cab76a05ca4f6dffbe6c6a671a4dbf585ab2"
integrity sha512-51mBp39rzBFpk1nyU9SkhPcwR67gBzWIH8p3pyeDmtNYgWzGF3q8MneD/xbMwsfTQkw2H1qBk6uwRaVy3M8Nxw==
vue-router@^4.0.0-rc.3:
version "4.0.0-rc.3"
resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.0-rc.3.tgz#70d18e90030bc6a25e81a30401d673223998ec6b"
integrity sha512-NnPqWIfanEhJC4wu8BEFBmnEDIrx9ST0/HtmBiE+oV2MQlhyRk1TmdttWwVqx6Sh7kONsrI10GQV9l3YEkcWXg==
vue-types@^3.0.0:
version "3.0.1"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册