feat: 合同管理上传附件模块
This commit is contained in:
parent
434b634375
commit
3ff2e732b2
2
.env
2
.env
|
@ -1,5 +1,5 @@
|
||||||
# 项目名称
|
# 项目名称
|
||||||
VITE_APP_TITLE = Admin
|
VITE_APP_TITLE = 华信OA
|
||||||
|
|
||||||
# 网站前缀
|
# 网站前缀
|
||||||
VITE_BASE_URL = /
|
VITE_BASE_URL = /
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<template>
|
||||||
|
<div>test</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({
|
||||||
|
name: 'FileManage',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
|
@ -10,17 +10,17 @@ export const baseColumns = (ctx: { contractTypes: API.DictItemEntity[] }): Table
|
||||||
const { contractTypes } = ctx;
|
const { contractTypes } = ctx;
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: '合同编号',
|
title: '编号',
|
||||||
width: 120,
|
width: 120,
|
||||||
dataIndex: 'contractNumber',
|
dataIndex: 'contractNumber',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '合同标题',
|
title: '标题',
|
||||||
width: 200,
|
width: 200,
|
||||||
dataIndex: 'title',
|
dataIndex: 'title',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '合同类型',
|
title: '类型',
|
||||||
width: 80,
|
width: 80,
|
||||||
formItemProps: {
|
formItemProps: {
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
|
|
|
@ -20,29 +20,34 @@
|
||||||
</template>
|
</template>
|
||||||
</DynamicTable>
|
</DynamicTable>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <UploadModal v-if="isUploadPopupVisiable" :visiable="isUploadPopupVisiable"></UploadModal> -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="tsx">
|
||||||
import { useTable } from '@/components/core/dynamic-table';
|
import { useTable } from '@/components/core/dynamic-table';
|
||||||
import { baseColumns, type TableColumnItem, type TableListItem } from './columns';
|
import { baseColumns, type TableColumnItem, type TableListItem } from './columns';
|
||||||
import Api from '@/api/';
|
import Api from '@/api/';
|
||||||
import { useDictStore } from '@/store/modules/dict';
|
import { useDictStore } from '@/store/modules/dict';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { DictEnum } from '@/enums/dictEnum';
|
import { DictEnum } from '@/enums/dictEnum';
|
||||||
import { useFormModal } from '@/hooks/useModal';
|
import { useFormModal, useModal } from '@/hooks/useModal';
|
||||||
import { contractSchemas } from './formSchemas';
|
import { contractSchemas } from './formSchemas';
|
||||||
import { formatToDate } from '@/utils/dateUtil';
|
import { formatToDate } from '@/utils/dateUtil';
|
||||||
|
import UploadContract from './upload-contract.vue';
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'Contract',
|
name: 'Contract',
|
||||||
});
|
});
|
||||||
const [DynamicTable, dynamicTableInstance] = useTable();
|
const [DynamicTable, dynamicTableInstance] = useTable();
|
||||||
const [showModal] = useFormModal();
|
const [showModal] = useFormModal();
|
||||||
|
const [fnModal] = useModal();
|
||||||
const { getDictItemsByCode } = useDictStore();
|
const { getDictItemsByCode } = useDictStore();
|
||||||
const contractTypes = ref<API.DictItemEntity[]>([]);
|
const contractTypes = ref<API.DictItemEntity[]>([]);
|
||||||
|
|
||||||
const getContractTypes = async () => {
|
const getContractTypes = async () => {
|
||||||
contractTypes.value = await getDictItemsByCode(DictEnum.ContractType);
|
contractTypes.value = await getDictItemsByCode(DictEnum.ContractType);
|
||||||
};
|
};
|
||||||
|
const isUploadPopupVisiable = ref(false);
|
||||||
|
|
||||||
// contractList;
|
// contractList;
|
||||||
let columns = ref<TableColumnItem[]>();
|
let columns = ref<TableColumnItem[]>();
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -80,12 +85,37 @@ import { formatToDate } from '@/utils/dateUtil';
|
||||||
onConfirm: () => delRowConfirm(record.id),
|
onConfirm: () => delRowConfirm(record.id),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'ant-design:cloud-server-outlined',
|
||||||
|
tooltip: '上传下载附件',
|
||||||
|
onClick: () => openFileManageModal(record),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const openFileManageModal = async (record: API.ContractEntity) => {
|
||||||
|
// UseModalComp.show({
|
||||||
|
// title: '文件管理',
|
||||||
|
// content: FileManage,
|
||||||
|
// });
|
||||||
|
isUploadPopupVisiable.value = true;
|
||||||
|
fnModal.show({
|
||||||
|
width: 800,
|
||||||
|
title: `合同编号: ${record.contractNumber}`,
|
||||||
|
content: () => {
|
||||||
|
return <UploadContract onClose={handleUploadClose}></UploadContract>;
|
||||||
|
},
|
||||||
|
footer: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleUploadClose = (hasSuccess: boolean) => {
|
||||||
|
fnModal.hide();
|
||||||
|
// 在这里你可以关闭模态框
|
||||||
|
// fnModal.hide();
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* @description 打开新增/编辑弹窗
|
* @description 打开新增/编辑弹窗
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
import { Tag, Tooltip, Image, Progress } from 'ant-design-vue';
|
||||||
|
import type { TableColumn } from '@/components/core/dynamic-table';
|
||||||
|
import type { FormSchema } from '@/components/core/schema-form';
|
||||||
|
import { formatToDateTime } from '@/utils/dateUtil';
|
||||||
|
import { baseApiUrl } from '@/utils/request';
|
||||||
|
|
||||||
|
export type TableListItem = API.StorageInfo;
|
||||||
|
export type TableColumnItem = TableColumn<TableListItem>;
|
||||||
|
|
||||||
|
export type FileItem = {
|
||||||
|
file: File;
|
||||||
|
uid: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
status: string;
|
||||||
|
thumbUrl: string;
|
||||||
|
percent: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum UploadResultStatus {
|
||||||
|
SUCCESS = 'success',
|
||||||
|
ERROR = 'error',
|
||||||
|
UPLOADING = 'uploading',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const baseColumns: TableColumnItem[] = [
|
||||||
|
{
|
||||||
|
title: '文件名',
|
||||||
|
dataIndex: 'name',
|
||||||
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
|
customRender({ record }) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
{{
|
||||||
|
title: () => record.path,
|
||||||
|
default: () => (
|
||||||
|
<a href={baseApiUrl + record.path} target="_blank">
|
||||||
|
{record.name}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '预览图',
|
||||||
|
dataIndex: 'path',
|
||||||
|
width: 150,
|
||||||
|
customRender({ record }) {
|
||||||
|
return <Image src={baseApiUrl + record.path}></Image>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '文件后缀',
|
||||||
|
dataIndex: 'extName',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '类别',
|
||||||
|
dataIndex: 'type',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '大小',
|
||||||
|
dataIndex: 'size',
|
||||||
|
width: 80,
|
||||||
|
customRender: ({ record }) => {
|
||||||
|
return <Tag color="blue">{record.size}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '上传者',
|
||||||
|
dataIndex: 'username',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
width: 160,
|
||||||
|
customRender: ({ record }) => formatToDateTime(record.createdAt),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const fileListColumns: TableColumn<FileItem>[] = [
|
||||||
|
{
|
||||||
|
dataIndex: 'thumbUrl',
|
||||||
|
title: '缩略图',
|
||||||
|
width: 100,
|
||||||
|
customRender: ({ record }) => {
|
||||||
|
const { thumbUrl } = record;
|
||||||
|
return thumbUrl && <Image src={thumbUrl} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'name',
|
||||||
|
title: '文件名',
|
||||||
|
align: 'left',
|
||||||
|
customRender: ({ text, record }) => {
|
||||||
|
const { percent, status: uploadStatus } = record || {};
|
||||||
|
let status: 'normal' | 'exception' | 'active' | 'success' = 'normal';
|
||||||
|
if (uploadStatus === UploadResultStatus.ERROR) {
|
||||||
|
status = 'exception';
|
||||||
|
} else if (uploadStatus === UploadResultStatus.UPLOADING) {
|
||||||
|
status = 'active';
|
||||||
|
} else if (uploadStatus === UploadResultStatus.SUCCESS) {
|
||||||
|
status = 'success';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p class="truncate mb-1 max-w-[280px]" title={text}>
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
<Progress percent={percent} size="small" status={status} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'size',
|
||||||
|
title: '文件大小',
|
||||||
|
width: 100,
|
||||||
|
customRender: ({ text = 0 }) => {
|
||||||
|
return text && `${(text / 1024).toFixed(2)}KB`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataIndex: 'status',
|
||||||
|
title: '状态',
|
||||||
|
width: 100,
|
||||||
|
customRender: ({ text }) => {
|
||||||
|
if (text === UploadResultStatus.SUCCESS) {
|
||||||
|
return <Tag color="green">上传成功</Tag>;
|
||||||
|
} else if (text === UploadResultStatus.ERROR) {
|
||||||
|
return <Tag color="red">上传失败</Tag>;
|
||||||
|
} else if (text === UploadResultStatus.UPLOADING) {
|
||||||
|
return <Tag color="blue">上传中</Tag>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text || '待上传';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const searchFormSchema: FormSchema[] = [
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
label: '名称',
|
||||||
|
component: 'Input',
|
||||||
|
colProps: { span: 8 },
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,119 @@
|
||||||
|
<template>
|
||||||
|
<a-flex justify="space-between" align="center">
|
||||||
|
<a-alert message="单个文件不超过5MB,最多只能上传10个文件" type="info" show-icon />
|
||||||
|
<a-upload :multiple="true" :before-upload="beforeUpload" :show-upload-list="false">
|
||||||
|
<a-button type="primary"> 选择文件 </a-button>
|
||||||
|
</a-upload>
|
||||||
|
</a-flex>
|
||||||
|
|
||||||
|
<DynamicTable :search="false" :data-source="fileList" :columns="columns" />
|
||||||
|
|
||||||
|
<a-flex justify="flex-end" gap="10">
|
||||||
|
<Button @click="onCancel">取消</Button>
|
||||||
|
<Button :type="'primary'" @click="onOk" :disabled="disabledUpload">上传</Button></a-flex
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { message, type UploadProps } from 'ant-design-vue';
|
||||||
|
import { UploadResultStatus, fileListColumns, type FileItem } from './upload-columns';
|
||||||
|
import { useTable, type TableColumn } from '@/components/core/dynamic-table';
|
||||||
|
import Button from '@/components/basic/button';
|
||||||
|
import Api from '@/api/';
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const [DynamicTable] = useTable();
|
||||||
|
|
||||||
|
const fileList = ref<FileItem[]>([]);
|
||||||
|
|
||||||
|
const disabledUpload = computed(() => {
|
||||||
|
return !fileList.value.some((n) => n.status !== UploadResultStatus.SUCCESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileToBase64 = (file: File): Promise<string> => {
|
||||||
|
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = () => {
|
||||||
|
resolve(reader.result as string);
|
||||||
|
};
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
const hasSuccess = fileList.value.some((n) => n.status === UploadResultStatus.SUCCESS);
|
||||||
|
emit('close', hasSuccess);
|
||||||
|
fileList.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOk = async () => {
|
||||||
|
const uploadFileList = fileList.value.filter((n) => n.status !== UploadResultStatus.SUCCESS);
|
||||||
|
await Promise.all(
|
||||||
|
uploadFileList.map(async (item) => {
|
||||||
|
try {
|
||||||
|
await Api.toolsUpload.uploadUpload({ file: item.file }, undefined, {
|
||||||
|
onUploadProgress(progressEvent) {
|
||||||
|
const complete = ((progressEvent.loaded / progressEvent.total!) * 100) | 0;
|
||||||
|
item.percent = complete;
|
||||||
|
item.status = UploadResultStatus.UPLOADING;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
item.status = UploadResultStatus.SUCCESS;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
item.status = UploadResultStatus.ERROR;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
|
||||||
|
if (file.size / 1024 / 1024 > 5) {
|
||||||
|
message.error('单个文件不超过5MB');
|
||||||
|
} else {
|
||||||
|
const item: FileItem = {
|
||||||
|
file,
|
||||||
|
uid: file.uid,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
status: '',
|
||||||
|
percent: 0,
|
||||||
|
thumbUrl: await fileToBase64(file),
|
||||||
|
};
|
||||||
|
fileList.value.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (record: FileItem) => {
|
||||||
|
fileList.value = fileList.value.filter((n) => n.uid !== record.uid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: TableColumn<FileItem>[] = [
|
||||||
|
...fileListColumns,
|
||||||
|
{
|
||||||
|
width: 120,
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'ACTION',
|
||||||
|
fixed: false,
|
||||||
|
actions: ({ record }) => [
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
color: 'red',
|
||||||
|
onClick: () => handleRemove(record),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
Loading…
Reference in New Issue