14 changed files with 2752 additions and 162 deletions
@ -0,0 +1,74 @@ |
|||||
|
import { defHttp } from '@/utils/http/axios'; |
||||
|
import { ID, IDS, commonExport } from '@/api/base'; |
||||
|
import { ContractualTaskChecklistVO, ContractualTaskChecklistFormList, ContractualTaskChecklistQuery } from './model'; |
||||
|
|
||||
|
/** |
||||
|
* 查询合同任务审查清单列表 |
||||
|
* @param params |
||||
|
* @returns |
||||
|
*/ |
||||
|
export function ContractualTaskChecklistList(params?: ContractualTaskChecklistQuery) { |
||||
|
return defHttp.get<ContractualTaskChecklistVO[]>({ url: '/productManagement/ContractualTaskChecklist/list', params }); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查询合同任务审查清单列表(不分页) |
||||
|
* @param params |
||||
|
* @returns |
||||
|
*/ |
||||
|
export function ContractualTaskChecklistQueryList(params?: ContractualTaskChecklistQuery) { |
||||
|
return defHttp.get<ContractualTaskChecklistVO[]>({ url: '/productManagement/ContractualTaskChecklist/queryList', params }); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 导出合同任务审查清单列表 |
||||
|
* @param params |
||||
|
* @returns |
||||
|
*/ |
||||
|
export function ContractualTaskChecklistExport(params?: ContractualTaskChecklistQuery) { |
||||
|
return commonExport('/productManagement/ContractualTaskChecklist/export', params ?? {}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查询合同任务审查清单详细 |
||||
|
* @param id id |
||||
|
* @returns |
||||
|
*/ |
||||
|
export function ContractualTaskChecklistInfo(id: ID) { |
||||
|
return defHttp.get<ContractualTaskChecklistVO[]>({ url: '/productManagement/ContractualTaskChecklist/' + id }); |
||||
|
} |
||||
|
/** |
||||
|
* 查询合同任务审查清单详细 |
||||
|
* @param id id |
||||
|
* @returns |
||||
|
*/ |
||||
|
export function ContractualTaskChecklistInfoByGroupId(id: ID) { |
||||
|
return defHttp.get<ContractualTaskChecklistVO[]>({ url: '/productManagement/ContractualTaskChecklist/queryByGroupId/' + id }); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 新增合同任务审查清单 |
||||
|
* @param data |
||||
|
* @returns |
||||
|
*/ |
||||
|
export function ContractualTaskChecklistAdd(data: ContractualTaskChecklistFormList) { |
||||
|
return defHttp.postWithMsg<void>({ url: '/productManagement/ContractualTaskChecklist', data }); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 更新合同任务审查清单 |
||||
|
* @param data |
||||
|
* @returns |
||||
|
*/ |
||||
|
export function ContractualTaskChecklistUpdate(data: ContractualTaskChecklistFormList) { |
||||
|
return defHttp.putWithMsg<void>({ url: '/productManagement/ContractualTaskChecklist', data }); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除合同任务审查清单 |
||||
|
* @param id id |
||||
|
* @returns |
||||
|
*/ |
||||
|
export function ContractualTaskChecklistRemove(id: ID | IDS) { |
||||
|
return defHttp.deleteWithMsg<void>({ url: '/productManagement/ContractualTaskChecklist/' + id },); |
||||
|
} |
@ -0,0 +1,77 @@ |
|||||
|
import { BaseEntity, PageQuery } from '@/api/base'; |
||||
|
|
||||
|
|
||||
|
export interface ContractualTaskChecklistVO { |
||||
|
/** |
||||
|
* id |
||||
|
*/ |
||||
|
id: string | number; |
||||
|
|
||||
|
/** |
||||
|
* 清单名称 |
||||
|
*/ |
||||
|
name: string; |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 清单项内容 |
||||
|
*/ |
||||
|
checklistItem: string; |
||||
|
|
||||
|
/** |
||||
|
* 清单项描述 |
||||
|
*/ |
||||
|
checklistItemDesc: string; |
||||
|
} |
||||
|
|
||||
|
export interface ChecklistItemForm { |
||||
|
/** |
||||
|
* id |
||||
|
*/ |
||||
|
id?: string | number; |
||||
|
|
||||
|
/** |
||||
|
* 清单项内容 |
||||
|
*/ |
||||
|
checklistItem?: string; |
||||
|
|
||||
|
/** |
||||
|
* 清单项描述 |
||||
|
*/ |
||||
|
checklistItemDesc?: string; |
||||
|
name?: string; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
export interface ContractualTaskChecklistQuery extends PageQuery { |
||||
|
/** |
||||
|
* 清单名称 |
||||
|
*/ |
||||
|
name?: string; |
||||
|
|
||||
|
/** |
||||
|
* 清单项内容 |
||||
|
*/ |
||||
|
checklistItem?: string; |
||||
|
|
||||
|
/** |
||||
|
* 日期范围参数 |
||||
|
*/ |
||||
|
params?: any; |
||||
|
} |
||||
|
export interface ContractualTaskChecklistForm extends BaseEntity { |
||||
|
id?: string | number; |
||||
|
name: string; |
||||
|
checklistItem: string; |
||||
|
checklistItemDesc?: string; |
||||
|
groupId?: string | number; |
||||
|
} |
||||
|
|
||||
|
export interface ContractualTaskChecklistResponse extends Omit<ContractualTaskChecklistForm, 'checklistItem'> { |
||||
|
checklistItems?: { |
||||
|
checklistItem: string; |
||||
|
checklistItemDesc: string; |
||||
|
}[]; |
||||
|
} |
||||
|
|
||||
|
export type ContractualTaskChecklistFormList = ContractualTaskChecklistForm[]; |
After Width: | Height: | Size: 577 B |
After Width: | Height: | Size: 368 B |
After Width: | Height: | Size: 481 B |
@ -0,0 +1,62 @@ |
|||||
|
import { BasicColumn } from '@/components/Table'; |
||||
|
import { FormSchema } from '@/components/Form'; |
||||
|
|
||||
|
export const formSchemas: FormSchema[] = [ |
||||
|
{ |
||||
|
label: '清单名称', |
||||
|
field: 'name', |
||||
|
component: 'Input', |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
export const columns: BasicColumn[] = [ |
||||
|
{ |
||||
|
title: 'groupId', |
||||
|
dataIndex: 'groupId', |
||||
|
ifShow: false, |
||||
|
}, |
||||
|
{ |
||||
|
title: '清单名称', |
||||
|
dataIndex: 'name', |
||||
|
}, |
||||
|
{ |
||||
|
title: '清单项数量', |
||||
|
dataIndex: 'checklistItemNum', |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
export const modalSchemas: FormSchema[] = [ |
||||
|
{ |
||||
|
label: 'id', |
||||
|
field: 'id', |
||||
|
required: false, |
||||
|
component: 'Input', |
||||
|
show: false, |
||||
|
}, |
||||
|
{ |
||||
|
label: '清单名称', |
||||
|
field: 'name', |
||||
|
required: true, |
||||
|
component: 'Input', |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
// 清单项表格列定义
|
||||
|
export const checklistItemColumns: BasicColumn[] = [ |
||||
|
{ |
||||
|
title: 'id', |
||||
|
dataIndex: 'id', |
||||
|
ifShow: false, |
||||
|
}, |
||||
|
{ |
||||
|
title: '清单项内容', |
||||
|
dataIndex: 'checklistItem', |
||||
|
align: 'left', |
||||
|
width: 200, |
||||
|
}, |
||||
|
{ |
||||
|
title: '清单项描述', |
||||
|
dataIndex: 'checklistItemDesc', |
||||
|
align: 'left', |
||||
|
} |
||||
|
]; |
@ -0,0 +1,207 @@ |
|||||
|
<template> |
||||
|
<BasicModal |
||||
|
v-bind="$attrs" |
||||
|
:title="title" |
||||
|
@register="registerInnerModal" |
||||
|
@ok="handleSubmit" |
||||
|
@cancel="resetForm" |
||||
|
width="800px" |
||||
|
> |
||||
|
<Form |
||||
|
ref="formRef" |
||||
|
:model="formState" |
||||
|
:rules="rules" |
||||
|
:label-col="{ span: 4 }" |
||||
|
:wrapper-col="{ span: 20 }" |
||||
|
> |
||||
|
<Form.Item label="清单名称" name="name"> |
||||
|
<Input v-model:value="formState.name" placeholder="请输入清单名称" /> |
||||
|
</Form.Item> |
||||
|
</Form> |
||||
|
|
||||
|
<div class="mt-4"> |
||||
|
<div class="flex justify-between mb-2"> |
||||
|
<div class="text-lg font-bold">清单项列表</div> |
||||
|
<Button type="primary" @click="handleAddItem">添加清单项</Button> |
||||
|
</div> |
||||
|
<Table |
||||
|
:columns="itemColumns" |
||||
|
:dataSource="checklistItems" |
||||
|
:pagination="false" |
||||
|
bordered |
||||
|
> |
||||
|
<template #bodyCell="{ column, record, index }"> |
||||
|
<template v-if="column.key === 'checklistItem'"> |
||||
|
<Input v-model:value="record.checklistItem" placeholder="请输入清单项内容" /> |
||||
|
</template> |
||||
|
<template v-else-if="column.key === 'checklistItemDesc'"> |
||||
|
<Input.TextArea |
||||
|
v-model:value="record.checklistItemDesc" |
||||
|
placeholder="请输入清单项描述" |
||||
|
:rows="2" |
||||
|
:autoSize="{ minRows: 2, maxRows: 6 }" |
||||
|
/> |
||||
|
</template> |
||||
|
<template v-else-if="column.key === 'action'"> |
||||
|
<Button type="primary" danger @click="handleDeleteItem(index)">删除</Button> |
||||
|
</template> |
||||
|
</template> |
||||
|
</Table> |
||||
|
</div> |
||||
|
</BasicModal> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { BasicModal, useModalInner } from '@/components/Modal'; |
||||
|
import { computed, ref, unref, reactive } from 'vue'; |
||||
|
import { ContractualTaskChecklistInfoByGroupId, ContractualTaskChecklistAdd, ContractualTaskChecklistUpdate } from '@/api/contractReview/ContractualTaskChecklist'; |
||||
|
import { Table, Button, Input, Form } from 'ant-design-vue'; |
||||
|
import type { Rule } from 'ant-design-vue/es/form'; |
||||
|
import type { ContractualTaskChecklistForm } from '@/api/contractReview/ContractualTaskChecklist/model'; |
||||
|
|
||||
|
interface ChecklistItem { |
||||
|
checklistItem: string; |
||||
|
checklistItemDesc: string; |
||||
|
} |
||||
|
|
||||
|
defineOptions({ name: 'ContractualTaskChecklistModal' }); |
||||
|
|
||||
|
const emit = defineEmits(['register', 'reload']); |
||||
|
const formRef = ref(); |
||||
|
|
||||
|
const isUpdate = ref<boolean>(false); |
||||
|
const title = computed<string>(() => { |
||||
|
return isUpdate.value ? '编辑合同任务审查清单' : '新增合同任务审查清单'; |
||||
|
}); |
||||
|
|
||||
|
// 表单状态 |
||||
|
const formState = reactive({ |
||||
|
groupId: undefined as string | number | undefined, |
||||
|
name: '', |
||||
|
}); |
||||
|
|
||||
|
// 表单验证规则 |
||||
|
const rules: Record<string, Rule[]> = { |
||||
|
name: [ |
||||
|
{ required: true, message: '请输入清单名称', trigger: 'blur' }, |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
// 清单项列表 |
||||
|
const checklistItems = ref<ChecklistItem[]>([]); |
||||
|
|
||||
|
// 清单项表格列定义 |
||||
|
const itemColumns = [ |
||||
|
{ |
||||
|
title: '序号', |
||||
|
dataIndex: 'index', |
||||
|
width: 60, |
||||
|
customRender: ({ index }) => index + 1, |
||||
|
}, |
||||
|
{ |
||||
|
title: '清单项内容', |
||||
|
dataIndex: 'checklistItem', |
||||
|
key: 'checklistItem', |
||||
|
align: 'left' as const, |
||||
|
width: 300, |
||||
|
}, |
||||
|
{ |
||||
|
title: '清单项描述', |
||||
|
dataIndex: 'checklistItemDesc', |
||||
|
key: 'checklistItemDesc', |
||||
|
align: 'left' as const, |
||||
|
}, |
||||
|
{ |
||||
|
title: '操作', |
||||
|
key: 'action', |
||||
|
width: 80, |
||||
|
align: 'center' as const, |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
const [registerInnerModal, { modalLoading, closeModal }] = useModalInner( |
||||
|
async (data: { record?: Recordable; update: boolean }) => { |
||||
|
modalLoading(true); |
||||
|
const { record, update } = data; |
||||
|
isUpdate.value = update; |
||||
|
checklistItems.value = []; |
||||
|
|
||||
|
if (update && record) { |
||||
|
const ret = await ContractualTaskChecklistInfoByGroupId(record.groupId); |
||||
|
formState.name = ret[0].name; |
||||
|
formState.groupId = record.groupId; |
||||
|
// 加载清单项列表 |
||||
|
if (ret.length > 0) { |
||||
|
checklistItems.value = ret; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
modalLoading(false); |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
// 添加清单项 |
||||
|
function handleAddItem() { |
||||
|
checklistItems.value.push({ |
||||
|
checklistItem: '', |
||||
|
checklistItemDesc: '', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 删除清单项 |
||||
|
function handleDeleteItem(index: number) { |
||||
|
checklistItems.value.splice(index, 1); |
||||
|
} |
||||
|
|
||||
|
// 重置表单 |
||||
|
async function resetForm() { |
||||
|
formRef.value?.resetFields(); |
||||
|
checklistItems.value = []; |
||||
|
} |
||||
|
|
||||
|
async function handleSubmit() { |
||||
|
try { |
||||
|
modalLoading(true); |
||||
|
|
||||
|
// 表单验证 |
||||
|
await formRef.value.validate(); |
||||
|
|
||||
|
// 验证清单项 |
||||
|
if (checklistItems.value.length === 0) { |
||||
|
modalLoading(false); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 验证每个清单项的必填字段 |
||||
|
for (const item of checklistItems.value) { |
||||
|
if (!item.checklistItem || !item.checklistItemDesc) { |
||||
|
modalLoading(false); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 构建提交数据 |
||||
|
const submitDataList: ContractualTaskChecklistForm[] = checklistItems.value.map(item => ({ |
||||
|
name: formState.name, |
||||
|
checklistItem: item.checklistItem, |
||||
|
checklistItemDesc: item.checklistItemDesc, |
||||
|
...(formState.groupId !== undefined ? { groupId: formState.groupId } : {}) |
||||
|
})); |
||||
|
|
||||
|
if (unref(isUpdate)) { |
||||
|
await ContractualTaskChecklistUpdate(submitDataList); |
||||
|
} else { |
||||
|
await ContractualTaskChecklistAdd(submitDataList); |
||||
|
} |
||||
|
emit('reload'); |
||||
|
closeModal(); |
||||
|
await resetForm(); |
||||
|
} catch (e) { |
||||
|
console.error(e); |
||||
|
} finally { |
||||
|
modalLoading(false); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped></style> |
@ -0,0 +1,115 @@ |
|||||
|
<template> |
||||
|
<PageWrapper dense> |
||||
|
<BasicTable @register="registerTable"> |
||||
|
<template #toolbar> |
||||
|
<a-button |
||||
|
@click="downloadExcel(ContractualTaskChecklistExport, '合同任务审查清单数据', getForm().getFieldsValue())" |
||||
|
v-auth="'productManagement:ContractualTaskChecklist:export'" |
||||
|
>导出</a-button |
||||
|
> |
||||
|
<a-button |
||||
|
type="primary" |
||||
|
danger |
||||
|
@click="multipleRemove(ContractualTaskChecklistRemove)" |
||||
|
:disabled="!selected" |
||||
|
v-auth="'productManagement:ContractualTaskChecklist:remove'" |
||||
|
>删除</a-button |
||||
|
> |
||||
|
<a-button |
||||
|
type="primary" |
||||
|
@click="handleAdd" |
||||
|
v-auth="'productManagement:ContractualTaskChecklist:add'" |
||||
|
>新增</a-button |
||||
|
> |
||||
|
</template> |
||||
|
<template #bodyCell="{ column, record }"> |
||||
|
<template v-if="column.key === 'action'"> |
||||
|
<TableAction |
||||
|
stopButtonPropagation |
||||
|
:actions="[ |
||||
|
{ |
||||
|
label: '修改', |
||||
|
icon: IconEnum.EDIT, |
||||
|
type: 'primary', |
||||
|
ghost: true, |
||||
|
auth: 'productManagement:ContractualTaskChecklist:edit', |
||||
|
onClick: handleEdit.bind(null, record), |
||||
|
}, |
||||
|
{ |
||||
|
label: '删除', |
||||
|
icon: IconEnum.DELETE, |
||||
|
type: 'primary', |
||||
|
danger: true, |
||||
|
ghost: true, |
||||
|
auth: 'productManagement:ContractualTaskChecklist:remove', |
||||
|
popConfirm: { |
||||
|
placement: 'left', |
||||
|
title: '是否删除合同任务审查清单[' + record.name + ']?', |
||||
|
confirm: handleDelete.bind(null, record.groupId), |
||||
|
}, |
||||
|
}, |
||||
|
]" |
||||
|
/> |
||||
|
</template> |
||||
|
</template> |
||||
|
</BasicTable> |
||||
|
<ContractualTaskChecklistModal @register="registerModal" @reload="reload" /> |
||||
|
</PageWrapper> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { PageWrapper } from '@/components/Page'; |
||||
|
import { BasicTable, useTable, TableAction } from '@/components/Table'; |
||||
|
import { ContractualTaskChecklistList, ContractualTaskChecklistExport, ContractualTaskChecklistRemove } from '@/api/contractReview/ContractualTaskChecklist'; |
||||
|
import { downloadExcel } from '@/utils/file/download'; |
||||
|
import { useModal } from '@/components/Modal'; |
||||
|
import ContractualTaskChecklistModal from './ContractualTaskChecklistModal.vue'; |
||||
|
import { formSchemas, columns } from './ContractualTaskChecklist.data'; |
||||
|
import { IconEnum } from '@/enums/appEnum'; |
||||
|
|
||||
|
defineOptions({ name: 'ContractualTaskChecklist' }); |
||||
|
|
||||
|
const [registerTable, { reload, multipleRemove, selected, getForm }] = useTable({ |
||||
|
rowSelection: { |
||||
|
type: 'checkbox', |
||||
|
}, |
||||
|
title: '合同任务审查清单列表', |
||||
|
api: ContractualTaskChecklistList, |
||||
|
showIndexColumn: false, |
||||
|
rowKey: 'id', |
||||
|
useSearchForm: true, |
||||
|
formConfig: { |
||||
|
schemas: formSchemas, |
||||
|
baseColProps: { |
||||
|
xs: 24, |
||||
|
sm: 24, |
||||
|
md: 24, |
||||
|
lg: 6, |
||||
|
}, |
||||
|
}, |
||||
|
columns: columns, |
||||
|
actionColumn: { |
||||
|
width: 200, |
||||
|
title: '操作', |
||||
|
key: 'action', |
||||
|
fixed: 'right', |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const [registerModal, { openModal }] = useModal(); |
||||
|
|
||||
|
function handleEdit(record: Recordable) { |
||||
|
openModal(true, { record, update: true }); |
||||
|
} |
||||
|
|
||||
|
function handleAdd() { |
||||
|
openModal(true, { update: false }); |
||||
|
} |
||||
|
|
||||
|
async function handleDelete(groupId: string) { |
||||
|
await ContractualTaskChecklistRemove([groupId]); |
||||
|
await reload(); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped></style> |
@ -0,0 +1,550 @@ |
|||||
|
<template> |
||||
|
<div class="comparison-container"> |
||||
|
<div class="comparison-header"> |
||||
|
<div class="comparison-title"> |
||||
|
<div class="dot-indicator"></div> |
||||
|
上传待审查合同 |
||||
|
</div> |
||||
|
<div class="comparison-title"> |
||||
|
<div class="dot-indicator"></div> |
||||
|
上传对比合同 |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="comparison-upload-area"> |
||||
|
<!-- 左侧上传区域 --> |
||||
|
<div class="upload-box" :class="{'file-preview': toReviewFileList && toReviewFileList.length > 0}"> |
||||
|
<template v-if="toReviewFileList && toReviewFileList.length > 0"> |
||||
|
<!-- 左侧文件预览 --> |
||||
|
<div class="file-info"> |
||||
|
<FileTextOutlined class="file-icon" /> |
||||
|
<div class="file-details"> |
||||
|
<p class="file-name">{{ toReviewFileList[0].name }}</p> |
||||
|
<div class="file-progress" v-if="toReviewUploading"> |
||||
|
<Progress :percent="toReviewUploadPercent" size="small" status="active" /> |
||||
|
</div> |
||||
|
<p class="file-status" v-else> |
||||
|
<CheckCircleFilled class="status-icon success" /> 上传成功 |
||||
|
<AButton type="link" class="remove-btn" @click="removeToReviewFile">删除</AButton> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<template v-else> |
||||
|
<!-- 左侧上传 --> |
||||
|
<AUpload |
||||
|
:fileList="toReviewFileList" |
||||
|
:customRequest="handleToReviewUpload" |
||||
|
:beforeUpload="beforeUpload" |
||||
|
:showUploadList="false" |
||||
|
:maxCount="1" |
||||
|
:multiple="false" |
||||
|
name="file" |
||||
|
accept=".doc,.docx" |
||||
|
draggable |
||||
|
> |
||||
|
<div class="upload-content"> |
||||
|
<div class="upload-icon"> |
||||
|
<UpOutlined class="upload-arrow-icon" /> |
||||
|
</div> |
||||
|
<div class="upload-text-container"> |
||||
|
<p class="upload-text"> |
||||
|
拖拽待审查合同文件至此,或 <a class="upload-link">选择文件</a> |
||||
|
</p> |
||||
|
<p class="upload-hint">仅支持doc、docx格式文件</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</AUpload> |
||||
|
</template> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 右侧上传区域 --> |
||||
|
<div class="upload-box" :class="{'file-preview': referenceFileList && referenceFileList.length > 0}"> |
||||
|
<template v-if="referenceFileList && referenceFileList.length > 0"> |
||||
|
<!-- 右侧文件预览 --> |
||||
|
<div class="file-info"> |
||||
|
<FileTextOutlined class="file-icon" /> |
||||
|
<div class="file-details"> |
||||
|
<p class="file-name">{{ referenceFileList[0].name }}</p> |
||||
|
<div class="file-progress" v-if="referenceUploading"> |
||||
|
<Progress :percent="referenceUploadPercent" size="small" status="active" /> |
||||
|
</div> |
||||
|
<p class="file-status" v-else> |
||||
|
<CheckCircleFilled class="status-icon success" /> 上传成功 |
||||
|
<AButton type="link" class="remove-btn" @click="removeReferenceFile">删除</AButton> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<template v-else> |
||||
|
<!-- 右侧上传 --> |
||||
|
<AUpload |
||||
|
:fileList="referenceFileList" |
||||
|
:customRequest="handleReferenceUpload" |
||||
|
:beforeUpload="beforeUpload" |
||||
|
:showUploadList="false" |
||||
|
:maxCount="1" |
||||
|
:multiple="false" |
||||
|
name="file" |
||||
|
accept=".doc,.docx" |
||||
|
draggable |
||||
|
> |
||||
|
<div class="upload-content"> |
||||
|
<div class="upload-icon"> |
||||
|
<UpOutlined class="upload-arrow-icon" /> |
||||
|
</div> |
||||
|
<div class="upload-text-container"> |
||||
|
<p class="upload-text"> |
||||
|
拖拽对比合同文件至此,或 <a class="upload-link">选择文件</a> |
||||
|
</p> |
||||
|
<p class="upload-hint">仅支持doc、docx格式文件</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</AUpload> |
||||
|
</template> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 底部按钮 --> |
||||
|
<div class="action-buttons"> |
||||
|
<AButton |
||||
|
type="primary" |
||||
|
class="start-button" |
||||
|
@click="startComparisonReview" |
||||
|
:disabled="toReviewUploading || referenceUploading" |
||||
|
> |
||||
|
开始审查 |
||||
|
</AButton> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref } from 'vue'; |
||||
|
import { message, Progress } from 'ant-design-vue'; |
||||
|
import { UploadOutlined as UpOutlined, FileTextOutlined, CheckCircleFilled } from '@ant-design/icons-vue'; |
||||
|
import { Button, Upload } from 'ant-design-vue'; |
||||
|
import type { UploadProps } from 'ant-design-vue'; |
||||
|
import { uploadDocument } from '@/api/documentReview/DocumentTasks'; |
||||
|
import { ossRemove } from '@/api/system/oss'; |
||||
|
import { UploadFileParams } from '#/axios'; |
||||
|
|
||||
|
// 注册组件 |
||||
|
const AButton = Button; |
||||
|
const AUpload = Upload; |
||||
|
|
||||
|
// 状态变量 |
||||
|
const toReviewFileList = ref<UploadProps['fileList']>([]); |
||||
|
const referenceFileList = ref<UploadProps['fileList']>([]); |
||||
|
const toReviewUploading = ref(false); |
||||
|
const referenceUploading = ref(false); |
||||
|
const toReviewUploadPercent = ref(0); |
||||
|
const referenceUploadPercent = ref(0); |
||||
|
const toReviewOssId = ref<string | null>(null); |
||||
|
const referenceOssId = ref<string | null>(null); |
||||
|
|
||||
|
// 待审查合同上传处理 |
||||
|
function handleToReviewUpload(options: any) { |
||||
|
const { file, onSuccess, onError } = options; |
||||
|
|
||||
|
// 设置上传状态 |
||||
|
toReviewUploading.value = true; |
||||
|
toReviewUploadPercent.value = 0; |
||||
|
|
||||
|
// 创建上传参数 |
||||
|
const uploadParams: UploadFileParams = { |
||||
|
name: 'file', |
||||
|
file: file, |
||||
|
data: { |
||||
|
// 可以设置额外参数 |
||||
|
fileType: 'contract_review_to_review', |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
// 显示初始文件(上传中状态) |
||||
|
toReviewFileList.value = [ |
||||
|
{ |
||||
|
uid: '1', |
||||
|
name: file.name, |
||||
|
status: 'uploading', |
||||
|
url: URL.createObjectURL(file), |
||||
|
} as any, |
||||
|
]; |
||||
|
|
||||
|
// 调用真实的上传API |
||||
|
uploadDocument( |
||||
|
uploadParams, |
||||
|
(progressEvent) => { |
||||
|
// 处理上传进度 |
||||
|
if (progressEvent.total) { |
||||
|
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100); |
||||
|
toReviewUploadPercent.value = percent; |
||||
|
} |
||||
|
} |
||||
|
).then((res) => { |
||||
|
// 上传成功处理 |
||||
|
if (toReviewFileList.value && toReviewFileList.value.length > 0) { |
||||
|
toReviewFileList.value[0].status = 'done'; |
||||
|
toReviewFileList.value[0].response = res; // 保存返回数据 |
||||
|
// 保存OSS ID,方便后续删除 |
||||
|
if (res && res.ossId) { |
||||
|
toReviewOssId.value = res.ossId; |
||||
|
} |
||||
|
} |
||||
|
toReviewUploading.value = false; |
||||
|
toReviewUploadPercent.value = 100; |
||||
|
message.success(`待审查合同上传成功`); |
||||
|
onSuccess(res); |
||||
|
}).catch((err) => { |
||||
|
// 上传失败处理 |
||||
|
toReviewUploading.value = false; |
||||
|
toReviewFileList.value = []; |
||||
|
toReviewOssId.value = null; |
||||
|
message.error(`待审查合同上传失败: ${err.message || '未知错误'}`); |
||||
|
onError(err); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 对比合同上传处理 |
||||
|
function handleReferenceUpload(options: any) { |
||||
|
const { file, onSuccess, onError } = options; |
||||
|
|
||||
|
// 设置上传状态 |
||||
|
referenceUploading.value = true; |
||||
|
referenceUploadPercent.value = 0; |
||||
|
|
||||
|
// 创建上传参数 |
||||
|
const uploadParams: UploadFileParams = { |
||||
|
name: 'file', |
||||
|
file: file, |
||||
|
data: { |
||||
|
// 可以设置额外参数 |
||||
|
fileType: 'contract_review_reference', |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
// 显示初始文件(上传中状态) |
||||
|
referenceFileList.value = [ |
||||
|
{ |
||||
|
uid: '1', |
||||
|
name: file.name, |
||||
|
status: 'uploading', |
||||
|
url: URL.createObjectURL(file), |
||||
|
} as any, |
||||
|
]; |
||||
|
|
||||
|
// 调用真实的上传API |
||||
|
uploadDocument( |
||||
|
uploadParams, |
||||
|
(progressEvent) => { |
||||
|
// 处理上传进度 |
||||
|
if (progressEvent.total) { |
||||
|
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100); |
||||
|
referenceUploadPercent.value = percent; |
||||
|
} |
||||
|
} |
||||
|
).then((res) => { |
||||
|
// 上传成功处理 |
||||
|
if (referenceFileList.value && referenceFileList.value.length > 0) { |
||||
|
referenceFileList.value[0].status = 'done'; |
||||
|
referenceFileList.value[0].response = res; // 保存返回数据 |
||||
|
// 保存OSS ID,方便后续删除 |
||||
|
if (res && res.ossId) { |
||||
|
referenceOssId.value = res.ossId; |
||||
|
} |
||||
|
} |
||||
|
referenceUploading.value = false; |
||||
|
referenceUploadPercent.value = 100; |
||||
|
message.success(`对比合同上传成功`); |
||||
|
onSuccess(res); |
||||
|
}).catch((err) => { |
||||
|
// 上传失败处理 |
||||
|
referenceUploading.value = false; |
||||
|
referenceFileList.value = []; |
||||
|
referenceOssId.value = null; |
||||
|
message.error(`对比合同上传失败: ${err.message || '未知错误'}`); |
||||
|
onError(err); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 删除待审查合同 |
||||
|
function removeToReviewFile() { |
||||
|
if (toReviewUploading.value) { |
||||
|
message.warning('文件正在上传中,请稍后再试'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 使用保存的OSS ID进行删除 |
||||
|
if (toReviewOssId.value) { |
||||
|
// 传入数组形式的参数,符合IDS类型要求 |
||||
|
ossRemove([toReviewOssId.value]) |
||||
|
.then(() => { |
||||
|
toReviewFileList.value = []; |
||||
|
toReviewUploadPercent.value = 0; |
||||
|
toReviewOssId.value = null; |
||||
|
message.success('待审查合同已删除'); |
||||
|
}) |
||||
|
.catch((err) => { |
||||
|
message.error(`文件删除失败: ${err.message || '未知错误'}`); |
||||
|
}); |
||||
|
} else { |
||||
|
// 如果没有ossId,直接清空本地状态 |
||||
|
toReviewFileList.value = []; |
||||
|
toReviewUploadPercent.value = 0; |
||||
|
message.success('待审查合同已删除'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 删除对比合同 |
||||
|
function removeReferenceFile() { |
||||
|
if (referenceUploading.value) { |
||||
|
message.warning('文件正在上传中,请稍后再试'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 使用保存的OSS ID进行删除 |
||||
|
if (referenceOssId.value) { |
||||
|
// 传入数组形式的参数,符合IDS类型要求 |
||||
|
ossRemove([referenceOssId.value]) |
||||
|
.then(() => { |
||||
|
referenceFileList.value = []; |
||||
|
referenceUploadPercent.value = 0; |
||||
|
referenceOssId.value = null; |
||||
|
message.success('对比合同已删除'); |
||||
|
}) |
||||
|
.catch((err) => { |
||||
|
message.error(`文件删除失败: ${err.message || '未知错误'}`); |
||||
|
}); |
||||
|
} else { |
||||
|
// 如果没有ossId,直接清空本地状态 |
||||
|
referenceFileList.value = []; |
||||
|
referenceUploadPercent.value = 0; |
||||
|
message.success('对比合同已删除'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function beforeUpload(file: File) { |
||||
|
const isDocx = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || |
||||
|
file.type === 'application/msword'; |
||||
|
if (!isDocx) { |
||||
|
message.error('只能上传 doc/docx 格式的文件!'); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// 文件大小限制(500MB) |
||||
|
const isLt500M = file.size / 1024 / 1024 < 500; |
||||
|
if (!isLt500M) { |
||||
|
message.error('文件大小不能超过 500MB!'); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// 开始对比审查 |
||||
|
function startComparisonReview() { |
||||
|
if (toReviewUploading.value || referenceUploading.value) { |
||||
|
message.warning('文件正在上传中,请稍后再试'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (!toReviewFileList.value || toReviewFileList.value.length === 0) { |
||||
|
message.warning('请先上传待审查合同'); |
||||
|
return; |
||||
|
} |
||||
|
if (!referenceFileList.value || referenceFileList.value.length === 0) { |
||||
|
message.warning('请先上传对比合同'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 使用保存的OSS ID进行审查 |
||||
|
if (!toReviewOssId.value || !referenceOssId.value) { |
||||
|
message.warning('文件上传异常,请重新上传'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// TODO: 调用对比审查API,传入文件ID等信息 |
||||
|
message.success('开始对比审查'); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.comparison-container { |
||||
|
background-color: #fff; |
||||
|
border-radius: 12px; |
||||
|
padding: 30px; |
||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
||||
|
margin-top: 30px; |
||||
|
max-width: 1100px; |
||||
|
margin-left: auto; |
||||
|
margin-right: auto; |
||||
|
} |
||||
|
|
||||
|
.comparison-header { |
||||
|
display: flex; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.comparison-title { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
font-size: 16px; |
||||
|
color: #333; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.dot-indicator { |
||||
|
width: 8px; |
||||
|
height: 8px; |
||||
|
background-color: #52c41a; |
||||
|
border-radius: 50%; |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
|
||||
|
.comparison-upload-area { |
||||
|
display: flex; |
||||
|
gap: 20px; |
||||
|
margin-bottom: 30px; |
||||
|
} |
||||
|
|
||||
|
.upload-box { |
||||
|
flex: 1; |
||||
|
border: 1px dashed #ddd; |
||||
|
border-radius: 8px; |
||||
|
padding: 20px; |
||||
|
height: 240px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
background-color: #f9f9f9; |
||||
|
} |
||||
|
|
||||
|
.upload-box.file-preview { |
||||
|
border: 2px solid #e6f7ff; |
||||
|
background-color: #f0f8ff; |
||||
|
padding: 20px; |
||||
|
} |
||||
|
|
||||
|
.upload-box:hover { |
||||
|
border-color: #1890ff; |
||||
|
background-color: rgba(24, 144, 255, 0.02); |
||||
|
} |
||||
|
|
||||
|
.upload-content { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.upload-icon { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
margin-bottom: 20px; |
||||
|
background-color: #e6f7ff; |
||||
|
width: 60px; |
||||
|
height: 60px; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
|
||||
|
.upload-arrow-icon { |
||||
|
font-size: 28px; |
||||
|
color: #1890ff; |
||||
|
} |
||||
|
|
||||
|
.upload-text-container { |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.upload-text { |
||||
|
font-size: 18px; |
||||
|
color: #333; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.upload-link { |
||||
|
color: #1890ff; |
||||
|
cursor: pointer; |
||||
|
text-decoration: none; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.upload-link:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
|
||||
|
.upload-hint { |
||||
|
color: #888; |
||||
|
font-size: 14px; |
||||
|
margin-top: 8px; |
||||
|
margin-bottom: 15px; |
||||
|
} |
||||
|
|
||||
|
.file-info { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.file-icon { |
||||
|
font-size: 36px; |
||||
|
color: #1890ff; |
||||
|
margin-right: 20px; |
||||
|
} |
||||
|
|
||||
|
.file-details { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.file-name { |
||||
|
font-size: 16px; |
||||
|
font-weight: 500; |
||||
|
margin: 0 0 10px 0; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.file-progress { |
||||
|
margin: 0 0 10px 0; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.file-status { |
||||
|
font-size: 14px; |
||||
|
color: #666; |
||||
|
margin: 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.status-icon { |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
|
||||
|
.status-icon.success { |
||||
|
color: #52c41a; |
||||
|
} |
||||
|
|
||||
|
.remove-btn { |
||||
|
padding: 0; |
||||
|
margin-left: 15px; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.action-buttons { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.start-button { |
||||
|
width: 180px; |
||||
|
height: 44px; |
||||
|
background-color: #52c41a; |
||||
|
border-color: #52c41a; |
||||
|
font-size: 16px; |
||||
|
border-radius: 22px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,413 @@ |
|||||
|
<template> |
||||
|
<div class="upload-container"> |
||||
|
<div class="upload-box" :class="{'file-preview': fileList && fileList.length > 0}"> |
||||
|
<template v-if="fileList && fileList.length > 0"> |
||||
|
<!-- 文件预览 --> |
||||
|
<div class="file-info"> |
||||
|
<FileTextOutlined class="file-icon" /> |
||||
|
<div class="file-details"> |
||||
|
<p class="file-name">{{ fileList[0].name }}</p> |
||||
|
<div class="file-progress" v-if="uploading"> |
||||
|
<Progress :percent="uploadPercent" size="small" status="active" /> |
||||
|
</div> |
||||
|
<p class="file-status" v-else> |
||||
|
<CheckCircleFilled class="status-icon success" /> 上传成功 |
||||
|
<AButton type="link" class="remove-btn" @click="removeFile">删除</AButton> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<template v-else> |
||||
|
<!-- 上传区域 --> |
||||
|
<AUpload |
||||
|
:fileList="fileList" |
||||
|
:customRequest="customUploadRequest" |
||||
|
:beforeUpload="beforeUpload" |
||||
|
:showUploadList="false" |
||||
|
:maxCount="1" |
||||
|
:multiple="false" |
||||
|
name="file" |
||||
|
accept=".doc,.docx" |
||||
|
draggable |
||||
|
> |
||||
|
<div class="upload-content"> |
||||
|
<div class="upload-icon"> |
||||
|
<UpOutlined class="upload-arrow-icon" /> |
||||
|
</div> |
||||
|
<div class="upload-text-container"> |
||||
|
<p class="upload-text"> |
||||
|
拖拽合同文件至此,或 <a class="upload-link">选择文件</a> |
||||
|
</p> |
||||
|
<p class="upload-hint">仅支持doc、docx格式文件</p> |
||||
|
<a class="sample-link" @click="useSampleFile">使用样例合同审查</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</AUpload> |
||||
|
</template> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 底部按钮 --> |
||||
|
<div class="action-buttons"> |
||||
|
<AButton type="primary" class="start-button" @click="startReview" :disabled="uploading"> |
||||
|
开始审查 |
||||
|
</AButton> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 审查弹窗 --> |
||||
|
<ReviewDialog |
||||
|
@register="registerReviewDialog" |
||||
|
@success="handleReviewSuccess" |
||||
|
/> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref } from 'vue'; |
||||
|
import { message, Progress } from 'ant-design-vue'; |
||||
|
import { UploadOutlined as UpOutlined, FileTextOutlined, CheckCircleFilled } from '@ant-design/icons-vue'; |
||||
|
import { Button, Upload } from 'ant-design-vue'; |
||||
|
import type { UploadProps } from 'ant-design-vue'; |
||||
|
import { uploadDocument } from '@/api/documentReview/DocumentTasks'; |
||||
|
import { ossRemove } from '@/api/system/oss'; |
||||
|
import { UploadFileParams } from '#/axios'; |
||||
|
import ReviewDialog from './ReviewDialog.vue'; |
||||
|
import { useModal } from '@/components/Modal'; |
||||
|
|
||||
|
// 注册组件 |
||||
|
const AButton = Button; |
||||
|
const AUpload = Upload; |
||||
|
|
||||
|
// 状态变量 |
||||
|
const fileList = ref<UploadProps['fileList']>([]); |
||||
|
const uploading = ref(false); |
||||
|
const uploadPercent = ref(0); |
||||
|
const currentOssId = ref<string | null>(null); |
||||
|
|
||||
|
// 弹窗相关 |
||||
|
const [registerReviewDialog, { openModal: openReviewDialog }] = useModal(); |
||||
|
|
||||
|
// 上传文件返回的信息 |
||||
|
interface UploadResponse { |
||||
|
ossId: string; |
||||
|
url: string; |
||||
|
fileName: string; |
||||
|
} |
||||
|
|
||||
|
// 文件上传相关处理 |
||||
|
function customUploadRequest(options: any) { |
||||
|
const { file, onSuccess, onError } = options; |
||||
|
|
||||
|
// 设置上传状态 |
||||
|
uploading.value = true; |
||||
|
uploadPercent.value = 0; |
||||
|
|
||||
|
// 创建上传参数 |
||||
|
const uploadParams: UploadFileParams = { |
||||
|
// 上传文件的字段名 |
||||
|
name: 'file', |
||||
|
// 文件对象 |
||||
|
file: file, |
||||
|
// 如果需要附加其他数据 |
||||
|
data: { |
||||
|
// 可以添加额外参数如需要 |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
// 显示初始文件(上传中状态) |
||||
|
fileList.value = [ |
||||
|
{ |
||||
|
uid: '1', |
||||
|
name: file.name, |
||||
|
status: 'uploading', |
||||
|
url: URL.createObjectURL(file), |
||||
|
} as any, |
||||
|
]; |
||||
|
|
||||
|
// 调用真实的上传API |
||||
|
uploadDocument( |
||||
|
uploadParams, |
||||
|
(progressEvent) => { |
||||
|
// 处理上传进度 |
||||
|
if (progressEvent.total) { |
||||
|
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100); |
||||
|
uploadPercent.value = percent; |
||||
|
} |
||||
|
} |
||||
|
).then((res) => { |
||||
|
// 上传成功处理 |
||||
|
if (fileList.value && fileList.value.length > 0) { |
||||
|
fileList.value[0].status = 'done'; |
||||
|
fileList.value[0].response = res; // 保存返回数据 |
||||
|
// 保存OSS ID,方便后续删除 |
||||
|
if (res && res.ossId) { |
||||
|
currentOssId.value = res.ossId; |
||||
|
} |
||||
|
} |
||||
|
uploading.value = false; |
||||
|
uploadPercent.value = 100; |
||||
|
message.success(`${file.name} 文件上传成功`); |
||||
|
onSuccess(res); |
||||
|
}).catch((err) => { |
||||
|
// 上传失败处理 |
||||
|
uploading.value = false; |
||||
|
fileList.value = []; |
||||
|
currentOssId.value = null; |
||||
|
message.error(`文件上传失败: ${err.message || '未知错误'}`); |
||||
|
onError(err); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function beforeUpload(file: File) { |
||||
|
const isDocx = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || |
||||
|
file.type === 'application/msword'; |
||||
|
if (!isDocx) { |
||||
|
message.error('只能上传 doc/docx 格式的文件!'); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// 文件大小限制(500MB) |
||||
|
const isLt500M = file.size / 1024 / 1024 < 500; |
||||
|
if (!isLt500M) { |
||||
|
message.error('文件大小不能超过 500MB!'); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// 使用样例合同 |
||||
|
function useSampleFile() { |
||||
|
message.warning('系统中暂无样例合同,请上传您自己的合同文件'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 删除已上传的文件 |
||||
|
function removeFile() { |
||||
|
if (uploading.value) { |
||||
|
message.warning('文件正在上传中,请稍后再试'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 使用保存的OSS ID进行删除 |
||||
|
if (currentOssId.value) { |
||||
|
// 传入数组形式的参数,符合IDS类型要求 |
||||
|
ossRemove([currentOssId.value]) |
||||
|
.then(() => { |
||||
|
fileList.value = []; |
||||
|
uploadPercent.value = 0; |
||||
|
currentOssId.value = null; |
||||
|
message.success('文件已删除'); |
||||
|
}) |
||||
|
.catch((err) => { |
||||
|
message.error(`文件删除失败: ${err.message || '未知错误'}`); |
||||
|
}); |
||||
|
} else { |
||||
|
// 如果没有ossId,直接清空本地状态 |
||||
|
fileList.value = []; |
||||
|
uploadPercent.value = 0; |
||||
|
message.success('文件已删除'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 开始审查 |
||||
|
function startReview() { |
||||
|
if (uploading.value) { |
||||
|
message.warning('文件正在上传中,请稍后再试'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (!fileList.value || fileList.value.length === 0) { |
||||
|
message.warning('请先上传文件'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 使用保存的OSS ID进行审查 |
||||
|
if (!currentOssId.value) { |
||||
|
message.warning('文件上传异常,请重新上传'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 打开审查弹窗,传入OssId |
||||
|
openReviewDialog(true, { |
||||
|
ossId: currentOssId.value |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 审查弹窗回调 |
||||
|
function handleReviewSuccess(data: any) { |
||||
|
console.log('Review completed with data:', data); |
||||
|
// 这里可以处理审查成功后的逻辑 |
||||
|
message.success('审查设置已完成,开始分析合同'); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.upload-container { |
||||
|
background-color: #fff; |
||||
|
border-radius: 12px; |
||||
|
padding: 30px; |
||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
||||
|
margin-top: 30px; |
||||
|
max-width: 1100px; |
||||
|
margin-left: auto; |
||||
|
margin-right: auto; |
||||
|
border: 1px solid rgba(0, 206, 177, 0.2); |
||||
|
} |
||||
|
|
||||
|
.upload-box { |
||||
|
border: 1px dashed #ddd; |
||||
|
border-radius: 8px; |
||||
|
padding: 20px; |
||||
|
text-align: center; |
||||
|
cursor: pointer; |
||||
|
margin-bottom: 30px; |
||||
|
transition: border-color 0.3s; |
||||
|
width: 100%; |
||||
|
height: 280px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
background-color: #f9f9f9; |
||||
|
} |
||||
|
|
||||
|
.upload-box:hover { |
||||
|
border-color: #1890ff; |
||||
|
background-color: rgba(24, 144, 255, 0.02); |
||||
|
} |
||||
|
|
||||
|
.upload-box.file-preview { |
||||
|
border: 2px solid #e6f7ff; |
||||
|
background-color: #f0f8ff; |
||||
|
cursor: default; |
||||
|
} |
||||
|
|
||||
|
.upload-content { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.upload-icon { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
margin-bottom: 20px; |
||||
|
background-color: #e6f7ff; |
||||
|
width: 60px; |
||||
|
height: 60px; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
|
||||
|
.upload-arrow-icon { |
||||
|
font-size: 28px; |
||||
|
color: #1890ff; |
||||
|
} |
||||
|
|
||||
|
.upload-text-container { |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.upload-text { |
||||
|
font-size: 18px; |
||||
|
color: #333; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.upload-link { |
||||
|
color: #1890ff; |
||||
|
cursor: pointer; |
||||
|
text-decoration: none; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.upload-link:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
|
||||
|
.upload-hint { |
||||
|
color: #888; |
||||
|
font-size: 14px; |
||||
|
margin-top: 8px; |
||||
|
margin-bottom: 15px; |
||||
|
} |
||||
|
|
||||
|
.sample-link { |
||||
|
color: #1890ff; |
||||
|
font-size: 14px; |
||||
|
cursor: pointer; |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
|
||||
|
.sample-link:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
|
||||
|
/* 文件预览区域 */ |
||||
|
.file-info { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
max-width: 800px; |
||||
|
} |
||||
|
|
||||
|
.file-icon { |
||||
|
font-size: 36px; |
||||
|
color: #1890ff; |
||||
|
margin-right: 20px; |
||||
|
} |
||||
|
|
||||
|
.file-details { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.file-name { |
||||
|
font-size: 18px; |
||||
|
font-weight: 500; |
||||
|
margin: 0 0 10px 0; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.file-progress { |
||||
|
margin: 0 0 10px 0; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.file-status { |
||||
|
font-size: 14px; |
||||
|
color: #666; |
||||
|
margin: 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.status-icon { |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
|
||||
|
.status-icon.success { |
||||
|
color: #52c41a; |
||||
|
} |
||||
|
|
||||
|
.remove-btn { |
||||
|
padding: 0; |
||||
|
margin-left: 15px; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.action-buttons { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.start-button { |
||||
|
width: 180px; |
||||
|
height: 44px; |
||||
|
background-color: #52c41a; |
||||
|
border-color: #52c41a; |
||||
|
font-size: 16px; |
||||
|
border-radius: 22px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,903 @@ |
|||||
|
<template> |
||||
|
<BasicModal |
||||
|
v-bind="$attrs" |
||||
|
@register="register" |
||||
|
title="合同审查" |
||||
|
:defaultFullscreen="true" |
||||
|
:maskClosable="false" |
||||
|
:keyboard="false" |
||||
|
@cancel="handleCancel" |
||||
|
:okText="'开始分析'" |
||||
|
@ok="handleConfirm" |
||||
|
> |
||||
|
<div class="review-dialog-content"> |
||||
|
<!-- 加载状态 --> |
||||
|
<div class="loading-container" v-if="analyzing"> |
||||
|
<div class="loading-spinner"> |
||||
|
<LoadingOutlined spin /> |
||||
|
</div> |
||||
|
<div class="loading-text"> |
||||
|
<p>正在分析合同文件...</p> |
||||
|
<p class="sub-text">请稍候,这可能需要几分钟时间</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 分析完成后的内容 --> |
||||
|
<div v-else> |
||||
|
<!-- 选择立场 --> |
||||
|
<div class="section position-section"> |
||||
|
<h3 class="section-title">选择你的立场</h3> |
||||
|
<p class="section-description">选定你的合同审查立场</p> |
||||
|
|
||||
|
<div class="position-options"> |
||||
|
<div |
||||
|
class="position-card" |
||||
|
:class="{ selected: selectedPosition === 'party-a' }" |
||||
|
@click="selectPosition('party-a')" |
||||
|
> |
||||
|
<div class="card-header">甲方</div> |
||||
|
<div class="card-body"> |
||||
|
<h3>{{ contractParties.partyA }}</h3> |
||||
|
</div> |
||||
|
<div class="card-footer"> |
||||
|
<span class="select-text" v-if="selectedPosition === 'party-a'">选中</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div |
||||
|
class="position-card" |
||||
|
:class="{ selected: selectedPosition === 'neutral' }" |
||||
|
@click="selectPosition('neutral')" |
||||
|
> |
||||
|
<div class="card-header">中立</div> |
||||
|
<div class="card-body"> |
||||
|
<h3>中立审查</h3> |
||||
|
</div> |
||||
|
<div class="card-footer"> |
||||
|
<span class="select-text" v-if="selectedPosition === 'neutral'">选中</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div |
||||
|
class="position-card" |
||||
|
:class="{ selected: selectedPosition === 'party-b' }" |
||||
|
@click="selectPosition('party-b')" |
||||
|
> |
||||
|
<div class="card-header">乙方</div> |
||||
|
<div class="card-body"> |
||||
|
<h3>{{ contractParties.partyB }}</h3> |
||||
|
</div> |
||||
|
<div class="card-footer"> |
||||
|
<span class="select-text" v-if="selectedPosition === 'party-b'">选中</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 自定义审查清单 --> |
||||
|
<div class="section checklist-section"> |
||||
|
<h3 class="section-title">设置自定义审查清单(可选)</h3> |
||||
|
<p class="section-description">系统自动解析合同条款,通过 AI 智能生成审查清单,按照审查清单审查合同。</p> |
||||
|
|
||||
|
<div class="checklist-selector"> |
||||
|
<Select |
||||
|
v-model:value="selectedGroupId" |
||||
|
style="width: 100%" |
||||
|
placeholder="请选择审查清单" |
||||
|
:loading="loading" |
||||
|
@change="handleChecklistChange" |
||||
|
size="large" |
||||
|
:dropdownMatchSelectWidth="false" |
||||
|
dropdownClassName="checklist-dropdown" |
||||
|
> |
||||
|
<!-- AI自动生成选项 --> |
||||
|
<Select.Option value="ai" class="ai-option"> |
||||
|
<div class="checklist-option-content"> |
||||
|
<div class="option-left"> |
||||
|
<RobotOutlined class="ai-icon" /> |
||||
|
<span class="option-name">AI自动生成</span> |
||||
|
</div> |
||||
|
<span class="option-desc">智能分析合同内容生成审查清单</span> |
||||
|
</div> |
||||
|
</Select.Option> |
||||
|
|
||||
|
<!-- 分隔线 --> |
||||
|
<Select.Divider /> |
||||
|
|
||||
|
<!-- 用户的审查清单 --> |
||||
|
<Select.Option |
||||
|
v-for="group in checklistGroups" |
||||
|
:key="group.groupId" |
||||
|
:value="group.groupId" |
||||
|
> |
||||
|
<div class="checklist-option-content"> |
||||
|
<div class="option-left"> |
||||
|
<FileTextOutlined class="checklist-icon" /> |
||||
|
<span class="option-name">{{ group.name }}</span> |
||||
|
</div> |
||||
|
<span class="option-count">{{ group.checklistItemNum }}项</span> |
||||
|
</div> |
||||
|
</Select.Option> |
||||
|
</Select> |
||||
|
|
||||
|
<!-- 新建清单按钮 --> |
||||
|
<Button type="link" class="add-checklist-btn" @click="handleAddChecklist" size="large"> |
||||
|
<PlusOutlined />新建清单 |
||||
|
</Button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 高级设置内容区域 --> |
||||
|
<div class="advanced-settings-content"> |
||||
|
<!-- 设置审查重点 --> |
||||
|
<div class="section review-focus-section"> |
||||
|
<div class="section-header"> |
||||
|
<BulbOutlined class="section-icon" /> |
||||
|
<h3 class="section-title">设置审查重点</h3> |
||||
|
</div> |
||||
|
<p class="section-description">编辑设置合同重点,影响代表方的审查倾向</p> |
||||
|
|
||||
|
<div class="review-focus-controls"> |
||||
|
<div class="toggle-container"> |
||||
|
<Switch v-model:checked="autoGenerateReviewPoints" /> |
||||
|
<span :class="{ 'enabled-text': autoGenerateReviewPoints, 'disabled-text': !autoGenerateReviewPoints }"> |
||||
|
{{ autoGenerateReviewPoints ? '启用' : '禁用' }} |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 审查重点列表 --> |
||||
|
<div class="review-points-list" :class="{ 'disabled': !autoGenerateReviewPoints }"> |
||||
|
<!-- 已有的审查重点 --> |
||||
|
<div |
||||
|
v-for="(point, index) in reviewPoints" |
||||
|
:key="point.id" |
||||
|
class="review-point-item" |
||||
|
> |
||||
|
<div class="review-point-number">{{ index + 1 }}.</div> |
||||
|
<div class="review-point-content" v-if="editingPointIndex !== index"> |
||||
|
{{ point.content }} |
||||
|
</div> |
||||
|
<div class="review-point-content edit-mode" v-else> |
||||
|
<Input |
||||
|
v-model:value="newPointContent" |
||||
|
placeholder="输入审查重点内容" |
||||
|
@pressEnter="saveEditReviewPoint" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="review-point-action" v-if="autoGenerateReviewPoints"> |
||||
|
<EditOutlined |
||||
|
class="edit-icon" |
||||
|
v-if="editingPointIndex !== index" |
||||
|
@click="startEditReviewPoint(index)" |
||||
|
/> |
||||
|
<div v-if="editingPointIndex === index" class="edit-actions"> |
||||
|
<CheckCircleFilled class="confirm-icon" @click="saveEditReviewPoint" /> |
||||
|
<MinusCircleOutlined class="cancel-icon" @click="cancelEditReviewPoint" /> |
||||
|
</div> |
||||
|
<MinusCircleOutlined |
||||
|
v-if="editingPointIndex !== index" |
||||
|
class="delete-icon" |
||||
|
@click="deleteReviewPoint(index)" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 新增审查重点的输入框 --> |
||||
|
<div class="review-point-item" v-if="isAddingNewPoint && autoGenerateReviewPoints"> |
||||
|
<div class="review-point-number">{{ reviewPoints.length + 1 }}.</div> |
||||
|
<div class="review-point-content edit-mode"> |
||||
|
<Input |
||||
|
v-model:value="newPointContent" |
||||
|
placeholder="输入新的审查重点内容" |
||||
|
@pressEnter="addReviewPoint" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="review-point-action"> |
||||
|
<div class="edit-actions"> |
||||
|
<CheckCircleFilled class="confirm-icon" @click="addReviewPoint" /> |
||||
|
<MinusCircleOutlined class="cancel-icon" @click="cancelAddNewPoint" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 添加按钮 --> |
||||
|
<div class="review-point-add" v-if="autoGenerateReviewPoints && !isAddingNewPoint"> |
||||
|
<Button class="add-point-btn" type="primary" @click="startAddNewPoint"> |
||||
|
<PlusOutlined /> |
||||
|
添加 |
||||
|
</Button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 选择合同审查组件 --> |
||||
|
<div class="section review-components-section"> |
||||
|
<div class="section-header"> |
||||
|
<BarsOutlined class="section-icon" /> |
||||
|
<h3 class="section-title">选择合同审查组件</h3> |
||||
|
</div> |
||||
|
<p class="section-description">系统将根据审查清单中的具体审查要求及参考资料审查合同。</p> |
||||
|
|
||||
|
<div class="review-components"> |
||||
|
<div |
||||
|
class="review-component-card" |
||||
|
v-for="component in reviewComponents" |
||||
|
:key="component.id" |
||||
|
:class="{ active: component.selected }" |
||||
|
@click="toggleReviewComponent(component.id)" |
||||
|
> |
||||
|
<CheckCircleFilled class="check-icon" v-if="component.selected" /> |
||||
|
<div class="component-icon"> |
||||
|
<SafetyCertificateOutlined v-if="component.icon === 'SafetyCertificateOutlined'" /> |
||||
|
<FileTextOutlined v-if="component.icon === 'FileTextOutlined'" /> |
||||
|
</div> |
||||
|
<h4 class="component-title">{{ component.name }}</h4> |
||||
|
<p class="component-desc">{{ component.description }}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</BasicModal> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref } from 'vue'; |
||||
|
import { BasicModal, useModalInner } from '@/components/Modal'; |
||||
|
import { Button, Switch, Input, Select } from 'ant-design-vue'; |
||||
|
import { |
||||
|
LoadingOutlined, |
||||
|
CheckCircleFilled, |
||||
|
PlusOutlined, |
||||
|
MinusCircleOutlined, |
||||
|
BulbOutlined, |
||||
|
BarsOutlined, |
||||
|
SafetyCertificateOutlined, |
||||
|
FileTextOutlined, |
||||
|
EditOutlined, |
||||
|
RobotOutlined |
||||
|
} from '@ant-design/icons-vue'; |
||||
|
import { message } from 'ant-design-vue'; |
||||
|
import { AnalyzeContract } from '@/api/contractReview/ContractualTasks'; |
||||
|
import { ContractualTaskChecklistQueryList } from '@/api/contractReview/ContractualTaskChecklist'; |
||||
|
import type { SelectValue } from 'ant-design-vue/es/select'; |
||||
|
|
||||
|
// 定义组件接收的属性 |
||||
|
const props = defineProps({ |
||||
|
ossId: { |
||||
|
type: String, |
||||
|
default: '', |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
// 发出的事件 |
||||
|
const emit = defineEmits(['success', 'register', 'cancel']); |
||||
|
|
||||
|
// 状态变量 |
||||
|
const analyzing = ref(true); |
||||
|
const selectedPosition = ref(''); |
||||
|
const autoGenerateReviewPoints = ref(true); |
||||
|
const editingPointIndex = ref(-1); |
||||
|
const newPointContent = ref(''); |
||||
|
const isAddingNewPoint = ref(false); |
||||
|
|
||||
|
// 审查重点列表 |
||||
|
const reviewPoints = ref([ |
||||
|
{ id: 1, content: '保密信息的定义是否全面覆盖了甲方需要保护的内容' }, |
||||
|
{ id: 2, content: '乙方对保密信息的使用范围是否明确且受限于项目评估和准备' }, |
||||
|
{ id: 3, content: '违约责任中关于信息披露的赔偿条款是否足够保护甲方利益' }, |
||||
|
]); |
||||
|
|
||||
|
// 审查组件选中状态 (默认都选中) |
||||
|
const reviewComponents = ref([ |
||||
|
{ |
||||
|
id: 'textSymbol', |
||||
|
name: '文本检查', |
||||
|
icon: 'SafetyCertificateOutlined', |
||||
|
selected: true, |
||||
|
description: '分析合同内容中是否存在未使用法言法语、过于开放性描述、指代不明确、表述存在歧义、前后不统一、表述不规范等风险。' |
||||
|
}, |
||||
|
{ |
||||
|
id: 'mainBody', |
||||
|
name: '主体审查', |
||||
|
icon: 'FileTextOutlined', |
||||
|
selected: true, |
||||
|
description: '调取合同相对方的信息,分析相关主体的资信能力以及是否具备合同签署的资质或许可。' |
||||
|
} |
||||
|
]); |
||||
|
|
||||
|
// 模拟甲乙方信息 |
||||
|
const contractParties = ref({ |
||||
|
partyA: '企查查科技股份有限公司', |
||||
|
partyB: '北京柒腾科技股份有限公司', |
||||
|
fileName: '保密协议' |
||||
|
}); |
||||
|
|
||||
|
// 审查清单相关 |
||||
|
const loading = ref(false); |
||||
|
const checklistGroups = ref<any[]>([]); |
||||
|
const selectedGroupId = ref<string>('ai'); // 默认选中AI自动生成 |
||||
|
|
||||
|
// 加载审查清单 |
||||
|
const loadChecklists = async () => { |
||||
|
loading.value = true; |
||||
|
try { |
||||
|
const res = await ContractualTaskChecklistQueryList(); |
||||
|
checklistGroups.value = res; |
||||
|
} catch (error) { |
||||
|
console.error('加载审查清单失败:', error); |
||||
|
message.error('加载审查清单失败'); |
||||
|
} finally { |
||||
|
loading.value = false; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 处理清单选择变化 |
||||
|
const handleChecklistChange = (value: SelectValue) => { |
||||
|
const strValue = String(value); |
||||
|
if (strValue === 'ai') { |
||||
|
message.success('已选择AI自动生成审查清单'); |
||||
|
} else { |
||||
|
const selectedGroup = checklistGroups.value.find(group => group.groupId === strValue); |
||||
|
if (selectedGroup) { |
||||
|
message.success(`已选择清单:${selectedGroup.name}`); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 新建清单 |
||||
|
const handleAddChecklist = () => { |
||||
|
message.info('新建清单功能开发中'); |
||||
|
}; |
||||
|
|
||||
|
// 使用modalInner注册modal,并在弹窗打开时启动分析过程 |
||||
|
const [register, { closeModal }] = useModalInner((data) => { |
||||
|
// 弹窗打开时,重置状态 |
||||
|
analyzing.value = true; |
||||
|
selectedPosition.value = ''; |
||||
|
autoGenerateReviewPoints.value = true; |
||||
|
editingPointIndex.value = -1; |
||||
|
newPointContent.value = ''; |
||||
|
isAddingNewPoint.value = false; |
||||
|
|
||||
|
// 重置审查组件为默认选中状态 |
||||
|
reviewComponents.value.forEach(component => { |
||||
|
component.selected = true; |
||||
|
}); |
||||
|
|
||||
|
console.log('Modal opened with data:', data); |
||||
|
|
||||
|
// 只有当弹窗打开后,才开始分析合同文档 |
||||
|
if (data && data.ossId) { |
||||
|
console.log('Received ossId:', data.ossId); |
||||
|
// 显示加载提示 |
||||
|
message.loading({ content: '正在分析合同文件...', duration: 0, key: 'analyzing' }); |
||||
|
|
||||
|
// 调用分析接口 |
||||
|
AnalyzeContract(data.ossId) |
||||
|
.then((res) => { |
||||
|
// 处理分析结果 |
||||
|
console.log('Contract analysis result:', res); |
||||
|
|
||||
|
// 如果需要,可以从结果中提取甲乙方信息等 |
||||
|
if (res.data) { |
||||
|
// 根据返回数据更新界面信息 |
||||
|
// 例如:根据返回数据更新甲乙方信息等 |
||||
|
if (res.data.partyA) { |
||||
|
contractParties.value.partyA = res.data.partyA; |
||||
|
} |
||||
|
if (res.data.partyB) { |
||||
|
contractParties.value.partyB = res.data.partyB; |
||||
|
} |
||||
|
if (res.data.fileName) { |
||||
|
contractParties.value.fileName = res.data.fileName; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 分析完成,更新状态 |
||||
|
analyzing.value = false; |
||||
|
message.success({ content: '合同分析完成', key: 'analyzing' }); |
||||
|
}) |
||||
|
.catch((error) => { |
||||
|
console.error('Contract analysis failed:', error); |
||||
|
analyzing.value = false; |
||||
|
message.error({ content: '合同分析失败: ' + (error.message || '未知错误'), key: 'analyzing' }); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 在弹窗打开时加载清单 |
||||
|
loadChecklists(); |
||||
|
}); |
||||
|
|
||||
|
// 选择立场 |
||||
|
function selectPosition(position: string) { |
||||
|
selectedPosition.value = position; |
||||
|
} |
||||
|
|
||||
|
// 切换审查组件选中状态 |
||||
|
function toggleReviewComponent(componentId: string) { |
||||
|
const component = reviewComponents.value.find(item => item.id === componentId); |
||||
|
if (component) { |
||||
|
component.selected = !component.selected; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 删除审查重点 |
||||
|
function deleteReviewPoint(index: number) { |
||||
|
if (autoGenerateReviewPoints.value) { |
||||
|
reviewPoints.value.splice(index, 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 开始编辑审查重点 |
||||
|
function startEditReviewPoint(index: number) { |
||||
|
if (autoGenerateReviewPoints.value) { |
||||
|
editingPointIndex.value = index; |
||||
|
newPointContent.value = reviewPoints.value[index].content; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 保存编辑的审查重点 |
||||
|
function saveEditReviewPoint() { |
||||
|
if (editingPointIndex.value >= 0) { |
||||
|
if (newPointContent.value.trim()) { |
||||
|
reviewPoints.value[editingPointIndex.value].content = newPointContent.value.trim(); |
||||
|
} |
||||
|
editingPointIndex.value = -1; |
||||
|
newPointContent.value = ''; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 添加新的审查重点 |
||||
|
function addReviewPoint() { |
||||
|
if (autoGenerateReviewPoints.value) { |
||||
|
if (newPointContent.value.trim()) { |
||||
|
const newId = reviewPoints.value.length > 0 ? Math.max(...reviewPoints.value.map(p => p.id)) + 1 : 1; |
||||
|
reviewPoints.value.push({ |
||||
|
id: newId, |
||||
|
content: newPointContent.value.trim() |
||||
|
}); |
||||
|
newPointContent.value = ''; |
||||
|
isAddingNewPoint.value = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 开始添加新的审查重点 |
||||
|
function startAddNewPoint() { |
||||
|
if (autoGenerateReviewPoints.value) { |
||||
|
isAddingNewPoint.value = true; |
||||
|
newPointContent.value = ''; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 取消添加或编辑 |
||||
|
function cancelEditReviewPoint() { |
||||
|
editingPointIndex.value = -1; |
||||
|
newPointContent.value = ''; |
||||
|
} |
||||
|
|
||||
|
// 取消添加新的审查重点 |
||||
|
function cancelAddNewPoint() { |
||||
|
isAddingNewPoint.value = false; |
||||
|
newPointContent.value = ''; |
||||
|
} |
||||
|
|
||||
|
// 取消按钮处理 |
||||
|
function handleCancel() { |
||||
|
closeModal(); |
||||
|
emit('cancel'); |
||||
|
} |
||||
|
|
||||
|
// 确认按钮处理 |
||||
|
function handleConfirm() { |
||||
|
// 检查是否选择了立场 |
||||
|
if (!selectedPosition.value) { |
||||
|
// 这里应该添加提示,要求选择立场 |
||||
|
message.warning('请选择您的立场'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 检查是否至少选择了一个审查组件 |
||||
|
const hasSelectedComponent = reviewComponents.value.some(component => component.selected); |
||||
|
if (!hasSelectedComponent) { |
||||
|
message.warning('请至少选择一个合同审查组件'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 关闭弹窗,触发成功事件 |
||||
|
closeModal(); |
||||
|
emit('success', { |
||||
|
position: selectedPosition.value, |
||||
|
reviewComponents: reviewComponents.value.filter(item => item.selected).map(item => item.id), |
||||
|
reviewPoints: !autoGenerateReviewPoints.value ? [] : reviewPoints.value, |
||||
|
// 可以添加其他需要传递的数据 |
||||
|
}); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
.review-dialog-content { |
||||
|
padding: 20px; |
||||
|
} |
||||
|
|
||||
|
// 加载状态 |
||||
|
.loading-container { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 30px; |
||||
|
min-height: 300px; |
||||
|
} |
||||
|
|
||||
|
.loading-spinner { |
||||
|
font-size: 36px; |
||||
|
margin-right: 20px; |
||||
|
} |
||||
|
|
||||
|
.loading-text { |
||||
|
p { |
||||
|
font-size: 18px; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.sub-text { |
||||
|
font-size: 14px; |
||||
|
color: #666; |
||||
|
margin-top: 8px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 各部分通用样式 |
||||
|
.section { |
||||
|
margin-bottom: 30px; |
||||
|
border-bottom: 1px dashed #eee; |
||||
|
padding-bottom: 20px; |
||||
|
|
||||
|
&:last-child { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.section-title { |
||||
|
font-size: 20px; |
||||
|
margin-bottom: 8px; |
||||
|
font-weight: 500; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.section-description { |
||||
|
color: #666; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.section-header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin-bottom: 8px; |
||||
|
|
||||
|
.section-icon { |
||||
|
font-size: 20px; |
||||
|
margin-right: 8px; |
||||
|
color: #1890ff; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 选择立场 |
||||
|
.position-options { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
gap: 20px; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.position-card { |
||||
|
flex: 1; |
||||
|
border: 1px solid #e8e8e8; |
||||
|
border-radius: 8px; |
||||
|
overflow: hidden; |
||||
|
transition: all 0.3s; |
||||
|
cursor: pointer; |
||||
|
|
||||
|
&:hover { |
||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
||||
|
border-color: #1890ff; |
||||
|
} |
||||
|
|
||||
|
&.selected { |
||||
|
border-color: #52c41a; |
||||
|
box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.2); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.card-header { |
||||
|
background-color: #f7f7f7; |
||||
|
padding: 12px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.card-body { |
||||
|
padding: 20px; |
||||
|
min-height: 100px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
|
||||
|
h3 { |
||||
|
margin: 0; |
||||
|
text-align: center; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.card-footer { |
||||
|
padding: 10px; |
||||
|
text-align: right; |
||||
|
border-top: 1px solid #f0f0f0; |
||||
|
|
||||
|
.select-text { |
||||
|
color: #1890ff; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 自定义审查清单 |
||||
|
.checklist-selector { |
||||
|
padding: 24px; |
||||
|
|
||||
|
:deep(.ant-select) { |
||||
|
.ant-select-selector { |
||||
|
height: 56px !important; |
||||
|
padding: 0 16px !important; |
||||
|
|
||||
|
.ant-select-selection-search { |
||||
|
height: 54px !important; |
||||
|
|
||||
|
input { |
||||
|
height: 54px !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.ant-select-selection-item { |
||||
|
height: 54px !important; |
||||
|
line-height: 54px !important; |
||||
|
font-size: 16px !important; |
||||
|
} |
||||
|
|
||||
|
.ant-select-selection-placeholder { |
||||
|
height: 54px !important; |
||||
|
line-height: 54px !important; |
||||
|
font-size: 16px !important; |
||||
|
display: flex !important; |
||||
|
align-items: center !important; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.checklist-dropdown { |
||||
|
min-width: 500px !important; |
||||
|
|
||||
|
:deep(.ant-select-item) { |
||||
|
padding: 12px 16px !important; |
||||
|
font-size: 14px !important; |
||||
|
min-height: 48px !important; |
||||
|
display: flex !important; |
||||
|
align-items: center !important; |
||||
|
|
||||
|
&-option-content { |
||||
|
white-space: normal; |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.checklist-option-content { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
width: 100%; |
||||
|
|
||||
|
.option-left { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
|
||||
|
.ai-icon { |
||||
|
font-size: 20px; |
||||
|
color: #52c41a; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.checklist-icon { |
||||
|
font-size: 20px; |
||||
|
color: #1890ff; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.option-name { |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.option-desc { |
||||
|
color: #666; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.option-count { |
||||
|
color: #666; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 高级设置内容区域 |
||||
|
.advanced-settings-content { |
||||
|
margin-top: 30px; |
||||
|
padding: 20px; |
||||
|
background-color: #f9f9f9; |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
|
||||
|
// 审查重点部分 |
||||
|
.review-focus-controls { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
align-items: center; |
||||
|
margin-bottom: 20px; |
||||
|
|
||||
|
.toggle-container { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
.enabled-text { |
||||
|
color: #52c41a; |
||||
|
} |
||||
|
|
||||
|
.disabled-text { |
||||
|
color: #999; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.review-points-list { |
||||
|
background-color: #fff; |
||||
|
border-radius: 4px; |
||||
|
overflow: hidden; |
||||
|
border: 1px solid #eee; |
||||
|
|
||||
|
&.disabled { |
||||
|
opacity: 0.7; |
||||
|
pointer-events: none; |
||||
|
filter: grayscale(0.5); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.review-point-item { |
||||
|
display: flex; |
||||
|
padding: 15px; |
||||
|
border-bottom: 1px solid #f0f0f0; |
||||
|
|
||||
|
&:last-child { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
|
||||
|
.review-point-number { |
||||
|
flex: 0 0 30px; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.review-point-content { |
||||
|
flex: 1; |
||||
|
|
||||
|
&.edit-mode { |
||||
|
padding-right: 10px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.review-point-action { |
||||
|
width: 60px; |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
align-items: center; |
||||
|
gap: 10px; |
||||
|
|
||||
|
.delete-icon { |
||||
|
color: #ff4d4f; |
||||
|
cursor: pointer; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.edit-icon { |
||||
|
color: #1890ff; |
||||
|
cursor: pointer; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.edit-actions { |
||||
|
display: flex; |
||||
|
gap: 10px; |
||||
|
|
||||
|
.confirm-icon { |
||||
|
color: #52c41a; |
||||
|
cursor: pointer; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.cancel-icon { |
||||
|
color: #ff4d4f; |
||||
|
cursor: pointer; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.review-point-add { |
||||
|
padding: 15px; |
||||
|
text-align: center; |
||||
|
|
||||
|
.add-point-btn { |
||||
|
background-color: #52c41a; |
||||
|
border-color: #52c41a; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 审查组件部分 |
||||
|
.review-components { |
||||
|
display: flex; |
||||
|
gap: 20px; |
||||
|
margin-top: 20px; |
||||
|
} |
||||
|
|
||||
|
.review-component-card { |
||||
|
flex: 1; |
||||
|
padding: 20px; |
||||
|
border: 1px solid #e8e8e8; |
||||
|
border-radius: 4px; |
||||
|
background-color: #fff; |
||||
|
position: relative; |
||||
|
transition: all 0.3s ease; |
||||
|
cursor: pointer; |
||||
|
|
||||
|
&:hover { |
||||
|
border-color: #1890ff; |
||||
|
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15); |
||||
|
} |
||||
|
|
||||
|
&.active { |
||||
|
border-color: #52c41a; |
||||
|
box-shadow: 0 0 0 1px rgba(82, 196, 26, 0.2); |
||||
|
background-color: #f6ffed; |
||||
|
} |
||||
|
|
||||
|
.check-icon { |
||||
|
position: absolute; |
||||
|
top: 10px; |
||||
|
right: 10px; |
||||
|
color: #52c41a; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.component-icon { |
||||
|
font-size: 24px; |
||||
|
color: #1890ff; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.component-title { |
||||
|
font-size: 16px; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
.component-desc { |
||||
|
font-size: 12px; |
||||
|
color: #666; |
||||
|
line-height: 1.5; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,220 @@ |
|||||
|
<template> |
||||
|
<PageWrapper dense> |
||||
|
<BasicTable @register="registerTable"> |
||||
|
<template #toolbar> |
||||
|
<!-- <a-button |
||||
|
@click="downloadExcel(ContractualTasksExport, '合同任务数据', getForm().getFieldsValue())" |
||||
|
v-auth="'productManagement:ContractualTasks:export'" |
||||
|
>导出</a-button |
||||
|
> |
||||
|
<a-button |
||||
|
type="primary" |
||||
|
danger |
||||
|
@click="multipleRemove(ContractualTasksRemove)" |
||||
|
:disabled="!selected" |
||||
|
v-auth="'productManagement:ContractualTasks:remove'" |
||||
|
>删除</a-button |
||||
|
> --> |
||||
|
<a-button |
||||
|
type="primary" |
||||
|
@click="handleAdd" |
||||
|
v-auth="'productManagement:ContractualTasks:add'" |
||||
|
>新增</a-button |
||||
|
> |
||||
|
</template> |
||||
|
<template #bodyCell="{ column, record }"> |
||||
|
<template v-if="column.key === 'action'"> |
||||
|
<TableAction |
||||
|
stopButtonPropagation |
||||
|
:actions="[ |
||||
|
{ |
||||
|
label: '详情', |
||||
|
icon: IconEnum.EDIT, |
||||
|
type: 'primary', |
||||
|
ghost: true, |
||||
|
ifShow: () => { |
||||
|
if ( |
||||
|
record.progressStatus != 'PENDING' && |
||||
|
record.progressStatus != 'STARTED' && |
||||
|
record.progressStatus != 'REVOKED' |
||||
|
) { |
||||
|
return true; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
onClick: handleDetail.bind(null, record), |
||||
|
}, |
||||
|
{ |
||||
|
label: '下载', |
||||
|
icon: IconEnum.DOWNLOAD, |
||||
|
type: 'primary', |
||||
|
color: 'success', |
||||
|
ghost: true, |
||||
|
ifShow: () => { |
||||
|
if ( |
||||
|
record.progressStatus != 'PENDING' && |
||||
|
record.progressStatus != 'STARTED' && |
||||
|
record.progressStatus != 'REVOKED' |
||||
|
) { |
||||
|
return true; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
onClick: handleDownload.bind(null, record), |
||||
|
}, |
||||
|
{ |
||||
|
label: '终止任务', |
||||
|
icon: IconEnum.DELETE, |
||||
|
type: 'primary', |
||||
|
danger: true, |
||||
|
ghost: true, |
||||
|
ifShow: () => { |
||||
|
if (record.progressStatus == 'PENDING') { |
||||
|
return true; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
popConfirm: { |
||||
|
placement: 'left', |
||||
|
title: '是否终止当前任务?', |
||||
|
confirm: handleStop.bind(null, record), |
||||
|
}, |
||||
|
}, |
||||
|
]" |
||||
|
/> |
||||
|
</template> |
||||
|
</template> |
||||
|
</BasicTable> |
||||
|
<ContractualTasksModal @register="registerModal" @reload="reload" /> |
||||
|
<DocsDrawer @register="registerDrawer" /> |
||||
|
<ResultDetailDrawer |
||||
|
:visible="resultDetailDrawerVisible" |
||||
|
:taskResultDetail="taskResultDetail" |
||||
|
:taskInfo="currentTaskInfo" |
||||
|
@update:visible="resultDetailDrawerVisible = $event" |
||||
|
@close="handleResultDetailDrawerClose" |
||||
|
/> |
||||
|
</PageWrapper> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { PageWrapper } from '@/components/Page'; |
||||
|
import { BasicTable, useTable, TableAction } from '@/components/Table'; |
||||
|
|
||||
|
import { downloadExcel } from '@/utils/file/download'; |
||||
|
import { useModal } from '@/components/Modal'; |
||||
|
import ContractualTasksModal from './ContractualTasksModal.vue'; |
||||
|
import { formSchemas, columns } from './ContractualTasks.data'; |
||||
|
import { IconEnum } from '@/enums/appEnum'; |
||||
|
import DocsDrawer from '@/views/documentReview/DocumentTasks/DocsDrawer.vue'; |
||||
|
import { useDrawer } from '@/components/Drawer'; |
||||
|
import { DocumentTasksStop } from '@/api/documentReview/DocumentTasks'; |
||||
|
import { |
||||
|
ContractualTasksList, |
||||
|
ContractualTasksExport, |
||||
|
ContractualTasksRemove, |
||||
|
} from '@/api/contractReview/ContractualTasks'; |
||||
|
import { |
||||
|
DocumentTaskResultsInfoByTaskId, |
||||
|
DocumentTaskResultDownload, |
||||
|
getDetailResultsByTaskId, |
||||
|
} from '@/api/documentReview/DocumentTaskResults'; |
||||
|
import { ref } from 'vue'; |
||||
|
import ResultDetailDrawer from '@/views/documentReview/DocumentTasks/ResultDetailDrawer.vue'; |
||||
|
import { DocumentTaskResultDetailVO } from '@/api/documentReview/DocumentTaskResults/model'; |
||||
|
|
||||
|
const [registerDrawer, { openDrawer }] = useDrawer(); |
||||
|
const resultDetailDrawerVisible = ref(false); |
||||
|
const taskResultDetail = ref<DocumentTaskResultDetailVO[]>([]); |
||||
|
const currentTaskInfo = ref<Recordable>({}); |
||||
|
|
||||
|
defineOptions({ name: 'ContractualTasks' }); |
||||
|
|
||||
|
const [registerTable, { reload, multipleRemove, selected, getForm }] = useTable({ |
||||
|
rowSelection: { |
||||
|
type: 'checkbox', |
||||
|
}, |
||||
|
title: '合同任务列表', |
||||
|
api: ContractualTasksList, |
||||
|
showIndexColumn: false, |
||||
|
rowKey: 'id', |
||||
|
useSearchForm: true, |
||||
|
formConfig: { |
||||
|
schemas: formSchemas, |
||||
|
baseColProps: { |
||||
|
xs: 24, |
||||
|
sm: 24, |
||||
|
md: 24, |
||||
|
lg: 6, |
||||
|
}, |
||||
|
}, |
||||
|
columns: columns, |
||||
|
actionColumn: { |
||||
|
width: 200, |
||||
|
title: '操作', |
||||
|
key: 'action', |
||||
|
fixed: 'right', |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const [registerModal, { openModal }] = useModal(); |
||||
|
|
||||
|
async function handleDetail(record: Recordable) { |
||||
|
try { |
||||
|
let res = await DocumentTaskResultsInfoByTaskId(record.id); |
||||
|
|
||||
|
if (!res || !res.result) { |
||||
|
try { |
||||
|
const detailRes = await getDetailResultsByTaskId(record.id); |
||||
|
if (detailRes && detailRes.length > 0) { |
||||
|
taskResultDetail.value = detailRes; |
||||
|
currentTaskInfo.value = record; |
||||
|
resultDetailDrawerVisible.value = true; |
||||
|
return; |
||||
|
} |
||||
|
} catch (detailEx) { |
||||
|
console.error('获取详细结果失败', detailEx); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
openDrawer(true, { value: res.result, type: 'markdown' }); |
||||
|
} catch (ex) { |
||||
|
try { |
||||
|
const detailRes = await getDetailResultsByTaskId(record.id); |
||||
|
if (detailRes && detailRes.length > 0) { |
||||
|
taskResultDetail.value = detailRes; |
||||
|
currentTaskInfo.value = record; |
||||
|
resultDetailDrawerVisible.value = true; |
||||
|
return; |
||||
|
} |
||||
|
} catch (detailEx) { |
||||
|
console.error('获取详细结果也失败', detailEx); |
||||
|
openDrawer(true, { value: '加载失败,请刷新页面', type: 'markdown' }); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handleResultDetailDrawerClose() { |
||||
|
resultDetailDrawerVisible.value = false; |
||||
|
} |
||||
|
|
||||
|
async function handleStop(record: Recordable) { |
||||
|
await DocumentTasksStop(record.id); |
||||
|
await reload(); |
||||
|
} |
||||
|
|
||||
|
function handleAdd() { |
||||
|
openModal(true, { update: false }); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
async function handleDownload(record: Recordable) { |
||||
|
await DocumentTaskResultDownload([record.id]); |
||||
|
await reload(); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped></style> |
@ -1,180 +1,118 @@ |
|||||
<template> |
<template> |
||||
<PageWrapper dense> |
<div class="markup-container"> |
||||
<BasicTable @register="registerTable"> |
<!-- 头部标题 --> |
||||
<template #toolbar> |
<div class="markup-header"> |
||||
<!-- <a-button |
<img src="@/assets/images/markup-logo.svg" alt="Markup" class="markup-logo" /> |
||||
@click="downloadExcel(ContractualTasksExport, '合同任务数据', getForm().getFieldsValue())" |
<h1 class="markup-title">大模型驱动的新一代合同审查工具</h1> |
||||
v-auth="'productManagement:ContractualTasks:export'" |
</div> |
||||
>导出</a-button |
|
||||
|
<!-- 审查选项卡 --> |
||||
|
<div class="review-tabs"> |
||||
|
<div class="tab-buttons"> |
||||
|
<div |
||||
|
:class="['tab-button', activeTab === 'inference' ? 'active' : '']" |
||||
|
@click="setActiveTab('inference')" |
||||
> |
> |
||||
<a-button |
<span class="tab-icon">📄</span> 推理审查 |
||||
type="primary" |
</div> |
||||
danger |
<div |
||||
@click="multipleRemove(ContractualTasksRemove)" |
:class="['tab-button', activeTab === 'comparison' ? 'active' : '']" |
||||
:disabled="!selected" |
@click="setActiveTab('comparison')" |
||||
v-auth="'productManagement:ContractualTasks:remove'" |
|
||||
>删除</a-button |
|
||||
> --> |
|
||||
<a-button |
|
||||
type="primary" |
|
||||
@click="handleAdd" |
|
||||
v-auth="'productManagement:ContractualTasks:add'" |
|
||||
>新增</a-button |
|
||||
> |
> |
||||
</template> |
<span class="tab-icon">🔄</span> 对比审查 |
||||
<template #bodyCell="{ column, record }"> |
</div> |
||||
<template v-if="column.key === 'action'"> |
</div> |
||||
<TableAction |
</div> |
||||
stopButtonPropagation |
|
||||
:actions="[ |
<!-- 根据选项卡切换不同的组件 --> |
||||
{ |
<InferenceReview v-if="activeTab === 'inference'" /> |
||||
label: '详情', |
<ComparisonReview v-else /> |
||||
icon: IconEnum.EDIT, |
</div> |
||||
type: 'primary', |
|
||||
ghost: true, |
|
||||
ifShow: () => { |
|
||||
if ( |
|
||||
record.progressStatus != 'PENDING' && |
|
||||
record.progressStatus != 'STARTED' && |
|
||||
record.progressStatus != 'REVOKED' |
|
||||
) { |
|
||||
return true; |
|
||||
} else { |
|
||||
return false; |
|
||||
} |
|
||||
}, |
|
||||
onClick: handleDetail.bind(null, record), |
|
||||
}, |
|
||||
{ |
|
||||
label: '下载', |
|
||||
icon: IconEnum.DOWNLOAD, |
|
||||
type: 'primary', |
|
||||
color: 'success', |
|
||||
ghost: true, |
|
||||
ifShow: () => { |
|
||||
if ( |
|
||||
record.progressStatus != 'PENDING' && |
|
||||
record.progressStatus != 'STARTED' && |
|
||||
record.progressStatus != 'REVOKED' |
|
||||
) { |
|
||||
return true; |
|
||||
} else { |
|
||||
return false; |
|
||||
} |
|
||||
}, |
|
||||
onClick: handleDownload.bind(null, record), |
|
||||
}, |
|
||||
{ |
|
||||
label: '终止任务', |
|
||||
icon: IconEnum.DELETE, |
|
||||
type: 'primary', |
|
||||
danger: true, |
|
||||
ghost: true, |
|
||||
ifShow: () => { |
|
||||
if (record.progressStatus == 'PENDING') { |
|
||||
return true; |
|
||||
} else { |
|
||||
return false; |
|
||||
} |
|
||||
}, |
|
||||
popConfirm: { |
|
||||
placement: 'left', |
|
||||
title: '是否终止当前任务?', |
|
||||
confirm: handleStop.bind(null, record), |
|
||||
}, |
|
||||
}, |
|
||||
]" |
|
||||
/> |
|
||||
</template> |
|
||||
</template> |
|
||||
</BasicTable> |
|
||||
<ContractualTasksModal @register="registerModal" @reload="reload" /> |
|
||||
<DocsDrawer @register="registerDrawer" /> |
|
||||
</PageWrapper> |
|
||||
</template> |
</template> |
||||
|
|
||||
<script setup lang="ts"> |
<script setup lang="ts"> |
||||
import { PageWrapper } from '@/components/Page'; |
import { ref } from 'vue'; |
||||
import { BasicTable, useTable, TableAction } from '@/components/Table'; |
import InferenceReview from './components/InferenceReview.vue'; |
||||
|
import ComparisonReview from './components/ComparisonReview.vue'; |
||||
import { downloadExcel } from '@/utils/file/download'; |
|
||||
import { useModal } from '@/components/Modal'; |
|
||||
import ContractualTasksModal from './ContractualTasksModal.vue'; |
|
||||
import { formSchemas, columns } from './ContractualTasks.data'; |
|
||||
import { IconEnum } from '@/enums/appEnum'; |
|
||||
import DocsDrawer from '@/views/documentReview/DocumentTasks/DocsDrawer.vue'; |
|
||||
import { useDrawer } from '@/components/Drawer'; |
|
||||
import { DocumentTasksStop } from '@/api/documentReview/DocumentTasks'; |
|
||||
import { |
|
||||
ContractualTasksList, |
|
||||
ContractualTasksExport, |
|
||||
ContractualTasksRemove, |
|
||||
} from '@/api/contractReview/ContractualTasks'; |
|
||||
import { |
|
||||
DocumentTaskResultsInfoByTaskId, |
|
||||
DocumentTaskResultDownload, |
|
||||
} from '@/api/documentReview/DocumentTaskResults'; |
|
||||
|
|
||||
const [registerDrawer, { openDrawer }] = useDrawer(); |
|
||||
|
|
||||
defineOptions({ name: 'ContractualTasks' }); |
defineOptions({ name: 'ContractualTasks' }); |
||||
|
|
||||
const [registerTable, { reload, multipleRemove, selected, getForm }] = useTable({ |
// 状态变量 |
||||
rowSelection: { |
const activeTab = ref('inference'); |
||||
type: 'checkbox', |
|
||||
}, |
|
||||
title: '合同任务列表', |
|
||||
api: ContractualTasksList, |
|
||||
showIndexColumn: false, |
|
||||
rowKey: 'id', |
|
||||
useSearchForm: true, |
|
||||
formConfig: { |
|
||||
schemas: formSchemas, |
|
||||
baseColProps: { |
|
||||
xs: 24, |
|
||||
sm: 24, |
|
||||
md: 24, |
|
||||
lg: 6, |
|
||||
}, |
|
||||
}, |
|
||||
columns: columns, |
|
||||
actionColumn: { |
|
||||
width: 200, |
|
||||
title: '操作', |
|
||||
key: 'action', |
|
||||
fixed: 'right', |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
const [registerModal, { openModal }] = useModal(); |
// 设置当前选中的标签 |
||||
|
function setActiveTab(tab) { |
||||
|
activeTab.value = tab; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
async function handleDetail(record: Recordable) { |
<style scoped> |
||||
try { |
.markup-container { |
||||
let res = await DocumentTaskResultsInfoByTaskId(record.id); |
padding: 20px; |
||||
|
min-width: 1400px; |
||||
|
max-width: 1400px; |
||||
|
margin: 0 auto; |
||||
|
background-color: #f5f5f5; |
||||
|
min-height: 100vh; |
||||
|
} |
||||
|
|
||||
openDrawer(true, { value: res.result, type: 'markdown' }); |
.markup-header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
margin-bottom: 30px; |
||||
|
} |
||||
|
|
||||
console.log('res', res); |
.markup-logo { |
||||
} catch (ex) { |
width: 40px; |
||||
openDrawer(true, { value: '加载失败,请刷新页面', type: 'markdown' }); |
height: 40px; |
||||
} |
margin-right: 10px; |
||||
//根据record.id查询结果详情 |
} |
||||
} |
|
||||
|
|
||||
async function handleStop(record: Recordable) { |
.markup-title { |
||||
await DocumentTasksStop(record.id); |
font-size: 24px; |
||||
await reload(); |
font-weight: bold; |
||||
} |
color: #333; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
function handleAdd() { |
.review-tabs { |
||||
openModal(true, { update: false }); |
margin-bottom: 30px; |
||||
} |
} |
||||
|
|
||||
|
.tab-buttons { |
||||
|
display: flex; |
||||
|
width: 450px; |
||||
|
margin: 0 auto; |
||||
|
background-color: #f0f2f5; |
||||
|
border-radius: 30px; |
||||
|
padding: 4px; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
async function handleDownload(record: Recordable) { |
.tab-button { |
||||
await DocumentTaskResultDownload([record.id]); |
flex: 1; |
||||
await reload(); |
text-align: center; |
||||
} |
padding: 10px 0; |
||||
</script> |
font-size: 16px; |
||||
|
color: #333; |
||||
|
cursor: pointer; |
||||
|
border-radius: 30px; |
||||
|
transition: all 0.3s; |
||||
|
position: relative; |
||||
|
background-color: transparent; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.tab-button.active { |
||||
|
background-color: #fff; |
||||
|
color: #1890ff; |
||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
<style scoped></style> |
.tab-icon { |
||||
|
margin-right: 5px; |
||||
|
} |
||||
|
</style> |
||||
|
Loading…
Reference in new issue