feat: 合同删除附件,附件管理模块封装
This commit is contained in:
parent
3ff2e732b2
commit
1392cf5441
|
@ -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} */
|
||||
export async function contractUpdate(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
|
|
|
@ -930,19 +930,20 @@ declare namespace API {
|
|||
page?: number;
|
||||
pageSize?: number;
|
||||
field?: string;
|
||||
ids?: string;
|
||||
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;
|
||||
};
|
||||
|
||||
|
@ -1293,6 +1294,9 @@ declare namespace API {
|
|||
deliveryDeadline: Date;
|
||||
/** 审核状态(字典) */
|
||||
status: number;
|
||||
|
||||
/** 附件 */
|
||||
files?: any[];
|
||||
id: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
@ -1315,28 +1319,30 @@ declare namespace API {
|
|||
/** 审核状态(字典) */
|
||||
status: number;
|
||||
};
|
||||
|
||||
|
||||
type ContractUpdateParams = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
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 = {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
### 业务组件(目录说明)
|
||||
|
||||
1.AttachmentManage不同的业务调用该组件需要传入该业务数据link的文件ids,需要传入unlink文件的方法,只是解除关联,文件实际还在服务器上没有删除,如果想要物理删除文件,需要到存储管理中去删除。
|
||||
|
||||
#### 与业务强耦合的组件可以放这里
|
||||
|
|
|
@ -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,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>
|
|
@ -1,11 +0,0 @@
|
|||
<template>
|
||||
<div>test</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'FileManage',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -1,22 +1,27 @@
|
|||
import type { TableColumn } from '@/components/core/dynamic-table';
|
||||
import { ContractStatusEnum } from '@/enums/contractEnum';
|
||||
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 TableColumnItem = TableColumn<TableListItem>;
|
||||
|
||||
export const baseColumns = (ctx: { contractTypes: API.DictItemEntity[] }): TableColumnItem[] => {
|
||||
export const baseColumns = (ctx: {
|
||||
contractTypes: API.DictItemEntity[];
|
||||
dynamicTableInstance;
|
||||
}): TableColumnItem[] => {
|
||||
const { contractTypes } = ctx;
|
||||
return [
|
||||
{
|
||||
title: '编号',
|
||||
width: 120,
|
||||
width: 100,
|
||||
maxWidth: 100,
|
||||
fixed: 'left',
|
||||
dataIndex: 'contractNumber',
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
width: 200,
|
||||
width: 180,
|
||||
dataIndex: 'title',
|
||||
},
|
||||
{
|
||||
|
@ -37,17 +42,18 @@ export const baseColumns = (ctx: { contractTypes: API.DictItemEntity[] }): Table
|
|||
},
|
||||
{
|
||||
title: '甲方',
|
||||
width: 150,
|
||||
width: 120,
|
||||
dataIndex: 'partyA',
|
||||
},
|
||||
{
|
||||
title: '乙方',
|
||||
width: 150,
|
||||
width: 120,
|
||||
dataIndex: 'partyB',
|
||||
},
|
||||
{
|
||||
title: '签订时间',
|
||||
width: 100,
|
||||
width: 60,
|
||||
maxWidth: 60,
|
||||
hideInSearch: true,
|
||||
dataIndex: 'signingDate',
|
||||
customRender: ({ record }) => {
|
||||
|
@ -56,7 +62,8 @@ export const baseColumns = (ctx: { contractTypes: API.DictItemEntity[] }): Table
|
|||
},
|
||||
{
|
||||
title: '交付期限',
|
||||
width: 100,
|
||||
width: 60,
|
||||
maxWidth: 60,
|
||||
hideInSearch: true,
|
||||
dataIndex: 'deliveryDeadline',
|
||||
customRender: ({ record }) => {
|
||||
|
@ -68,12 +75,13 @@ export const baseColumns = (ctx: { contractTypes: API.DictItemEntity[] }): Table
|
|||
dataIndex: 'status',
|
||||
maxWidth: 60,
|
||||
width: 60,
|
||||
fixed:'right',
|
||||
formItemProps: {
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: Object.values(ContractStatusEnum)
|
||||
.filter((value) => typeof value === 'number')
|
||||
.map((item) => formatStatus(item as ContractStatusEnum)),
|
||||
.filter((value) => typeof value === 'number')
|
||||
.map((item) => formatStatus(item as ContractStatusEnum)),
|
||||
},
|
||||
},
|
||||
customRender: ({ record }) => {
|
||||
|
@ -81,6 +89,7 @@ export const baseColumns = (ctx: { contractTypes: API.DictItemEntity[] }): Table
|
|||
return <Tag color={color}>{label}</Tag>;
|
||||
},
|
||||
},
|
||||
|
||||
];
|
||||
};
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
:data-request="Api.contract.contractList"
|
||||
:columns="columns"
|
||||
bordered
|
||||
:scroll="{ x: 1920 }"
|
||||
size="small"
|
||||
>
|
||||
<template #toolbar>
|
||||
|
@ -20,7 +21,6 @@
|
|||
</template>
|
||||
</DynamicTable>
|
||||
</div>
|
||||
<!-- <UploadModal v-if="isUploadPopupVisiable" :visiable="isUploadPopupVisiable"></UploadModal> -->
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
|
@ -28,12 +28,14 @@
|
|||
import { baseColumns, type TableColumnItem, type TableListItem } from './columns';
|
||||
import Api from '@/api/';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onMounted, ref, type FunctionalComponent } from 'vue';
|
||||
import { DictEnum } from '@/enums/dictEnum';
|
||||
import { useFormModal, useModal } from '@/hooks/useModal';
|
||||
import { contractSchemas } from './formSchemas';
|
||||
import { formatToDate } from '@/utils/dateUtil';
|
||||
import UploadContract from './upload-contract.vue';
|
||||
import { Button } from 'ant-design-vue';
|
||||
import AttachmentManage from '@/components/business/attachment-manage/index.vue';
|
||||
defineOptions({
|
||||
name: 'Contract',
|
||||
});
|
||||
|
@ -54,13 +56,23 @@
|
|||
getContractTypes().then((res) => {
|
||||
columns.value = [
|
||||
...baseColumns({
|
||||
dynamicTableInstance,
|
||||
contractTypes: contractTypes.value,
|
||||
}),
|
||||
{
|
||||
title: '附件',
|
||||
width: 50,
|
||||
maxWidth: 50,
|
||||
hideInSearch: true,
|
||||
fixed:'right',
|
||||
dataIndex: 'files',
|
||||
customRender: ({ record }) => <FilesRender {...record} />,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
maxWidth: 100,
|
||||
width: 100,
|
||||
minWidth: 100,
|
||||
maxWidth: 80,
|
||||
width: 80,
|
||||
minWidth: 80,
|
||||
dataIndex: 'ACTION',
|
||||
hideInSearch: true,
|
||||
fixed: 'right',
|
||||
|
@ -86,8 +98,8 @@
|
|||
},
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:cloud-server-outlined',
|
||||
tooltip: '上传下载附件',
|
||||
icon: 'ant-design:cloud-upload-outlined',
|
||||
tooltip: '上传附件',
|
||||
onClick: () => openFileManageModal(record),
|
||||
},
|
||||
],
|
||||
|
@ -97,25 +109,37 @@
|
|||
});
|
||||
|
||||
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>;
|
||||
return (
|
||||
<UploadContract
|
||||
onClose={handleUploadClose}
|
||||
afterUploadCallback={(files) => {
|
||||
afterUploadCallback(files, record.id);
|
||||
}}
|
||||
></UploadContract>
|
||||
);
|
||||
},
|
||||
destroyOnClose: true,
|
||||
open: isUploadPopupVisiable.value,
|
||||
footer: null,
|
||||
});
|
||||
};
|
||||
const handleUploadClose = (hasSuccess: boolean) => {
|
||||
fnModal.hide();
|
||||
// 在这里你可以关闭模态框
|
||||
// fnModal.hide();
|
||||
isUploadPopupVisiable.value = false;
|
||||
};
|
||||
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 打开新增/编辑弹窗
|
||||
*/
|
||||
|
@ -153,6 +177,42 @@
|
|||
}
|
||||
};
|
||||
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>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -23,7 +23,12 @@
|
|||
import Api from '@/api/';
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const { afterUploadCallback } = defineProps({
|
||||
afterUploadCallback: {
|
||||
type: Function,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
const [DynamicTable] = useTable();
|
||||
|
||||
const fileList = ref<FileItem[]>([]);
|
||||
|
@ -56,10 +61,10 @@
|
|||
|
||||
const onOk = async () => {
|
||||
const uploadFileList = fileList.value.filter((n) => n.status !== UploadResultStatus.SUCCESS);
|
||||
await Promise.all(
|
||||
const res = await Promise.all(
|
||||
uploadFileList.map(async (item) => {
|
||||
try {
|
||||
await Api.toolsUpload.uploadUpload({ file: item.file }, undefined, {
|
||||
const itemRes = await Api.toolsUpload.uploadUpload({ file: item.file }, undefined, {
|
||||
onUploadProgress(progressEvent) {
|
||||
const complete = ((progressEvent.loaded / progressEvent.total!) * 100) | 0;
|
||||
item.percent = complete;
|
||||
|
@ -67,12 +72,16 @@
|
|||
},
|
||||
});
|
||||
item.status = UploadResultStatus.SUCCESS;
|
||||
return itemRes;
|
||||
} catch (error) {
|
||||
console.log(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) => {
|
||||
|
|
Loading…
Reference in New Issue