Browse Source

新合同

ai_dev_new
zhouhaibin 3 weeks ago
parent
commit
b53a329412
  1. 74
      src/api/contractReview/ContractualTaskChecklist/index.ts
  2. 77
      src/api/contractReview/ContractualTaskChecklist/model.ts
  3. 9
      src/api/contractReview/ContractualTasks/index.ts
  4. 10
      src/assets/icons/file-list.svg
  5. 6
      src/assets/images/markup-logo.svg
  6. 6
      src/assets/images/upload-icon.svg
  7. 62
      src/views/contractReview/ContractualTaskChecklist/ContractualTaskChecklist.data.ts
  8. 207
      src/views/contractReview/ContractualTaskChecklist/ContractualTaskChecklistModal.vue
  9. 115
      src/views/contractReview/ContractualTaskChecklist/index.vue
  10. 550
      src/views/contractReview/ContractualTasks/components/ComparisonReview.vue
  11. 413
      src/views/contractReview/ContractualTasks/components/InferenceReview.vue
  12. 903
      src/views/contractReview/ContractualTasks/components/ReviewDialog.vue
  13. 220
      src/views/contractReview/ContractualTasks/index copy.vue
  14. 262
      src/views/contractReview/ContractualTasks/index.vue

74
src/api/contractReview/ContractualTaskChecklist/index.ts

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

77
src/api/contractReview/ContractualTaskChecklist/model.ts

@ -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[];

9
src/api/contractReview/ContractualTasks/index.ts

@ -55,3 +55,12 @@ export function ContractualTasksUpdate(data: ContractualTasksForm) {
export function ContractualTasksRemove(id: ID | IDS) { export function ContractualTasksRemove(id: ID | IDS) {
return defHttp.deleteWithMsg<void>({ url: '/productManagement/ContractualTasks/' + id },); return defHttp.deleteWithMsg<void>({ url: '/productManagement/ContractualTasks/' + id },);
} }
/**
*
* @param ossId OSS ID
* @returns
*/
export function AnalyzeContract(ossId: string) {
return defHttp.get<any>({ url: '/productManagement/ContractualTasks/analyzeContract', params: { ossId } });
}

10
src/assets/icons/file-list.svg

@ -0,0 +1,10 @@
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path
d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32zm-40 824H232V136h560v752z"
fill="#1890ff"
/>
<path
d="M696 472H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h368c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM696 304H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h368c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM696 640H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h368c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"
fill="#1890ff"
/>
</svg>

After

Width:  |  Height:  |  Size: 577 B

6
src/assets/images/markup-logo.svg

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
<circle cx="20" cy="20" r="18" fill="#10b981" />
<path d="M15 15H25V25H15V15Z" fill="white" />
<path d="M12 12L28 28" stroke="white" stroke-width="2" stroke-linecap="round" />
<path d="M12 28L28 12" stroke="white" stroke-width="2" stroke-linecap="round" />
</svg>

After

Width:  |  Height:  |  Size: 368 B

6
src/assets/images/upload-icon.svg

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none">
<circle cx="32" cy="32" r="32" fill="#E6F7FF" />
<path d="M32 20V44" stroke="#1890FF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
<path d="M20 32L32 20L44 32" stroke="#1890FF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
<path d="M44 44H20" stroke="#1890FF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 481 B

62
src/views/contractReview/ContractualTaskChecklist/ContractualTaskChecklist.data.ts

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

207
src/views/contractReview/ContractualTaskChecklist/ContractualTaskChecklistModal.vue

@ -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>

115
src/views/contractReview/ContractualTaskChecklist/index.vue

@ -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>

550
src/views/contractReview/ContractualTasks/components/ComparisonReview.vue

@ -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">仅支持docdocx格式文件</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">仅支持docdocx格式文件</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: APIID
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>

413
src/views/contractReview/ContractualTasks/components/InferenceReview.vue

@ -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">仅支持docdocx格式文件</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>

903
src/views/contractReview/ContractualTasks/components/ReviewDialog.vue

@ -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('新建清单功能开发中');
};
// 使modalInnermodal
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>

220
src/views/contractReview/ContractualTasks/index copy.vue

@ -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>

262
src/views/contractReview/ContractualTasks/index.vue

@ -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…
Cancel
Save