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> |
|||
<PageWrapper dense> |
|||
<BasicTable @register="registerTable"> |
|||
<template #toolbar> |
|||
<!-- <a-button |
|||
@click="downloadExcel(ContractualTasksExport, '合同任务数据', getForm().getFieldsValue())" |
|||
v-auth="'productManagement:ContractualTasks:export'" |
|||
>导出</a-button |
|||
<div class="markup-container"> |
|||
<!-- 头部标题 --> |
|||
<div class="markup-header"> |
|||
<img src="@/assets/images/markup-logo.svg" alt="Markup" class="markup-logo" /> |
|||
<h1 class="markup-title">大模型驱动的新一代合同审查工具</h1> |
|||
</div> |
|||
|
|||
<!-- 审查选项卡 --> |
|||
<div class="review-tabs"> |
|||
<div class="tab-buttons"> |
|||
<div |
|||
:class="['tab-button', activeTab === 'inference' ? 'active' : '']" |
|||
@click="setActiveTab('inference')" |
|||
> |
|||
<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 |
|||
<span class="tab-icon">📄</span> 推理审查 |
|||
</div> |
|||
<div |
|||
:class="['tab-button', activeTab === 'comparison' ? 'active' : '']" |
|||
@click="setActiveTab('comparison')" |
|||
> |
|||
</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" /> |
|||
</PageWrapper> |
|||
<span class="tab-icon">🔄</span> 对比审查 |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 根据选项卡切换不同的组件 --> |
|||
<InferenceReview v-if="activeTab === 'inference'" /> |
|||
<ComparisonReview v-else /> |
|||
</div> |
|||
</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, |
|||
} from '@/api/documentReview/DocumentTaskResults'; |
|||
|
|||
const [registerDrawer, { openDrawer }] = useDrawer(); |
|||
import { ref } from 'vue'; |
|||
import InferenceReview from './components/InferenceReview.vue'; |
|||
import ComparisonReview from './components/ComparisonReview.vue'; |
|||
|
|||
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 activeTab = ref('inference'); |
|||
|
|||
// 设置当前选中的标签 |
|||
function setActiveTab(tab) { |
|||
activeTab.value = tab; |
|||
} |
|||
</script> |
|||
|
|||
const [registerModal, { openModal }] = useModal(); |
|||
<style scoped> |
|||
.markup-container { |
|||
padding: 20px; |
|||
min-width: 1400px; |
|||
max-width: 1400px; |
|||
margin: 0 auto; |
|||
background-color: #f5f5f5; |
|||
min-height: 100vh; |
|||
} |
|||
|
|||
async function handleDetail(record: Recordable) { |
|||
try { |
|||
let res = await DocumentTaskResultsInfoByTaskId(record.id); |
|||
.markup-header { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin-bottom: 30px; |
|||
} |
|||
|
|||
openDrawer(true, { value: res.result, type: 'markdown' }); |
|||
.markup-logo { |
|||
width: 40px; |
|||
height: 40px; |
|||
margin-right: 10px; |
|||
} |
|||
|
|||
console.log('res', res); |
|||
} catch (ex) { |
|||
openDrawer(true, { value: '加载失败,请刷新页面', type: 'markdown' }); |
|||
} |
|||
//根据record.id查询结果详情 |
|||
} |
|||
.markup-title { |
|||
font-size: 24px; |
|||
font-weight: bold; |
|||
color: #333; |
|||
margin: 0; |
|||
} |
|||
|
|||
async function handleStop(record: Recordable) { |
|||
await DocumentTasksStop(record.id); |
|||
await reload(); |
|||
} |
|||
.review-tabs { |
|||
margin-bottom: 30px; |
|||
} |
|||
|
|||
function handleAdd() { |
|||
openModal(true, { update: false }); |
|||
} |
|||
.tab-buttons { |
|||
display: flex; |
|||
width: 450px; |
|||
margin: 0 auto; |
|||
background-color: #f0f2f5; |
|||
border-radius: 30px; |
|||
padding: 4px; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.tab-button { |
|||
flex: 1; |
|||
text-align: center; |
|||
padding: 10px 0; |
|||
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; |
|||
} |
|||
|
|||
async function handleDownload(record: Recordable) { |
|||
await DocumentTaskResultDownload([record.id]); |
|||
await reload(); |
|||
} |
|||
</script> |
|||
.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