feat: 合同删除附件,附件管理模块封装

This commit is contained in:
louis 2024-03-01 15:23:54 +08:00
parent 3ff2e732b2
commit 1392cf5441
9 changed files with 383 additions and 53 deletions

View File

@ -51,6 +51,25 @@ export async function contractInfo(
}); });
} }
/** 解除合同和附件关联 PUT /api/contract/unlink-attachments/${param0} */
export async function unlinkAttachments(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.ContractUpdateParams,
body: API.ContractUpdateDto,
options?: RequestOptions,
) {
const { id: param0, ...queryParams } = params;
return request<any>(`/api/contract/unlink-attachments/${param0}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...options,
});
}
/** 更新合同 PUT /api/contract/${param0} */ /** 更新合同 PUT /api/contract/${param0} */
export async function contractUpdate( export async function contractUpdate(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)

View File

@ -930,19 +930,20 @@ declare namespace API {
page?: number; page?: number;
pageSize?: number; pageSize?: number;
field?: string; field?: string;
ids?: string;
order?: 'ASC' | 'DESC'; order?: 'ASC' | 'DESC';
/** 文件名 */ /** 文件名 */
name: string; name?: string;
/** 文件后缀 */ /** 文件后缀 */
extName: string; extName?: string;
/** 文件类型 */ /** 文件类型 */
type: string; type?: string;
/** 大小 */ /** 大小 */
size: string; size?: string;
/** 上传时间 */ /** 上传时间 */
time: string[]; time?: string[];
/** 上传者 */ /** 上传者 */
username: string; username?: string;
_t?: number; _t?: number;
}; };
@ -1293,6 +1294,9 @@ declare namespace API {
deliveryDeadline: Date; deliveryDeadline: Date;
/** 审核状态(字典) */ /** 审核状态(字典) */
status: number; status: number;
/** 附件 */
files?: any[];
id: number; id: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@ -1315,28 +1319,30 @@ declare namespace API {
/** 审核状态(字典) */ /** 审核状态(字典) */
status: number; status: number;
}; };
type ContractUpdateParams = { type ContractUpdateParams = {
id: number; id: number;
}; };
type ContractUpdateDto = { type ContractUpdateDto = {
/** 合同编号 */ /** 合同编号 */
contractNumber: string; contractNumber?: string;
/** 合同标题 */ /** 合同标题 */
title: string; title?: string;
/** 合同类型(字典) */ /** 合同类型(字典) */
type: number; type?: number;
/** 甲方 */ /** 甲方 */
partyA: string; partyA?: string;
/** 乙方 */ /** 乙方 */
partyB: string; partyB?: string;
/** 签订日期 */ /** 签订日期 */
signingDate: string; signingDate?: string;
/** 交付期限 */ /** 交付期限 */
deliveryDeadline: string; deliveryDeadline?: string;
/** 审核状态(字典) */ /** 审核状态(字典) */
status: number; status?: number;
/** 附件 */
fileIds?: number[];
}; };
type ContractDeleteParams = { type ContractDeleteParams = {

View File

@ -1,3 +1,5 @@
### 业务组件(目录说明) ### 业务组件(目录说明)
1.AttachmentManage不同的业务调用该组件需要传入该业务数据link的文件ids需要传入unlink文件的方法只是解除关联文件实际还在服务器上没有删除如果想要物理删除文件需要到存储管理中去删除。
#### 与业务强耦合的组件可以放这里 #### 与业务强耦合的组件可以放这里

View File

@ -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 },
},
];

View File

@ -0,0 +1,83 @@
<template>
<div>
<DynamicTable
row-key="id"
header-title="附件管理"
:data-request="loadTableData"
:columns="baseColumns"
bordered
size="small"
>
<template #toolbar>
<a-popconfirm
title="你确定要删除这些附件吗?"
ok-text="确定"
cancel-text="取消"
@confirm="handleDelete"
>
<a-button :disabled="!$auth('tool:storage:delete') || !checkedKeys.length" type="error">
删除
</a-button>
</a-popconfirm>
</template>
</DynamicTable>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { baseColumns, searchFormSchema } from './columns';
import { useTable, type LoadDataParams } from '@/components/core/dynamic-table';
import Api from '@/api/';
defineOptions({
name: 'AttachmentManage',
});
const props = defineProps({
fileIds: {
type: Array as PropType<number[]>,
},
onDelete: { type: Function as PropType<(ids: number[]) => Promise<void>>, default: () => {} },
});
const checkedKeys = ref<Array<number>>([]);
const loadTableData = async (params: LoadDataParams) => {
const data = await Api.toolsStorage.storageList({
...params,
ids: (props.fileIds || []).join(','),
});
return data;
};
const [DynamicTable, dynamicTableInstance] = useTable({
formProps: {
schemas: searchFormSchema,
},
rowSelection: {
type: 'checkbox',
selectedRowKeys: checkedKeys as unknown as Key[],
onSelect: (record, selected) => {
if (selected) {
checkedKeys.value = [...checkedKeys.value, record.id];
} else {
checkedKeys.value = checkedKeys.value.filter((id) => id !== record.id);
}
},
onSelectAll: (selected, selectedRows, changeRows) => {
const changeIds = changeRows.map((item) => item.id);
if (selected) {
checkedKeys.value = [...checkedKeys.value, ...changeIds];
} else {
checkedKeys.value = checkedKeys.value.filter((id) => {
return !changeIds.includes(id);
});
}
},
},
});
const handleDelete = async () => {
await props.onDelete?.(checkedKeys.value); // unlink
await Api.toolsStorage.storageDelete({ ids: checkedKeys.value });
checkedKeys.value = [];
dynamicTableInstance?.reload();
};
</script>

View File

@ -1,11 +0,0 @@
<template>
<div>test</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'FileManage',
});
</script>
<style lang="less" scoped></style>

View File

@ -1,22 +1,27 @@
import type { TableColumn } from '@/components/core/dynamic-table'; import type { TableColumn } from '@/components/core/dynamic-table';
import { ContractStatusEnum } from '@/enums/contractEnum'; import { ContractStatusEnum } from '@/enums/contractEnum';
import { formatToDate } from '@/utils/dateUtil'; import { formatToDate } from '@/utils/dateUtil';
import { Tag } from 'ant-design-vue'; import { Tag, Button } from 'ant-design-vue';
export type TableListItem = API.ContractEntity; export type TableListItem = API.ContractEntity;
export type TableColumnItem = TableColumn<TableListItem>; export type TableColumnItem = TableColumn<TableListItem>;
export const baseColumns = (ctx: { contractTypes: API.DictItemEntity[] }): TableColumnItem[] => { export const baseColumns = (ctx: {
contractTypes: API.DictItemEntity[];
dynamicTableInstance;
}): TableColumnItem[] => {
const { contractTypes } = ctx; const { contractTypes } = ctx;
return [ return [
{ {
title: '编号', title: '编号',
width: 120, width: 100,
maxWidth: 100,
fixed: 'left',
dataIndex: 'contractNumber', dataIndex: 'contractNumber',
}, },
{ {
title: '标题', title: '标题',
width: 200, width: 180,
dataIndex: 'title', dataIndex: 'title',
}, },
{ {
@ -37,17 +42,18 @@ export const baseColumns = (ctx: { contractTypes: API.DictItemEntity[] }): Table
}, },
{ {
title: '甲方', title: '甲方',
width: 150, width: 120,
dataIndex: 'partyA', dataIndex: 'partyA',
}, },
{ {
title: '乙方', title: '乙方',
width: 150, width: 120,
dataIndex: 'partyB', dataIndex: 'partyB',
}, },
{ {
title: '签订时间', title: '签订时间',
width: 100, width: 60,
maxWidth: 60,
hideInSearch: true, hideInSearch: true,
dataIndex: 'signingDate', dataIndex: 'signingDate',
customRender: ({ record }) => { customRender: ({ record }) => {
@ -56,7 +62,8 @@ export const baseColumns = (ctx: { contractTypes: API.DictItemEntity[] }): Table
}, },
{ {
title: '交付期限', title: '交付期限',
width: 100, width: 60,
maxWidth: 60,
hideInSearch: true, hideInSearch: true,
dataIndex: 'deliveryDeadline', dataIndex: 'deliveryDeadline',
customRender: ({ record }) => { customRender: ({ record }) => {
@ -68,12 +75,13 @@ export const baseColumns = (ctx: { contractTypes: API.DictItemEntity[] }): Table
dataIndex: 'status', dataIndex: 'status',
maxWidth: 60, maxWidth: 60,
width: 60, width: 60,
fixed:'right',
formItemProps: { formItemProps: {
component: 'Select', component: 'Select',
componentProps: { componentProps: {
options: Object.values(ContractStatusEnum) options: Object.values(ContractStatusEnum)
.filter((value) => typeof value === 'number') .filter((value) => typeof value === 'number')
.map((item) => formatStatus(item as ContractStatusEnum)), .map((item) => formatStatus(item as ContractStatusEnum)),
}, },
}, },
customRender: ({ record }) => { customRender: ({ record }) => {
@ -81,6 +89,7 @@ export const baseColumns = (ctx: { contractTypes: API.DictItemEntity[] }): Table
return <Tag color={color}>{label}</Tag>; return <Tag color={color}>{label}</Tag>;
}, },
}, },
]; ];
}; };

View File

@ -7,6 +7,7 @@
:data-request="Api.contract.contractList" :data-request="Api.contract.contractList"
:columns="columns" :columns="columns"
bordered bordered
:scroll="{ x: 1920 }"
size="small" size="small"
> >
<template #toolbar> <template #toolbar>
@ -20,7 +21,6 @@
</template> </template>
</DynamicTable> </DynamicTable>
</div> </div>
<!-- <UploadModal v-if="isUploadPopupVisiable" :visiable="isUploadPopupVisiable"></UploadModal> -->
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
@ -28,12 +28,14 @@
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, type FunctionalComponent } from 'vue';
import { DictEnum } from '@/enums/dictEnum'; import { DictEnum } from '@/enums/dictEnum';
import { useFormModal, useModal } 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'; import UploadContract from './upload-contract.vue';
import { Button } from 'ant-design-vue';
import AttachmentManage from '@/components/business/attachment-manage/index.vue';
defineOptions({ defineOptions({
name: 'Contract', name: 'Contract',
}); });
@ -54,13 +56,23 @@
getContractTypes().then((res) => { getContractTypes().then((res) => {
columns.value = [ columns.value = [
...baseColumns({ ...baseColumns({
dynamicTableInstance,
contractTypes: contractTypes.value, contractTypes: contractTypes.value,
}), }),
{
title: '附件',
width: 50,
maxWidth: 50,
hideInSearch: true,
fixed:'right',
dataIndex: 'files',
customRender: ({ record }) => <FilesRender {...record} />,
},
{ {
title: '操作', title: '操作',
maxWidth: 100, maxWidth: 80,
width: 100, width: 80,
minWidth: 100, minWidth: 80,
dataIndex: 'ACTION', dataIndex: 'ACTION',
hideInSearch: true, hideInSearch: true,
fixed: 'right', fixed: 'right',
@ -86,8 +98,8 @@
}, },
}, },
{ {
icon: 'ant-design:cloud-server-outlined', icon: 'ant-design:cloud-upload-outlined',
tooltip: '上传下载附件', tooltip: '上传附件',
onClick: () => openFileManageModal(record), onClick: () => openFileManageModal(record),
}, },
], ],
@ -97,25 +109,37 @@
}); });
const openFileManageModal = async (record: API.ContractEntity) => { const openFileManageModal = async (record: API.ContractEntity) => {
// UseModalComp.show({
// title: '',
// content: FileManage,
// });
isUploadPopupVisiable.value = true; isUploadPopupVisiable.value = true;
fnModal.show({ fnModal.show({
width: 800, width: 800,
title: `合同编号: ${record.contractNumber}`, title: `合同编号: ${record.contractNumber}`,
content: () => { content: () => {
return <UploadContract onClose={handleUploadClose}></UploadContract>; return (
<UploadContract
onClose={handleUploadClose}
afterUploadCallback={(files) => {
afterUploadCallback(files, record.id);
}}
></UploadContract>
);
}, },
destroyOnClose: true,
open: isUploadPopupVisiable.value,
footer: null, footer: null,
}); });
}; };
const handleUploadClose = (hasSuccess: boolean) => { const handleUploadClose = (hasSuccess: boolean) => {
fnModal.hide(); fnModal.hide();
// isUploadPopupVisiable.value = false;
// fnModal.hide();
}; };
const afterUploadCallback = async (
files: { filename: { path: string; id: number } }[],
id: number,
) => {
await Api.contract.contractUpdate({ id }, { fileIds: files.map((item) => item.filename.id) });
dynamicTableInstance?.reload();
};
/** /**
* @description 打开新增/编辑弹窗 * @description 打开新增/编辑弹窗
*/ */
@ -153,6 +177,42 @@
} }
}; };
function delRowConfirm(record) {} function delRowConfirm(record) {}
const FilesRender: FunctionalComponent<TableListItem> = (contract: API.ContractEntity) => {
const [fnModal] = useModal();
return (
<Button
type="link"
onClick={() => {
openFilesManageModal(fnModal, contract);
}}
>
{contract.files?.length || 0}
</Button>
);
};
const openFilesManageModal = (fnModal, contract: API.ContractEntity) => {
const fileIds = contract.files?.map((item) => item.id) || [];
fnModal.show({
width: 1200,
title: `附件管理`,
content: () => {
return (
<AttachmentManage
fileIds={fileIds}
onDelete={(unlinkIds) => unlinkAttachments(contract.id, unlinkIds)}
></AttachmentManage>
);
},
destroyOnClose: true,
footer: null,
});
};
const unlinkAttachments = async (id: number, unlinkIds: number[]) => {
await Api.contract.unlinkAttachments({ id }, { fileIds: unlinkIds });
dynamicTableInstance?.reload();
};
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -23,7 +23,12 @@
import Api from '@/api/'; import Api from '@/api/';
const emit = defineEmits(['close']); const emit = defineEmits(['close']);
const { afterUploadCallback } = defineProps({
afterUploadCallback: {
type: Function,
default: () => ({}),
},
});
const [DynamicTable] = useTable(); const [DynamicTable] = useTable();
const fileList = ref<FileItem[]>([]); const fileList = ref<FileItem[]>([]);
@ -56,10 +61,10 @@
const onOk = async () => { const onOk = async () => {
const uploadFileList = fileList.value.filter((n) => n.status !== UploadResultStatus.SUCCESS); const uploadFileList = fileList.value.filter((n) => n.status !== UploadResultStatus.SUCCESS);
await Promise.all( const res = await Promise.all(
uploadFileList.map(async (item) => { uploadFileList.map(async (item) => {
try { try {
await Api.toolsUpload.uploadUpload({ file: item.file }, undefined, { const itemRes = await Api.toolsUpload.uploadUpload({ file: item.file }, undefined, {
onUploadProgress(progressEvent) { onUploadProgress(progressEvent) {
const complete = ((progressEvent.loaded / progressEvent.total!) * 100) | 0; const complete = ((progressEvent.loaded / progressEvent.total!) * 100) | 0;
item.percent = complete; item.percent = complete;
@ -67,12 +72,16 @@
}, },
}); });
item.status = UploadResultStatus.SUCCESS; item.status = UploadResultStatus.SUCCESS;
return itemRes;
} catch (error) { } catch (error) {
console.log(error); console.log(error);
item.status = UploadResultStatus.ERROR; item.status = UploadResultStatus.ERROR;
} }
}), }),
); );
message.success({ content: '上传完成', key: '上传完成' });
afterUploadCallback && afterUploadCallback(res.filter((item) => !!item));
console.log(fileList.value.filter((n) => n.status === UploadResultStatus.SUCCESS));
}; };
const beforeUpload: UploadProps['beforeUpload'] = async (file) => { const beforeUpload: UploadProps['beforeUpload'] = async (file) => {