mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-01-24 02:00:25 +08:00
feat(feature):upload->preview & upload->resultfield (#3707)
* feat(components->upload->resultField): upload和imageupload组件中添加resultField与demo * feat(components->upload->preview): upload组件中添加自定义预览组件的方法与demo
This commit is contained in:
parent
b5c87cf6ab
commit
b28a46edcb
@ -34,6 +34,8 @@
|
|||||||
@register="registerPreviewModal"
|
@register="registerPreviewModal"
|
||||||
@list-change="handlePreviewChange"
|
@list-change="handlePreviewChange"
|
||||||
@delete="handlePreviewDelete"
|
@delete="handlePreviewDelete"
|
||||||
|
v-bind:preview-columns="props.previewColumns"
|
||||||
|
v-bind:before-preview-data="props.beforePreviewData"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import { useSortable } from '@/hooks/web/useSortable';
|
import { useSortable } from '@/hooks/web/useSortable';
|
||||||
import { useModalContext } from '@/components/Modal/src/hooks/useModalContext';
|
import { useModalContext } from '@/components/Modal/src/hooks/useModalContext';
|
||||||
import { defineComponent, CSSProperties, watch, nextTick, ref, onMounted } from 'vue';
|
import { defineComponent, CSSProperties, watch, nextTick, ref, onMounted } from 'vue';
|
||||||
|
import { FileBasicColumn } from '../types/typing';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'FileList',
|
name: 'FileList',
|
||||||
@ -51,7 +52,9 @@
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const { columns, actionColumn, dataSource } = props;
|
const { columns, actionColumn, dataSource } = props;
|
||||||
const columnList = [...columns, actionColumn];
|
let columnList: FileBasicColumn[];
|
||||||
|
columnList = (actionColumn ? [...columns, actionColumn] : [...columns]) as FileBasicColumn[];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// x scrollbar
|
// x scrollbar
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
|
@ -37,12 +37,12 @@
|
|||||||
import { uploadContainerProps } from '../props';
|
import { uploadContainerProps } from '../props';
|
||||||
import { isImgTypeByName } from '../helper';
|
import { isImgTypeByName } from '../helper';
|
||||||
import { UploadResultStatus } from '@/components/Upload/src/types/typing';
|
import { UploadResultStatus } from '@/components/Upload/src/types/typing';
|
||||||
|
import { get,omit } from 'lodash-es';
|
||||||
defineOptions({ name: 'ImageUpload' });
|
defineOptions({ name: 'ImageUpload' });
|
||||||
|
|
||||||
const emit = defineEmits(['change', 'update:value', 'delete']);
|
const emit = defineEmits(['change', 'update:value', 'delete']);
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
...uploadContainerProps,
|
...omit(uploadContainerProps,["previewColumns","beforePreviewData"]),
|
||||||
});
|
});
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { createMessage } = useMessage();
|
const { createMessage } = useMessage();
|
||||||
@ -170,7 +170,12 @@
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
filename: props.filename,
|
filename: props.filename,
|
||||||
});
|
});
|
||||||
|
if(props.resultField){
|
||||||
|
info.onSuccess!(res);
|
||||||
|
}else{
|
||||||
|
// 不传入 resultField 的情况
|
||||||
info.onSuccess!(res.data);
|
info.onSuccess!(res.data);
|
||||||
|
}
|
||||||
const value = getValue();
|
const value = getValue();
|
||||||
isInnerOperate.value = true;
|
isInnerOperate.value = true;
|
||||||
emit('update:value', value);
|
emit('update:value', value);
|
||||||
@ -185,6 +190,9 @@
|
|||||||
const list = (fileList.value || [])
|
const list = (fileList.value || [])
|
||||||
.filter((item) => item?.status === UploadResultStatus.DONE)
|
.filter((item) => item?.status === UploadResultStatus.DONE)
|
||||||
.map((item: any) => {
|
.map((item: any) => {
|
||||||
|
if(props.resultField){
|
||||||
|
return get(item?.response, props.resultField)
|
||||||
|
}
|
||||||
return item?.url || item?.response?.url;
|
return item?.url || item?.response?.url;
|
||||||
});
|
});
|
||||||
return props.multiple ? list : list.length > 0 ? list[0] : '';
|
return props.multiple ? list : list.length > 0 ? list[0] : '';
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
import { warn } from '@/utils/log';
|
import { warn } from '@/utils/log';
|
||||||
import FileList from './FileList.vue';
|
import FileList from './FileList.vue';
|
||||||
import { useI18n } from '@/hooks/web/useI18n';
|
import { useI18n } from '@/hooks/web/useI18n';
|
||||||
|
import { get } from 'lodash-es';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
...basicProps,
|
...basicProps,
|
||||||
@ -190,6 +191,14 @@
|
|||||||
const { data } = ret;
|
const { data } = ret;
|
||||||
item.status = UploadResultStatus.SUCCESS;
|
item.status = UploadResultStatus.SUCCESS;
|
||||||
item.response = data;
|
item.response = data;
|
||||||
|
if(props.resultField){
|
||||||
|
// 适配预览组件而进行封装
|
||||||
|
item.response = {
|
||||||
|
code:0,
|
||||||
|
message:"upload Success!",
|
||||||
|
url:get(ret, props.resultField)
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
error: null,
|
error: null,
|
||||||
|
@ -15,30 +15,55 @@
|
|||||||
import FileList from './FileList.vue';
|
import FileList from './FileList.vue';
|
||||||
import { BasicModal, useModalInner } from '@/components/Modal';
|
import { BasicModal, useModalInner } from '@/components/Modal';
|
||||||
import { previewProps } from '../props';
|
import { previewProps } from '../props';
|
||||||
import { PreviewFileItem } from '../types/typing';
|
import { FileBasicColumn, PreviewFileItem } from '../types/typing';
|
||||||
import { downloadByUrl } from '@/utils/file/download';
|
import { downloadByUrl } from '@/utils/file/download';
|
||||||
import { createPreviewColumns, createPreviewActionColumn } from './data';
|
import { createPreviewColumns, createPreviewActionColumn } from './data';
|
||||||
import { useI18n } from '@/hooks/web/useI18n';
|
import { useI18n } from '@/hooks/web/useI18n';
|
||||||
import { isArray } from '@/utils/is';
|
import { isArray } from '@/utils/is';
|
||||||
|
import { BasicColumn } from '@/components/Table';
|
||||||
|
|
||||||
const props = defineProps(previewProps);
|
const props = defineProps(previewProps);
|
||||||
|
|
||||||
const emit = defineEmits(['list-change', 'register', 'delete']);
|
const emit = defineEmits(['list-change', 'register', 'delete']);
|
||||||
|
|
||||||
const columns = createPreviewColumns() as any[];
|
let columns : BasicColumn[] | FileBasicColumn[] = createPreviewColumns();
|
||||||
const actionColumn = createPreviewActionColumn({ handleRemove, handleDownload }) as any;
|
let actionColumn :any;
|
||||||
|
|
||||||
const [register] = useModalInner();
|
const [register] = useModalInner();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const fileListRef = ref<PreviewFileItem[]>([]);
|
const fileListRef = ref<PreviewFileItem[] | Array<any>>([]);
|
||||||
|
watch(
|
||||||
|
() => props.previewColumns,
|
||||||
|
() => {
|
||||||
|
if (props.previewColumns.length) {
|
||||||
|
columns = props.previewColumns;
|
||||||
|
actionColumn = null
|
||||||
|
}else{
|
||||||
|
columns=createPreviewColumns();
|
||||||
|
actionColumn = createPreviewActionColumn({ handleRemove, handleDownload })
|
||||||
|
};
|
||||||
|
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.value,
|
() => props.value,
|
||||||
(value) => {
|
(value) => {
|
||||||
if (!isArray(value)) value = [];
|
if (!isArray(value)) value = [];
|
||||||
|
if(props.beforePreviewData){
|
||||||
|
value = props.beforePreviewData(value) as any
|
||||||
|
fileListRef.value = value
|
||||||
|
return
|
||||||
|
}
|
||||||
fileListRef.value = value
|
fileListRef.value = value
|
||||||
.filter((item) => !!item)
|
.filter((item) => !!item)
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
|
if(typeof item!="string"){
|
||||||
|
console.error("return value should be string")
|
||||||
|
return
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
url: item,
|
url: item,
|
||||||
type: item.split('.').pop() || '',
|
type: item.split('.').pop() || '',
|
||||||
|
@ -4,6 +4,8 @@ import { FileBasicColumn } from './types/typing';
|
|||||||
import type { Options } from 'sortablejs';
|
import type { Options } from 'sortablejs';
|
||||||
|
|
||||||
import { Merge } from '@/utils/types';
|
import { Merge } from '@/utils/types';
|
||||||
|
import { propTypes } from '@/utils/propTypes';
|
||||||
|
import { BasicColumn } from '@/components/Table';
|
||||||
|
|
||||||
type SortableOptions = Merge<
|
type SortableOptions = Merge<
|
||||||
Omit<Options, 'onEnd'>,
|
Omit<Options, 'onEnd'>,
|
||||||
@ -13,6 +15,19 @@ type SortableOptions = Merge<
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export const previewType = {
|
||||||
|
previewColumns:{
|
||||||
|
type: Array as (PropType<BasicColumn[] | FileBasicColumn[]>),
|
||||||
|
default: [],
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
beforePreviewData:{
|
||||||
|
type: Function as PropType<(arg:string[])=>Recordable<any>>,
|
||||||
|
default: null,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
type ListType = 'text' | 'picture' | 'picture-card';
|
type ListType = 'text' | 'picture' | 'picture-card';
|
||||||
|
|
||||||
export const basicProps = {
|
export const basicProps = {
|
||||||
@ -69,11 +84,13 @@ export const basicProps = {
|
|||||||
type: Object as PropType<SortableOptions>,
|
type: Object as PropType<SortableOptions>,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
|
// support xxx.xxx.xx
|
||||||
|
resultField: propTypes.string.def(''),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uploadContainerProps = {
|
export const uploadContainerProps = {
|
||||||
value: {
|
value: {
|
||||||
type: Array as PropType<string[]>,
|
type: Array as (PropType<string[]>),
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
...basicProps,
|
...basicProps,
|
||||||
@ -85,6 +102,7 @@ export const uploadContainerProps = {
|
|||||||
type: Boolean as PropType<boolean>,
|
type: Boolean as PropType<boolean>,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
...previewType
|
||||||
};
|
};
|
||||||
|
|
||||||
export const previewProps = {
|
export const previewProps = {
|
||||||
@ -92,11 +110,12 @@ export const previewProps = {
|
|||||||
type: Array as PropType<string[]>,
|
type: Array as PropType<string[]>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
...previewType
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fileListProps = {
|
export const fileListProps = {
|
||||||
columns: {
|
columns: {
|
||||||
type: Array as PropType<FileBasicColumn[]>,
|
type: Array as (PropType<BasicColumn[] | FileBasicColumn[]> ),
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
actionColumn: {
|
actionColumn: {
|
||||||
|
@ -16,7 +16,7 @@ export interface FileItem {
|
|||||||
percent: number;
|
percent: number;
|
||||||
file: File;
|
file: File;
|
||||||
status?: UploadResultStatus;
|
status?: UploadResultStatus;
|
||||||
response?: UploadApiResult;
|
response?: UploadApiResult | Recordable<any>;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,14 @@
|
|||||||
|
|
||||||
<Alert message="嵌入表单,加入表单校验" />
|
<Alert message="嵌入表单,加入表单校验" />
|
||||||
|
|
||||||
<BasicForm @register="register" class="my-5" />
|
<BasicForm @register="registerValiate" class="my-5" />
|
||||||
|
|
||||||
|
<Alert message="嵌入表单,加入resultFiled自定义返回值" />
|
||||||
|
<BasicForm @register="registerCustom" class="my-5" />
|
||||||
|
|
||||||
|
<Alert message="嵌入表单,自定义预览内容" />
|
||||||
|
<BasicForm @register="registerPreview" class="my-5" />
|
||||||
|
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@ -20,33 +27,193 @@
|
|||||||
import { useMessage } from '@/hooks/web/useMessage';
|
import { useMessage } from '@/hooks/web/useMessage';
|
||||||
import { BasicForm, FormSchema, useForm } from '@/components/Form';
|
import { BasicForm, FormSchema, useForm } from '@/components/Form';
|
||||||
import { PageWrapper } from '@/components/Page';
|
import { PageWrapper } from '@/components/Page';
|
||||||
import { Alert } from 'ant-design-vue';
|
import { Alert,Button } from 'ant-design-vue';
|
||||||
import { uploadApi } from '@/api/sys/upload';
|
import { uploadApi } from '@/api/sys/upload';
|
||||||
|
import { createVNode } from "vue"
|
||||||
|
|
||||||
const schemas: FormSchema[] = [
|
const schemasValiate: FormSchema[] = [
|
||||||
{
|
{
|
||||||
field: 'field1',
|
field: 'field1',
|
||||||
component: 'Upload',
|
component: 'Upload',
|
||||||
label: '字段1',
|
label: '字段1',
|
||||||
colProps: {
|
|
||||||
span: 8,
|
|
||||||
},
|
|
||||||
rules: [{ required: true, message: '请选择上传文件' }],
|
rules: [{ required: true, message: '请选择上传文件' }],
|
||||||
componentProps: {
|
componentProps: {
|
||||||
api: uploadApi,
|
api: uploadApi,
|
||||||
},
|
},
|
||||||
|
},{
|
||||||
|
field: 'field2',
|
||||||
|
component: "ImageUpload",
|
||||||
|
label: '字段2(ImageUpload)',
|
||||||
|
colProps: {
|
||||||
|
span: 8,
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
api: uploadApi,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const { createMessage } = useMessage();
|
const schemasCustom: FormSchema[] = [
|
||||||
const [register] = useForm({
|
{
|
||||||
labelWidth: 120,
|
field: 'field3',
|
||||||
schemas,
|
component: 'Upload',
|
||||||
actionColOptions: {
|
label: '字段3',
|
||||||
span: 16,
|
componentProps: {
|
||||||
|
resultField:"data3.url",
|
||||||
|
api: (file,progress)=>{
|
||||||
|
return new Promise((resolve)=>{
|
||||||
|
uploadApi(file,progress).then((uploadApiResponse)=>{
|
||||||
|
resolve({
|
||||||
|
code:200,
|
||||||
|
data3:{
|
||||||
|
url:uploadApiResponse.data.url
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'field4',
|
||||||
|
component: "ImageUpload",
|
||||||
|
label: '字段4(ImageUpload)',
|
||||||
|
colProps: {
|
||||||
|
span: 8,
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
resultField:"data4.url",
|
||||||
|
api: (file,progress)=>{
|
||||||
|
return new Promise((resolve)=>{
|
||||||
|
uploadApi(file,progress).then((uploadApiResponse)=>{
|
||||||
|
resolve({
|
||||||
|
code:200,
|
||||||
|
data4:{
|
||||||
|
url:uploadApiResponse.data.url
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const schemasPreview: FormSchema[] = [
|
||||||
|
{
|
||||||
|
field: 'field5',
|
||||||
|
component: 'Upload',
|
||||||
|
label: '字段5',
|
||||||
|
componentProps: {
|
||||||
|
previewColumns:[{
|
||||||
|
title:"url5",
|
||||||
|
dataIndex:"url5"
|
||||||
|
},{
|
||||||
|
title:"type5",
|
||||||
|
dataIndex:"type5"
|
||||||
|
},{
|
||||||
|
title:"name5",
|
||||||
|
dataIndex:"name5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title:"operation",
|
||||||
|
dataIndex:"",
|
||||||
|
customRender: ({ record })=>{
|
||||||
|
return createVNode(Button,{
|
||||||
|
onclick:()=>{
|
||||||
|
console.log(record)
|
||||||
|
createMessage.success(`请到控制台查看该行输出结果`);
|
||||||
|
}
|
||||||
|
},"点我")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
beforePreviewData:(arg)=>{
|
||||||
|
let data = arg.filter((item) => !!item).map((item) => {
|
||||||
|
if(typeof item !== "string"){
|
||||||
|
console.error("return value should be string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
url5: item,
|
||||||
|
type5: item.split('.').pop() || '',
|
||||||
|
name5: item.split('/').pop() || '',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
resultField:"data5.url",
|
||||||
|
api: (file,progress)=>{
|
||||||
|
return new Promise((resolve)=>{
|
||||||
|
uploadApi(file,progress).then((uploadApiResponse)=>{
|
||||||
|
resolve({
|
||||||
|
code:200,
|
||||||
|
data5:{
|
||||||
|
url:uploadApiResponse.data.url
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
|
||||||
function handleChange(list: string[]) {
|
function handleChange(list: string[]) {
|
||||||
createMessage.info(`已上传文件${JSON.stringify(list)}`);
|
createMessage.success(`已上传文件${JSON.stringify(list)}`);
|
||||||
}
|
}
|
||||||
|
const [registerValiate,{getFieldsValue:getFieldsValueValiate,validate}] = useForm({
|
||||||
|
labelWidth: 160,
|
||||||
|
schemas:schemasValiate,
|
||||||
|
actionColOptions: {
|
||||||
|
span: 18,
|
||||||
|
},
|
||||||
|
submitFunc:()=>{
|
||||||
|
return new Promise((resolve)=>{
|
||||||
|
validate().then((e)=>{
|
||||||
|
resolve()
|
||||||
|
console.log(getFieldsValueValiate())
|
||||||
|
createMessage.success(`请到控制台查看结果`);
|
||||||
|
}).catch(()=>{
|
||||||
|
createMessage.error(`请输入必填项`);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// resultFields 字段示例
|
||||||
|
const [registerCustom,{getFieldsValue:getFieldsValueCustom}] = useForm({
|
||||||
|
labelWidth: 160,
|
||||||
|
schemas:schemasCustom,
|
||||||
|
actionColOptions: {
|
||||||
|
span: 18,
|
||||||
|
},
|
||||||
|
submitFunc:()=>{
|
||||||
|
return new Promise((resolve)=>{
|
||||||
|
console.log(getFieldsValueCustom())
|
||||||
|
resolve()
|
||||||
|
createMessage.success(`请到控制台查看结果`);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// registerPreview
|
||||||
|
const [registerPreview,{getFieldsValue:getFieldsValuePreview}] = useForm({
|
||||||
|
labelWidth: 160,
|
||||||
|
schemas:schemasPreview,
|
||||||
|
actionColOptions: {
|
||||||
|
span: 18,
|
||||||
|
},
|
||||||
|
submitFunc:()=>{
|
||||||
|
return new Promise((resolve)=>{
|
||||||
|
console.log(getFieldsValuePreview())
|
||||||
|
resolve()
|
||||||
|
createMessage.success(`请到控制台查看结果`);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
Reference in New Issue
Block a user