Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Form增加图片上传组件 #3172

Merged
merged 7 commits into from
Oct 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/Form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ApiCascader } from './src/components/ApiCascader.vue';
export { default as ApiTransfer } from './src/components/ApiTransfer.vue';
export { default as ImageUpload } from './src/components/ImageUpload.vue';

export { BasicForm };
18 changes: 9 additions & 9 deletions src/components/Form/src/componentMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,29 @@ import type { ComponentType } from './types/index';
* Component list, register here to setting it in the form
*/
import {
Input,
Select,
Radio,
Checkbox,
AutoComplete,
Cascader,
Checkbox,
DatePicker,
Divider,
Input,
InputNumber,
Radio,
Rate,
Select,
Slider,
Switch,
TimePicker,
TreeSelect,
Slider,
Rate,
Divider,
} from 'ant-design-vue';

import ApiRadioGroup from './components/ApiRadioGroup.vue';
import RadioButtonGroup from './components/RadioButtonGroup.vue';
import ApiSelect from './components/ApiSelect.vue';
import ApiTree from './components/ApiTree.vue';
import ApiTreeSelect from './components/ApiTreeSelect.vue';
import ApiCascader from './components/ApiCascader.vue';
import ApiTransfer from './components/ApiTransfer.vue';
import ImageUpload from './components/ImageUpload.vue';
import { BasicUpload } from '/@/components/Upload';
import { StrengthMeter } from '/@/components/StrengthMeter';
import { IconPicker } from '/@/components/Icon';
Expand All @@ -42,7 +42,7 @@ componentMap.set('InputSearch', Input.Search);
componentMap.set('InputTextArea', Input.TextArea);
componentMap.set('InputNumber', InputNumber);
componentMap.set('AutoComplete', AutoComplete);

componentMap.set('ImageUpload', ImageUpload);
componentMap.set('Select', Select);
componentMap.set('ApiSelect', ApiSelect);
componentMap.set('ApiTree', ApiTree);
Expand Down
253 changes: 253 additions & 0 deletions src/components/Form/src/components/ImageUpload.vue
Original file line number Diff line number Diff line change
@@ -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>
1 change: 1 addition & 0 deletions src/components/Form/src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,6 @@ export const NO_AUTO_LINK_COMPONENTS: ComponentType[] = [
'ApiCascader',
'AutoComplete',
'RadioButtonGroup',
'ImageUpload',
'ApiSelect',
];
1 change: 1 addition & 0 deletions src/components/Form/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export type ComponentType =
| 'Switch'
| 'StrengthMeter'
| 'Upload'
| 'ImageUpload'
| 'IconPicker'
| 'Render'
| 'Slider'
Expand Down
11 changes: 11 additions & 0 deletions src/views/demo/form/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,17 @@
allowHalf: true,
},
},
{
field: 'field23',
component: 'ImageUpload',
label: '字段23',
colProps: {
span: 8,
},
componentProps: {
api: () => Promise.resolve('https://via.placeholder.com/600/92c952'),
},
},
];

export default defineComponent({
Expand Down