mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-01-24 18:40:22 +08:00
feat: Form增加ImageUpload组件 (#3172)
* feat: Form增加图片上传组件 * fix: 还原表单组件引用 * chore: ImageUpload demo * chore: update ImageUpload demo * fix: 'visible' will be removed in next major version * chore: 修改api接口返回值参数 --------- Co-authored-by: Li Kui <90845831+likui628@users.noreply.github.com>
This commit is contained in:
parent
e4bcf8c899
commit
b776ac4cd8
@ -13,5 +13,6 @@ export { default as ApiTree } from './src/components/ApiTree.vue';
|
|||||||
export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
|
export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
|
||||||
export { default as ApiCascader } from './src/components/ApiCascader.vue';
|
export { default as ApiCascader } from './src/components/ApiCascader.vue';
|
||||||
export { default as ApiTransfer } from './src/components/ApiTransfer.vue';
|
export { default as ApiTransfer } from './src/components/ApiTransfer.vue';
|
||||||
|
export { default as ImageUpload } from './src/components/ImageUpload.vue';
|
||||||
|
|
||||||
export { BasicForm };
|
export { BasicForm };
|
||||||
|
@ -5,22 +5,21 @@ import type { ComponentType } from './types/index';
|
|||||||
* Component list, register here to setting it in the form
|
* Component list, register here to setting it in the form
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
Radio,
|
|
||||||
Checkbox,
|
|
||||||
AutoComplete,
|
AutoComplete,
|
||||||
Cascader,
|
Cascader,
|
||||||
|
Checkbox,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
|
Divider,
|
||||||
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
|
Radio,
|
||||||
|
Rate,
|
||||||
|
Select,
|
||||||
|
Slider,
|
||||||
Switch,
|
Switch,
|
||||||
TimePicker,
|
TimePicker,
|
||||||
TreeSelect,
|
TreeSelect,
|
||||||
Slider,
|
|
||||||
Rate,
|
|
||||||
Divider,
|
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import ApiRadioGroup from './components/ApiRadioGroup.vue';
|
import ApiRadioGroup from './components/ApiRadioGroup.vue';
|
||||||
import RadioButtonGroup from './components/RadioButtonGroup.vue';
|
import RadioButtonGroup from './components/RadioButtonGroup.vue';
|
||||||
import ApiSelect from './components/ApiSelect.vue';
|
import ApiSelect from './components/ApiSelect.vue';
|
||||||
@ -28,6 +27,7 @@ import ApiTree from './components/ApiTree.vue';
|
|||||||
import ApiTreeSelect from './components/ApiTreeSelect.vue';
|
import ApiTreeSelect from './components/ApiTreeSelect.vue';
|
||||||
import ApiCascader from './components/ApiCascader.vue';
|
import ApiCascader from './components/ApiCascader.vue';
|
||||||
import ApiTransfer from './components/ApiTransfer.vue';
|
import ApiTransfer from './components/ApiTransfer.vue';
|
||||||
|
import ImageUpload from './components/ImageUpload.vue';
|
||||||
import { BasicUpload } from '/@/components/Upload';
|
import { BasicUpload } from '/@/components/Upload';
|
||||||
import { StrengthMeter } from '/@/components/StrengthMeter';
|
import { StrengthMeter } from '/@/components/StrengthMeter';
|
||||||
import { IconPicker } from '/@/components/Icon';
|
import { IconPicker } from '/@/components/Icon';
|
||||||
@ -42,7 +42,7 @@ componentMap.set('InputSearch', Input.Search);
|
|||||||
componentMap.set('InputTextArea', Input.TextArea);
|
componentMap.set('InputTextArea', Input.TextArea);
|
||||||
componentMap.set('InputNumber', InputNumber);
|
componentMap.set('InputNumber', InputNumber);
|
||||||
componentMap.set('AutoComplete', AutoComplete);
|
componentMap.set('AutoComplete', AutoComplete);
|
||||||
|
componentMap.set('ImageUpload', ImageUpload);
|
||||||
componentMap.set('Select', Select);
|
componentMap.set('Select', Select);
|
||||||
componentMap.set('ApiSelect', ApiSelect);
|
componentMap.set('ApiSelect', ApiSelect);
|
||||||
componentMap.set('ApiTree', ApiTree);
|
componentMap.set('ApiTree', ApiTree);
|
||||||
|
253
src/components/Form/src/components/ImageUpload.vue
Normal file
253
src/components/Form/src/components/ImageUpload.vue
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
<template>
|
||||||
|
<div class="clearfix">
|
||||||
|
<a-upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
:list-type="listType"
|
||||||
|
:multiple="multiple"
|
||||||
|
:max-count="maxCount"
|
||||||
|
:customRequest="handleCustomRequest"
|
||||||
|
:before-upload="handleBeforeUpload"
|
||||||
|
v-bind="$attrs"
|
||||||
|
@preview="handlePreview"
|
||||||
|
v-model:value="state"
|
||||||
|
>
|
||||||
|
<div v-if="fileList.length < maxCount">
|
||||||
|
<plus-outlined />
|
||||||
|
<div style="margin-top: 8px">
|
||||||
|
{{ t('component.upload.upload') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-upload>
|
||||||
|
<a-modal :open="previewOpen" :footer="null" @cancel="handleCancel">
|
||||||
|
<img alt="example" style="width: 100%" :src="previewImage" />
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType, reactive, ref, watch } from 'vue';
|
||||||
|
import { message, Modal, Upload, UploadProps } from 'ant-design-vue';
|
||||||
|
import { UploadFile } from 'ant-design-vue/lib/upload/interface';
|
||||||
|
import { useI18n } from '@/hooks/web/useI18n';
|
||||||
|
import { join } from 'lodash-es';
|
||||||
|
import { buildShortUUID } from '@/utils/uuid';
|
||||||
|
import { isArray, isNotEmpty, isUrl } from '@/utils/is';
|
||||||
|
import { useRuleFormItem } from '@/hooks/component/useFormItem';
|
||||||
|
import { useAttrs } from '@vben/hooks';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
|
type ImageUploadType = 'text' | 'picture' | 'picture-card';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ImageUpload',
|
||||||
|
components: {
|
||||||
|
PlusOutlined,
|
||||||
|
AUpload: Upload,
|
||||||
|
AModal: Modal,
|
||||||
|
},
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
value: [Array, String],
|
||||||
|
api: {
|
||||||
|
type: Function as PropType<(file: UploadFile) => Promise<string>>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
listType: {
|
||||||
|
type: String as PropType<ImageUploadType>,
|
||||||
|
default: () => 'picture-card',
|
||||||
|
},
|
||||||
|
// 文件类型
|
||||||
|
fileType: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ['image/png', 'image/jpeg'],
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: () => false,
|
||||||
|
},
|
||||||
|
// 最大数量的文件
|
||||||
|
maxCount: {
|
||||||
|
type: Number,
|
||||||
|
default: () => 1,
|
||||||
|
},
|
||||||
|
// 最小数量的文件
|
||||||
|
minCount: {
|
||||||
|
type: Number,
|
||||||
|
default: () => 0,
|
||||||
|
},
|
||||||
|
// 文件最大多少MB
|
||||||
|
maxSize: {
|
||||||
|
type: Number,
|
||||||
|
default: () => 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['change', 'update:value'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const attrs = useAttrs();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const previewOpen = ref(false);
|
||||||
|
const previewImage = ref('');
|
||||||
|
const emitData = ref<any[] | any | undefined>();
|
||||||
|
const fileList = ref<UploadFile[]>([]);
|
||||||
|
|
||||||
|
// Embedded in the form, just use the hook binding to perform form verification
|
||||||
|
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||||
|
|
||||||
|
const fileState = reactive<{
|
||||||
|
newList: any[];
|
||||||
|
newStr: string;
|
||||||
|
oldStr: string;
|
||||||
|
}>({
|
||||||
|
newList: [],
|
||||||
|
newStr: '',
|
||||||
|
oldStr: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => fileList.value,
|
||||||
|
(v) => {
|
||||||
|
fileState.newList = v
|
||||||
|
.filter((item: any) => {
|
||||||
|
return item?.url && item.status === 'done' && isUrl(item?.url);
|
||||||
|
})
|
||||||
|
.map((item: any) => item?.url);
|
||||||
|
fileState.newStr = join(fileState.newList);
|
||||||
|
// 不相等代表数据变更
|
||||||
|
if (fileState.newStr !== fileState.oldStr) {
|
||||||
|
fileState.oldStr = fileState.newStr;
|
||||||
|
emitData.value = props.multiple ? fileState.newList : fileState.newStr;
|
||||||
|
state.value = props.multiple ? fileState.newList : fileState.newStr;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => state.value,
|
||||||
|
(v) => {
|
||||||
|
changeFileValue(v);
|
||||||
|
emit('update:value', v);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function changeFileValue(value: any) {
|
||||||
|
const stateStr = props.multiple ? join((value as string[]) || []) : value || '';
|
||||||
|
if (stateStr !== fileState.oldStr) {
|
||||||
|
fileState.oldStr = stateStr;
|
||||||
|
let list: string[] = [];
|
||||||
|
if (props.multiple) {
|
||||||
|
if (isNotEmpty(value)) {
|
||||||
|
if (isArray(value)) {
|
||||||
|
list = value as string[];
|
||||||
|
} else {
|
||||||
|
list.push(value as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isNotEmpty(value)) {
|
||||||
|
list.push(value as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileList.value = list.map((item) => {
|
||||||
|
const uuid = buildShortUUID();
|
||||||
|
return {
|
||||||
|
uid: uuid,
|
||||||
|
name: uuid,
|
||||||
|
status: 'done',
|
||||||
|
url: item,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关闭查看 */
|
||||||
|
const handleCancel = () => {
|
||||||
|
previewOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 查看图片 */
|
||||||
|
// @ts-ignore
|
||||||
|
const handlePreview = async (file: UploadProps['fileList'][number]) => {
|
||||||
|
if (!file.url && !file.preview) {
|
||||||
|
file.preview = (await getBase64(file.originFileObj)) as string;
|
||||||
|
}
|
||||||
|
previewImage.value = file.url || file.preview;
|
||||||
|
previewOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 上传前校验 */
|
||||||
|
const handleBeforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||||
|
if (fileList.value.length > props.maxCount) {
|
||||||
|
fileList.value.splice(props.maxCount, fileList.value.length - props.maxCount);
|
||||||
|
message.error(t('component.upload.maxNumber', [props.maxCount]));
|
||||||
|
return Upload.LIST_IGNORE;
|
||||||
|
}
|
||||||
|
const isPNG = props.fileType.includes(file.type);
|
||||||
|
if (!isPNG) {
|
||||||
|
message.error(t('component.upload.acceptUpload', [props.fileType.toString()]));
|
||||||
|
}
|
||||||
|
const isLt2M = file.size / 1024 / 1024 < props.maxSize;
|
||||||
|
if (!isLt2M) {
|
||||||
|
message.error(t('component.upload.maxSizeMultiple', [props.maxSize]));
|
||||||
|
}
|
||||||
|
if (!(isPNG && isLt2M)) {
|
||||||
|
fileList.value.pop();
|
||||||
|
}
|
||||||
|
return (isPNG && isLt2M) || Upload.LIST_IGNORE;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 自定义上传 */
|
||||||
|
const handleCustomRequest = async (option: any) => {
|
||||||
|
const { file } = option;
|
||||||
|
await props
|
||||||
|
.api(option)
|
||||||
|
.then((url) => {
|
||||||
|
file.url = url;
|
||||||
|
file.status = 'done';
|
||||||
|
fileList.value.pop();
|
||||||
|
fileList.value.push(file);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
fileList.value.pop();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function getBase64(file: File) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => resolve(reader.result);
|
||||||
|
reader.onerror = (error) => reject(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
previewOpen,
|
||||||
|
fileList,
|
||||||
|
state,
|
||||||
|
attrs,
|
||||||
|
t,
|
||||||
|
handlePreview,
|
||||||
|
handleBeforeUpload,
|
||||||
|
handleCustomRequest,
|
||||||
|
handleCancel,
|
||||||
|
previewImage,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* you can make up upload button and sample style by using stylesheets */
|
||||||
|
.ant-upload-select-picture-card i {
|
||||||
|
color: #999;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-upload-select-picture-card .ant-upload-text {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
@ -86,5 +86,6 @@ export const NO_AUTO_LINK_COMPONENTS: ComponentType[] = [
|
|||||||
'ApiCascader',
|
'ApiCascader',
|
||||||
'AutoComplete',
|
'AutoComplete',
|
||||||
'RadioButtonGroup',
|
'RadioButtonGroup',
|
||||||
|
'ImageUpload',
|
||||||
'ApiSelect',
|
'ApiSelect',
|
||||||
];
|
];
|
||||||
|
@ -110,6 +110,7 @@ export type ComponentType =
|
|||||||
| 'Switch'
|
| 'Switch'
|
||||||
| 'StrengthMeter'
|
| 'StrengthMeter'
|
||||||
| 'Upload'
|
| 'Upload'
|
||||||
|
| 'ImageUpload'
|
||||||
| 'IconPicker'
|
| 'IconPicker'
|
||||||
| 'Render'
|
| 'Render'
|
||||||
| 'Slider'
|
| 'Slider'
|
||||||
|
@ -697,6 +697,17 @@
|
|||||||
allowHalf: true,
|
allowHalf: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'field23',
|
||||||
|
component: 'ImageUpload',
|
||||||
|
label: '字段23',
|
||||||
|
colProps: {
|
||||||
|
span: 8,
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
api: () => Promise.resolve('https://via.placeholder.com/600/92c952'),
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
Loading…
Reference in New Issue
Block a user