Browse Source

优化合同审核页面

ai_dev
zhouhaibin 2 weeks ago
parent
commit
90c8002793
  1. 37
      src/api/contractReview/ContractualTaskChecklist/index.ts
  2. 102
      src/api/contractReview/ContractualTaskChecklist/model.ts
  3. 57
      src/api/contractReview/ContractualTaskType/index.ts
  4. 39
      src/api/contractReview/ContractualTaskType/model.ts
  5. 62
      src/views/contractReview/ContractualTaskChecklist/ContractualTaskChecklist.data.ts
  6. 207
      src/views/contractReview/ContractualTaskChecklist/ContractualTaskChecklistModal.vue
  7. 884
      src/views/contractReview/ContractualTaskChecklist/index.vue
  8. 550
      src/views/contractReview/ContractualTasks/components/ComparisonReview.vue
  9. 445
      src/views/contractReview/ContractualTasks/components/ComplianceContent.vue
  10. 642
      src/views/contractReview/ContractualTasks/components/ConsistencyContent.vue
  11. 51
      src/views/contractReview/ContractualTasks/components/InferenceReview.vue
  12. 319
      src/views/contractReview/ContractualTasks/components/ReviewConfigDialog.vue
  13. 564
      src/views/contractReview/ContractualTasks/components/ReviewDialog.vue
  14. 481
      src/views/contractReview/ContractualTasks/components/SubstantiveContent.vue
  15. 137
      src/views/contractReview/ContractualTasks/index.vue

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

@ -1,9 +1,9 @@
import { defHttp } from '@/utils/http/axios';
import { ID, IDS, commonExport } from '@/api/base';
import { ContractualTaskChecklistVO, ContractualTaskChecklistFormList, ContractualTaskChecklistQuery } from './model';
import { ContractualTaskChecklistVO, ContractualTaskChecklistForm, ContractualTaskChecklistQuery } from './model';
/**
*
*
* @param params
* @returns
*/
@ -12,16 +12,7 @@ export function ContractualTaskChecklistList(params?: ContractualTaskChecklistQu
}
/**
*
* @param params
* @returns
*/
export function ContractualTaskChecklistQueryList(params?: ContractualTaskChecklistQuery) {
return defHttp.get<ContractualTaskChecklistVO[]>({ url: '/productManagement/ContractualTaskChecklist/queryList', params });
}
/**
*
*
* @param params
* @returns
*/
@ -30,42 +21,34 @@ export function ContractualTaskChecklistExport(params?: ContractualTaskChecklist
}
/**
*
*
* @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 });
return defHttp.get<ContractualTaskChecklistVO>({ url: '/productManagement/ContractualTaskChecklist/' + id });
}
/**
*
*
* @param data
* @returns
*/
export function ContractualTaskChecklistAdd(data: ContractualTaskChecklistFormList) {
export function ContractualTaskChecklistAdd(data: ContractualTaskChecklistForm) {
return defHttp.postWithMsg<void>({ url: '/productManagement/ContractualTaskChecklist', data });
}
/**
*
*
* @param data
* @returns
*/
export function ContractualTaskChecklistUpdate(data: ContractualTaskChecklistFormList) {
export function ContractualTaskChecklistUpdate(data: ContractualTaskChecklistForm) {
return defHttp.putWithMsg<void>({ url: '/productManagement/ContractualTaskChecklist', data });
}
/**
*
*
* @param id id
* @returns
*/

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

@ -1,77 +1,43 @@
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;
/** 风险等级 */
riskLevel?: string;
/** 要点名称 */
title?: string;
/** 合同类型id */
typeId?: string | number;
/** 要点描述 */
description?: string;
}
/**
*
*/
export interface ContractualTaskChecklistForm extends BaseEntity {
/** ID */
id?: string | number;
name: string;
checklistItem: string;
checklistItemDesc?: string;
groupId?: string | number;
/** 风险等级 */
riskLevel: string;
/** 要点名称 */
title: string;
/** 排序 */
sortOrder?: number;
/** 合同类型id */
typeId: string | number;
/** 要点描述 */
description: string;
}
export interface ContractualTaskChecklistResponse extends Omit<ContractualTaskChecklistForm, 'checklistItem'> {
checklistItems?: {
checklistItem: string;
checklistItemDesc: string;
}[];
/**
*
*/
export interface ContractualTaskChecklistVO extends ContractualTaskChecklistForm {
/** 创建时间 */
createTime: string;
/** 更新时间 */
updateTime: string;
}
export type ContractualTaskChecklistFormList = ContractualTaskChecklistForm[];

57
src/api/contractReview/ContractualTaskType/index.ts

@ -0,0 +1,57 @@
import { defHttp } from '@/utils/http/axios';
import { ID, IDS, commonExport } from '@/api/base';
import { ContractualTaskTypeVO, ContractualTaskTypeForm, ContractualTaskTypeQuery } from './model';
/**
*
* @param params
* @returns
*/
export function ContractualTaskTypeList(params?: ContractualTaskTypeQuery) {
return defHttp.get<ContractualTaskTypeVO[]>({ url: '/productManagement/ContractualTaskType/list', params });
}
/**
*
* @param params
* @returns
*/
export function ContractualTaskTypeExport(params?: ContractualTaskTypeQuery) {
return commonExport('/productManagement/ContractualTaskType/export', params ?? {});
}
/**
*
* @param id id
* @returns
*/
export function ContractualTaskTypeInfo(id: ID) {
return defHttp.get<ContractualTaskTypeVO>({ url: '/productManagement/ContractualTaskType/' + id });
}
/**
*
* @param data
* @returns
*/
export function ContractualTaskTypeAdd(data: ContractualTaskTypeForm) {
return defHttp.postWithMsg<void>({ url: '/productManagement/ContractualTaskType', data });
}
/**
*
* @param data
* @returns
*/
export function ContractualTaskTypeUpdate(data: ContractualTaskTypeForm) {
return defHttp.putWithMsg<void>({ url: '/productManagement/ContractualTaskType', data });
}
/**
*
* @param id id
* @returns
*/
export function ContractualTaskTypeRemove(id: ID | IDS) {
return defHttp.deleteWithMsg<void>({ url: '/productManagement/ContractualTaskType/' + id },);
}

39
src/api/contractReview/ContractualTaskType/model.ts

@ -0,0 +1,39 @@
import { BaseEntity, PageQuery } from '@/api/base';
/**
*
*/
export interface ContractualTaskTypeQuery extends PageQuery {
/** 类型名称 */
contractName?: string;
/** 显示顺序 */
sort?: number | string;
/** 状态(0正常 1停用) */
status?: string;
}
/**
*
*/
export interface ContractualTaskTypeForm extends BaseEntity {
/** 类型ID */
id?: number | string;
/** 类型名称 */
contractName: string;
/** 显示顺序 */
sort?: number | string;
/** 状态(0正常 1停用) */
status?: string;
/** 备注 */
remark?: string;
}
/**
*
*/
export interface ContractualTaskTypeVO extends ContractualTaskTypeForm {
/** 创建时间 */
createTime: string;
/** 更新时间 */
updateTime: string;
}

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

@ -1,62 +0,0 @@
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

@ -1,207 +0,0 @@
<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>

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

@ -1,115 +1,793 @@
<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 dense>
<div class="container">
<!-- 左侧合同类型 -->
<div class="rule-category w-64 border-r border-gray-200 p-4">
<div class="category-title font-bold text-lg mb-4">合同类型</div>
<Button type="primary" class="w-full mb-4" @click="handleCreateCategory">
<template #icon><PlusOutlined /></template>
新建
</Button>
<div class="category-list overflow-y-auto">
<div
v-for="category in ruleCategories"
:key="category.id"
:class="['category-item p-3 cursor-pointer rounded mb-2 flex justify-between',
{ 'selected': selectedCategory === String(category.id) }]"
>
<div class="flex-1" @click="() => category.id !== undefined && selectCategory(category.id)">
{{ category.contractName }}
</div>
<Button
type="text"
size="small"
danger
@click.stop="confirmDeleteCategory(category)"
v-if="ruleCategories.length > 1"
>
<template #icon><DeleteOutlined /></template>
</Button>
</div>
</div>
</div>
<!-- 右侧审查要点 -->
<div class="rule-detail flex-1 flex flex-col">
<!-- 上部分审查要点列表 -->
<div class="rule-list-section flex-1 p-4">
<div class="review-header flex justify-between items-center mb-4">
<div class="text-lg font-bold">审查要点</div>
<div class="actions flex space-x-2">
<Button type="primary" danger @click="handleBatchDelete" :disabled="!hasChecked">
<template #icon><DeleteOutlined /></template>
批量删除
</Button>
<Button type="primary" @click="handleAddPoint">
<template #icon><PlusOutlined /></template>
新建要点
</Button>
</div>
</div>
<div class="rule-list overflow-y-auto">
<div v-for="rule in filteredRules" :key="rule.id" class="rule-item mb-3 border border-gray-200 rounded">
<div class="rule-header flex items-center p-2">
<Checkbox v-model:checked="rule.checked" class="mr-2"></Checkbox>
<div :class="['risk-level px-2 py-1 rounded mr-3 text-sm', riskLevelClass(rule.riskLevel)]">
{{ riskLevelText(rule.riskLevel) }}
</div>
<div class="rule-title flex-1 cursor-pointer" @click="handleSelectRule(rule)">{{ rule.title }}</div>
<div class="action-buttons flex space-x-1">
<Button type="link" size="small" danger @click="confirmDeleteRule(rule)">删除</Button>
</div>
</div>
</div>
</div>
</div>
<!-- 下部分详情/编辑区域 -->
<div class="rule-detail-section border-t border-gray-200 p-2">
<div v-if="!selectedRule && !isAddingNew" class="empty-state text-center py-4">
<div class="text-gray-400 text-lg mb-2">📋</div>
<div class="text-gray-500">请选择一个审查要点查看详情或点击"新建要点"添加新的审查要点</div>
</div>
<div v-else class="rule-detail-content">
<div class="detail-header flex justify-between items-center mb-2">
<h3 class="text-lg font-bold">
{{ isAddingNew ? '新建审查要点' : (isEditing ? '编辑审查要点' : '审查要点详情') }}
</h3>
<div class="actions flex space-x-2">
<Button v-if="!isEditing && !isAddingNew" type="primary" @click="handleEdit">编辑</Button>
<Button v-if="isEditing || isAddingNew" type="primary" @click="handleSave">保存</Button>
<Button v-if="isEditing || isAddingNew" @click="handleCancel">取消</Button>
</div>
</div>
<Form
:model="editFormData"
ref="editFormRef"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 21 }"
class="detail-form"
:rules="formRules"
>
<FormItem label="要点名称" name="title">
<Input
v-if="isEditing || isAddingNew"
v-model:value="editFormData.title"
placeholder="请输入要点名称"
/>
<span v-else>{{ selectedRule?.title }}</span>
</FormItem>
<FormItem label="合同类型" name="typeId">
<Select
v-if="isEditing || isAddingNew"
v-model:value="editFormData.typeId"
placeholder="请选择合同类型"
>
<SelectOption v-for="category in ruleCategories" :key="category.id" :value="String(category.id)">
{{ category.contractName }}
</SelectOption>
</Select>
<span v-else>{{ getCategoryName(selectedRule?.typeId) }}</span>
</FormItem>
<FormItem label="风险等级" name="riskLevel">
<Select
v-if="isEditing || isAddingNew"
v-model:value="editFormData.riskLevel"
placeholder="请选择风险等级"
>
<SelectOption value="H">高风险</SelectOption>
<SelectOption value="M">中风险</SelectOption>
<SelectOption value="L">低风险</SelectOption>
</Select>
<span v-else-if="selectedRule" :class="['risk-badge px-2 py-1 rounded text-sm', riskLevelClass(selectedRule.riskLevel)]">
{{ riskLevelText(selectedRule.riskLevel) }}
</span>
</FormItem>
<FormItem label="排序" name="sortOrder">
<InputNumber
v-if="isEditing || isAddingNew"
v-model:value="editFormData.sortOrder"
placeholder="请输入排序"
style="width: 200px"
/>
<span v-else>{{ selectedRule?.sortOrder }}</span>
</FormItem>
<FormItem label="要点描述" name="description">
<Textarea
v-if="isEditing || isAddingNew"
v-model:value="editFormData.description"
placeholder="请输入要点描述"
:auto-size="{ minRows: 2, maxRows: 4 }"
/>
<div v-else class="description-content">{{ selectedRule?.description }}</div>
</FormItem>
</Form>
</div>
</div>
</div>
</div>
<!-- 删除确认模态框 -->
<Modal
v-model:visible="deleteModalVisible"
:title="deleteModalTitle"
@ok="handleDeleteConfirm"
@cancel="deleteModalVisible = false"
okText="确认删除"
cancelText="取消"
okType="danger"
>
<p>{{ deleteModalContent }}</p>
</Modal>
<!-- 合同类型表单模态框 -->
<Modal
v-model:visible="categoryModalVisible"
:title="categoryModalTitle"
@ok="handleCategorySave"
@cancel="categoryModalVisible = false"
okText="保存"
cancelText="取消"
>
<Form
:model="categoryForm"
ref="categoryFormRef"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
>
<FormItem label="类型名称" name="contractName" :rules="[{ required: true, message: '请输入类型名称' }]">
<Input v-model:value="categoryForm.contractName" placeholder="请输入类型名称" />
</FormItem>
<FormItem label="显示顺序" name="sort">
<InputNumber v-model:value="categoryForm.sort" placeholder="请输入显示顺序" style="width: 100%" />
</FormItem>
<FormItem label="状态" name="status">
<Select v-model:value="categoryForm.status" placeholder="请选择状态">
<SelectOption value="0">正常</SelectOption>
<SelectOption value="1">停用</SelectOption>
</Select>
</FormItem>
<FormItem label="备注" name="remark">
<Textarea v-model:value="categoryForm.remark" placeholder="请输入备注信息" :rows="4" />
</FormItem>
</Form>
</Modal>
</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',
},
import { ref, computed, onMounted, reactive } from 'vue';
import { PlusOutlined, DownloadOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import { Button, InputSearch, Checkbox, Modal, message, Form, FormItem, Input, InputNumber, Select, SelectOption, Textarea } from 'ant-design-vue';
import { FormInstance } from 'ant-design-vue/es/form';
import RuleFormModal from './RuleFormModal.vue';
import { ContractualTaskChecklistList, ContractualTaskChecklistRemove, ContractualTaskChecklistAdd, ContractualTaskChecklistUpdate, ContractualTaskChecklistInfo } from '@/api/contractReview/ContractualTaskChecklist';
import { ContractualTaskTypeList, ContractualTaskTypeAdd, ContractualTaskTypeUpdate, ContractualTaskTypeRemove } from '@/api/contractReview/ContractualTaskType';
import { ContractualTaskChecklistVO, ContractualTaskChecklistForm } from '@/api/contractReview/ContractualTaskChecklist/model';
import { ContractualTaskTypeVO, ContractualTaskTypeForm } from '@/api/contractReview/ContractualTaskType/model';
defineOptions({ name: 'RuleManagement' });
// checked
type CheckableRule = ContractualTaskChecklistVO & { checked: boolean };
//
const ruleCategories = ref<ContractualTaskTypeVO[]>([]);
const selectedCategory = ref<string | number>('');
const categoryModalVisible = ref(false);
const categoryModalTitle = ref('新增合同类型');
const categoryForm = reactive<ContractualTaskTypeForm>({
id: undefined,
contractName: '',
sort: 0,
status: '0',
remark: ''
});
const categoryFormRef = ref<FormInstance>();
const [registerModal, { openModal }] = useModal();
//
const rules = ref<CheckableRule[]>([]);
const selectedRule = ref<CheckableRule | null>(null);
const isEditing = ref(false);
const isAddingNew = ref(false);
const editFormData = reactive<ContractualTaskChecklistForm>({
id: undefined,
title: '',
typeId: '',
riskLevel: 'M',
sortOrder: 0,
description: ''
});
const editFormRef = ref<FormInstance>();
//
const formRules: any = {
title: [{ required: true, message: '请输入要点名称', trigger: 'blur' }],
typeId: [{ required: true, message: '请选择合同类型', trigger: 'change' }],
riskLevel: [{ required: true, message: '请选择风险等级', trigger: 'change' }],
description: [{ required: true, message: '请输入要点描述', trigger: 'blur' }],
};
function handleEdit(record: Recordable) {
openModal(true, { record, update: true });
}
//
const deleteModalVisible = ref(false);
const deleteModalTitle = ref('');
const deleteModalContent = ref('');
const itemToDelete = ref<any>(null);
const deleteType = ref<'category' | 'rule'>('rule');
function handleAdd() {
openModal(true, { update: false });
}
//
const hasChecked = computed(() => {
return rules.value.some(r => r.checked);
});
async function handleDelete(groupId: string) {
await ContractualTaskChecklistRemove([groupId]);
await reload();
}
//
const filteredRules = computed(() => {
if (!selectedCategory.value) return [];
return rules.value.filter(rule => {
return String(rule.typeId) === String(selectedCategory.value);
});
});
//
onMounted(async () => {
await loadCategories();
});
//
const loadCategories = async () => {
try {
const res = await ContractualTaskTypeList({});
// 使
const data = res as any;
console.log('合同类型数据:', data);
if (data && data.length > 0) {
// ID
const currentSelectedId = selectedCategory.value;
//
ruleCategories.value = data;
//
const shouldSelectFirst = !currentSelectedId ||
!ruleCategories.value.some(item => String(item.id) === currentSelectedId);
if (shouldSelectFirst && ruleCategories.value.length > 0 && ruleCategories.value[0].id) {
selectedCategory.value = String(ruleCategories.value[0].id);
} else if (currentSelectedId) {
//
selectedCategory.value = currentSelectedId;
}
//
await loadRules();
} else {
ruleCategories.value = [];
rules.value = [];
}
} catch (error) {
console.error('加载合同类型失败', error);
message.error('加载合同类型失败');
}
};
//
const loadRules = async () => {
if (!selectedCategory.value) {
rules.value = [];
return;
}
try {
const res = await ContractualTaskChecklistList({ typeId: selectedCategory.value });
// 使
const data = res as any;
console.log('审查要点数据:', data);
if (data && data.length > 0) {
rules.value = data.map(item => ({
...item,
checked: false
}));
} else {
rules.value = [];
}
} catch (error) {
console.error('加载审查要点失败', error);
message.error('加载审查要点失败');
}
};
//
const selectCategory = (categoryId: string | number) => {
if (categoryId !== undefined) {
selectedCategory.value = String(categoryId);
selectedRule.value = null; //
isEditing.value = false;
loadRules();
}
};
//
const handleSelectRule = (rule: CheckableRule) => {
selectedRule.value = rule;
isEditing.value = false;
//
editFormData.id = rule.id;
editFormData.title = rule.title || '';
editFormData.typeId = rule.typeId ? String(rule.typeId) : '';
editFormData.riskLevel = rule.riskLevel || 'M';
editFormData.sortOrder = rule.sortOrder || 0;
editFormData.description = rule.description || '';
};
//
const riskLevelClass = (level: string) => {
switch (level) {
case 'H':
case '高风险':
return 'bg-red-100 text-red-600';
case 'M':
case '中风险':
return 'bg-orange-100 text-orange-600';
case 'L':
case '低风险':
return 'bg-green-100 text-green-600';
default:
return '';
}
};
//
const riskLevelText = (level: string) => {
switch (level) {
case 'H':
return '高风险';
case 'M':
return '中风险';
case 'L':
return '低风险';
case '高风险':
case '中风险':
case '低风险':
return level;
default:
return level;
}
};
//
const handleAddPoint = () => {
//
editFormData.id = undefined;
editFormData.title = '';
editFormData.typeId = selectedCategory.value || '';
editFormData.riskLevel = 'M';
editFormData.sortOrder = 0;
editFormData.description = '';
//
selectedRule.value = null;
isAddingNew.value = true;
isEditing.value = false;
};
//
const handleSave = async () => {
try {
await editFormRef.value?.validate();
if (isAddingNew.value) {
//
await ContractualTaskChecklistAdd(editFormData);
message.success('新增审查要点成功');
} else {
//
await ContractualTaskChecklistUpdate(editFormData);
message.success('更新审查要点成功');
}
//
isEditing.value = false;
isAddingNew.value = false;
selectedRule.value = null;
//
await loadRules();
} catch (error) {
console.error('保存审查要点失败', error);
message.error('保存审查要点失败');
}
};
//
const handleCancel = () => {
isEditing.value = false;
isAddingNew.value = false;
if (isAddingNew.value) {
//
selectedRule.value = null;
} else if (selectedRule.value) {
//
editFormData.title = selectedRule.value.title || '';
editFormData.typeId = selectedRule.value.typeId ? String(selectedRule.value.typeId) : '';
editFormData.riskLevel = selectedRule.value.riskLevel || 'M';
editFormData.sortOrder = selectedRule.value.sortOrder || 0;
editFormData.description = selectedRule.value.description || '';
}
};
//
const confirmDeleteRule = (rule: CheckableRule) => {
deleteModalTitle.value = '删除审查要点';
deleteModalContent.value = `确定要删除审查要点 "${rule.title}" 吗?此操作不可恢复。`;
itemToDelete.value = rule;
deleteType.value = 'rule';
deleteModalVisible.value = true;
};
//
const confirmDeleteCategory = async (category: ContractualTaskTypeVO) => {
try {
//
const res = await ContractualTaskChecklistList({ typeId: category.id });
// 使
const data = res as any;
if (data?.data?.rows && data.data.rows.length > 0) {
message.warning(`无法删除 "${category.contractName}",该类型下有 ${data.data.rows.length} 个审查要点。请先删除或移动其中的审查要点。`);
return;
}
deleteModalTitle.value = '删除合同类型';
deleteModalContent.value = `确定要删除合同类型 "${category.contractName}" 吗?此操作不可恢复。`;
itemToDelete.value = category;
deleteType.value = 'category';
deleteModalVisible.value = true;
} catch (error) {
console.error('检查合同类型下审查要点失败', error);
message.error('操作失败');
}
};
//
const handleDeleteConfirm = async () => {
try {
if (deleteType.value === 'rule') {
if (itemToDelete.value.batchDelete) {
//
const selectedIds = rules.value
.filter(r => r.checked && r.id !== undefined)
.map(r => String(r.id));
if (selectedIds.length > 0) {
// 使
const idsParam = selectedIds.join(',');
await ContractualTaskChecklistRemove(idsParam);
message.success(`成功删除 ${selectedIds.length} 个审查要点`);
}
await loadRules();
} else {
//
if (itemToDelete.value.id !== undefined) {
await ContractualTaskChecklistRemove(String(itemToDelete.value.id));
message.success('审查要点删除成功');
}
await loadRules();
}
deleteModalVisible.value = false;
} else {
//
if (itemToDelete.value.id !== undefined) {
await ContractualTaskTypeRemove(String(itemToDelete.value.id));
message.success('合同类型删除成功');
await loadCategories();
}
deleteModalVisible.value = false;
}
} catch (error) {
console.error('删除操作失败', error);
message.error('删除操作失败');
}
};
//
const handleBatchDelete = () => {
const selectedPoints = rules.value.filter(r => r.checked);
if (selectedPoints.length === 0) {
message.warning('请先选择要删除的审查要点');
return;
}
deleteModalTitle.value = '批量删除审查要点';
deleteModalContent.value = `确定要删除选中的 ${selectedPoints.length} 个审查要点吗?此操作不可恢复。`;
deleteType.value = 'rule';
deleteModalVisible.value = true;
itemToDelete.value = { batchDelete: true };
};
//
const handleCreateCategory = () => {
categoryForm.id = undefined;
categoryForm.contractName = '';
categoryForm.sort = 0;
categoryForm.status = '0';
categoryForm.remark = '';
categoryModalTitle.value = '新增合同类型';
categoryModalVisible.value = true;
};
//
const handleCategorySave = async () => {
try {
await categoryFormRef.value?.validate();
if (categoryForm.id) {
//
await ContractualTaskTypeUpdate(categoryForm);
message.success('更新合同类型成功');
} else {
//
await ContractualTaskTypeAdd(categoryForm);
message.success('新增合同类型成功');
}
categoryModalVisible.value = false;
//
await loadCategories();
} catch (error) {
console.error('保存合同类型失败', error);
}
};
//
const handleEdit = () => {
isEditing.value = true;
};
// ID
const getCategoryName = (typeId: string | number | undefined) => {
if (!typeId) return '未知类型';
const category = ruleCategories.value.find(cat => String(cat.id) === String(typeId));
return category ? category.contractName : '未知类型';
};
</script>
<style scoped></style>
<style scoped>
.container {
min-height: calc(100vh - 120px);
height: calc(100vh - 120px);
background-color: #fff;
display: flex;
width: 100%;
max-width: none;
}
.rule-category {
min-width: 240px;
width: 240px;
height: 100%;
display: flex;
flex-direction: column;
}
.category-list {
flex: 1;
overflow-y: auto;
padding-right: 4px;
}
.rule-detail {
flex: 1;
height: 100%;
min-width: 0; /* 确保flex item可以缩小 */
}
.rule-list-section {
height: 65%; /* 调整上部分占65%的高度 */
min-height: 350px;
display: flex;
flex-direction: column;
}
.rule-list {
flex: 1;
overflow-y: auto;
padding-right: 4px;
margin-right: 8px;
}
.rule-detail-section {
height: 35%; /* 调整下部分占35%的高度 */
min-height: 250px;
overflow-y: auto;
border-top: 2px solid #f0f0f0;
}
.category-item {
transition: all 0.3s;
border-radius: 6px;
margin-bottom: 4px;
}
.category-item:hover {
background-color: #f0f7ff !important;
}
.category-item.selected {
background-color: #e6f7ff !important;
border-left: 3px solid #1890ff;
}
.rule-item {
transition: all 0.2s;
border-radius: 6px;
margin-bottom: 8px;
}
.rule-item:hover {
border-color: #1890ff;
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.1);
}
.risk-level, .risk-badge {
min-width: 60px;
text-align: center;
font-weight: 500;
}
.rule-title {
transition: color 0.2s;
font-weight: 500;
}
.rule-title:hover {
color: #1890ff;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
background-color: #fafafa;
border-radius: 8px;
margin: 16px;
}
.detail-form {
padding: 8px;
}
.detail-form .ant-form-item {
margin-bottom: 12px;
}
.detail-form .ant-form-item-label {
font-weight: 500;
}
.description-content {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
padding: 8px;
background-color: #fafafa;
border-radius: 6px;
min-height: 40px;
}
.detail-header {
padding: 8px 8px 0 8px;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 0;
}
.review-header {
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 16px;
}
/* 滚动条样式 */
.category-list::-webkit-scrollbar,
.rule-list::-webkit-scrollbar,
.rule-detail-section::-webkit-scrollbar {
width: 6px;
}
.category-list::-webkit-scrollbar-track,
.rule-list::-webkit-scrollbar-track,
.rule-detail-section::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.category-list::-webkit-scrollbar-thumb,
.rule-list::-webkit-scrollbar-thumb,
.rule-detail-section::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.category-list::-webkit-scrollbar-thumb:hover,
.rule-list::-webkit-scrollbar-thumb:hover,
.rule-detail-section::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
/* Firefox滚动条样式 */
.category-list,
.rule-list,
.rule-detail-section {
scrollbar-width: thin;
scrollbar-color: #c1c1c1 #f1f1f1;
}
/* 响应式优化 */
@media (max-width: 1200px) {
.rule-category {
min-width: 200px;
width: 200px;
}
}
/* 确保PageWrapper不限制宽度 */
:deep(.ant-page-wrapper) {
padding: 0 !important;
}
:deep(.ant-page-wrapper-content) {
margin: 0 !important;
padding: 16px !important;
}
</style>

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

@ -1,550 +0,0 @@
<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>

445
src/views/contractReview/ContractualTasks/components/ComplianceContent.vue

@ -0,0 +1,445 @@
<template>
<div class="compliance-content">
<!-- 法规范围选择 -->
<div class="section regulation-section">
<h3 class="section-title">选择适用法规范围</h3>
<p class="section-description">选择需要进行合规性检查的法律法规类别</p>
<div class="regulation-options">
<div
class="regulation-card"
:class="{ selected: selectedRegulations.includes('contract-law') }"
@click="toggleRegulation('contract-law')"
>
<div class="card-header">
<CheckCircleOutlined v-if="selectedRegulations.includes('contract-law')" class="check-icon" />
合同法
</div>
<div class="card-body">
<h3>中华人民共和国合同法</h3>
<p class="card-desc">检查合同条款是否符合合同法基本要求</p>
</div>
</div>
<div
class="regulation-card"
:class="{ selected: selectedRegulations.includes('labor-law') }"
@click="toggleRegulation('labor-law')"
>
<div class="card-header">
<CheckCircleOutlined v-if="selectedRegulations.includes('labor-law')" class="check-icon" />
劳动法
</div>
<div class="card-body">
<h3>劳动合同相关法规</h3>
<p class="card-desc">检查劳动合同条款合规性</p>
</div>
</div>
<div
class="regulation-card"
:class="{ selected: selectedRegulations.includes('company-law') }"
@click="toggleRegulation('company-law')"
>
<div class="card-header">
<CheckCircleOutlined v-if="selectedRegulations.includes('company-law')" class="check-icon" />
公司法
</div>
<div class="card-body">
<h3>公司法相关规定</h3>
<p class="card-desc">检查公司间合同的法律合规性</p>
</div>
</div>
</div>
</div>
<!-- 行业特殊要求 -->
<div class="section industry-section">
<h3 class="section-title">行业特殊要求</h3>
<p class="section-description">选择合同涉及的行业领域进行专项合规检查</p>
<div class="industry-selector">
<div class="industry-options">
<div
class="industry-card"
:class="{ selected: selectedIndustry === 'financial' }"
@click="selectIndustry('financial')"
>
<div class="card-icon">
<BankOutlined />
</div>
<div class="card-title">金融服务</div>
<div class="card-desc">银行证券保险等金融行业合规要求</div>
</div>
<div
class="industry-card"
:class="{ selected: selectedIndustry === 'medical' }"
@click="selectIndustry('medical')"
>
<div class="card-icon">
<MedicineBoxOutlined />
</div>
<div class="card-title">医疗健康</div>
<div class="card-desc">医疗器械药品健康服务等行业规范</div>
</div>
<div
class="industry-card"
:class="{ selected: selectedIndustry === 'technology' }"
@click="selectIndustry('technology')"
>
<div class="card-icon">
<LaptopOutlined />
</div>
<div class="card-title">科技互联网</div>
<div class="card-desc">数据保护网络安全等科技行业要求</div>
</div>
<div
class="industry-card"
:class="{ selected: selectedIndustry === 'general' }"
@click="selectIndustry('general')"
>
<div class="card-icon">
<GlobalOutlined />
</div>
<div class="card-title">通用行业</div>
<div class="card-desc">一般商业合同合规检查</div>
</div>
</div>
</div>
</div>
<!-- 合规检查级别 -->
<div class="section level-section">
<h3 class="section-title">合规检查级别</h3>
<p class="section-description">选择合规检查的严格程度</p>
<div class="level-options">
<div
class="level-card"
:class="{ selected: selectedLevel === 'basic' }"
@click="selectLevel('basic')"
>
<div class="level-icon">🔍</div>
<div class="level-title">基础检查</div>
<div class="level-desc">检查明显的法律风险和合规问题</div>
</div>
<div
class="level-card"
:class="{ selected: selectedLevel === 'standard' }"
@click="selectLevel('standard')"
>
<div class="level-icon"></div>
<div class="level-title">标准检查</div>
<div class="level-desc">全面的合规性审查覆盖常见风险点</div>
</div>
<div
class="level-card"
:class="{ selected: selectedLevel === 'strict' }"
@click="selectLevel('strict')"
>
<div class="level-icon">🛡</div>
<div class="level-title">严格检查</div>
<div class="level-desc">最高级别检查包含潜在风险分析</div>
</div>
</div>
</div>
<!-- 特别关注点 -->
<div class="section focus-section">
<h3 class="section-title">特别关注点可选</h3>
<p class="section-description">指定需要特别关注的合规风险点</p>
<div class="focus-input">
<Input.TextArea
v-model:value="focusPoints"
placeholder="请输入特别关注的合规要求,如特定法规条款、行业标准、监管要求等..."
:rows="3"
:maxlength="500"
show-count
size="large"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Input } from 'ant-design-vue';
import {
CheckCircleOutlined,
BankOutlined,
MedicineBoxOutlined,
LaptopOutlined,
GlobalOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
//
const selectedRegulations = ref<string[]>(['contract-law']); //
const selectedIndustry = ref<string>('general'); //
const selectedLevel = ref<string>('standard'); //
const focusPoints = ref<string>(''); //
//
const toggleRegulation = (regulation: string) => {
const index = selectedRegulations.value.indexOf(regulation);
if (index > -1) {
selectedRegulations.value.splice(index, 1);
} else {
selectedRegulations.value.push(regulation);
}
};
//
const selectIndustry = (industry: string) => {
selectedIndustry.value = industry;
};
//
const selectLevel = (level: string) => {
selectedLevel.value = level;
};
//
const getData = () => {
//
if (selectedRegulations.value.length === 0) {
message.warning('请至少选择一个法规范围');
return null;
}
return {
type: 'compliance',
regulations: selectedRegulations.value,
industry: selectedIndustry.value,
level: selectedLevel.value,
focusPoints: focusPoints.value || undefined,
};
};
// getData
defineExpose({
getData
});
</script>
<style lang="less" scoped>
.compliance-content {
padding: 16px;
}
//
.section {
margin-bottom: 24px;
border-bottom: 1px dashed #eee;
padding-bottom: 16px;
&:last-child {
border-bottom: none;
}
}
.section-title {
font-size: 16px;
margin-bottom: 6px;
font-weight: 500;
color: #333;
}
.section-description {
color: #666;
margin-bottom: 16px;
font-size: 14px;
}
//
.regulation-options {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.regulation-card {
flex: 1;
min-width: 280px;
border: 2px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
transition: all 0.3s;
cursor: pointer;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #722ed1;
transform: translateY(-2px);
}
&.selected {
border-color: #722ed1;
box-shadow: 0 0 0 2px rgba(114, 46, 209, 0.2);
background-color: #f9f0ff;
}
}
.card-header {
background-color: #f7f7f7;
padding: 8px;
font-weight: 500;
font-size: 14px;
text-align: center;
color: #333;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
.regulation-card.selected & {
background-color: #722ed1;
color: white;
}
.check-icon {
font-size: 16px;
}
}
.card-body {
padding: 12px;
text-align: center;
h3 {
margin: 0 0 6px 0;
font-size: 14px;
font-weight: 500;
color: #333;
}
.card-desc {
margin: 0;
font-size: 12px;
color: #666;
line-height: 1.4;
}
}
//
.industry-options {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.industry-card {
flex: 1;
min-width: 180px;
border: 2px solid #e8e8e8;
border-radius: 6px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #722ed1;
box-shadow: 0 2px 8px rgba(114, 46, 209, 0.15);
}
&.selected {
border-color: #722ed1;
background-color: #f9f0ff;
box-shadow: 0 0 0 2px rgba(114, 46, 209, 0.2);
}
}
.card-icon {
font-size: 24px;
color: #722ed1;
margin-bottom: 8px;
}
.card-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
color: #333;
}
.card-desc {
font-size: 12px;
color: #666;
line-height: 1.4;
}
//
.level-options {
display: flex;
gap: 12px;
justify-content: center;
}
.level-card {
flex: 1;
max-width: 250px;
border: 2px solid #e8e8e8;
border-radius: 6px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #722ed1;
box-shadow: 0 2px 8px rgba(114, 46, 209, 0.15);
}
&.selected {
border-color: #722ed1;
background-color: #f9f0ff;
box-shadow: 0 0 0 2px rgba(114, 46, 209, 0.2);
}
}
.level-icon {
font-size: 28px;
margin-bottom: 12px;
}
.level-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 6px;
color: #333;
}
.level-desc {
font-size: 12px;
color: #666;
line-height: 1.4;
}
//
.focus-input {
margin-top: 16px;
:deep(.ant-input) {
font-size: 14px !important;
border-radius: 6px !important;
&:focus {
border-color: #722ed1;
box-shadow: 0 0 0 2px rgba(114, 46, 209, 0.2);
}
}
:deep(.ant-input-data-count) {
color: #999;
font-size: 12px;
}
}
</style>

642
src/views/contractReview/ContractualTasks/components/ConsistencyContent.vue

@ -0,0 +1,642 @@
<template>
<div class="consistency-content">
<!-- 对比文件类型选择 -->
<div class="section comparison-section">
<h3 class="section-title">选择对比文件类型</h3>
<p class="section-description">选择需要与合同进行一致性对比的文件类型</p>
<div class="comparison-options">
<div
class="comparison-card"
:class="{ selected: selectedComparisons.includes('tender') }"
@click="toggleComparison('tender')"
>
<div class="card-header">
<CheckCircleOutlined v-if="selectedComparisons.includes('tender')" class="check-icon" />
招标文件
</div>
<div class="card-body">
<h3>与招标文件对比</h3>
<p class="card-desc">检查合同条款是否与招标文件要求一致</p>
</div>
</div>
<div
class="comparison-card"
:class="{ selected: selectedComparisons.includes('bid') }"
@click="toggleComparison('bid')"
>
<div class="card-header">
<CheckCircleOutlined v-if="selectedComparisons.includes('bid')" class="check-icon" />
投标文件
</div>
<div class="card-body">
<h3>与投标文件对比</h3>
<p class="card-desc">检查合同是否履行投标承诺和要求</p>
</div>
</div>
<div
class="comparison-card"
:class="{ selected: selectedComparisons.includes('negotiation') }"
@click="toggleComparison('negotiation')"
>
<div class="card-header">
<CheckCircleOutlined v-if="selectedComparisons.includes('negotiation')" class="check-icon" />
谈判记录
</div>
<div class="card-body">
<h3>与谈判记录对比</h3>
<p class="card-desc">检查合同是否体现谈判确定的内容</p>
</div>
</div>
</div>
</div>
<!-- 检查维度选择 -->
<div class="section dimension-section">
<h3 class="section-title">选择检查维度</h3>
<p class="section-description">选择需要进行一致性检查的关键维度</p>
<div class="dimension-options">
<div
class="dimension-card"
:class="{ selected: selectedDimensions.includes('technical') }"
@click="toggleDimension('technical')"
>
<div class="card-header">
<CheckCircleOutlined v-if="selectedDimensions.includes('technical')" class="check-icon" />
技术规格
</div>
<div class="card-body">
<h3>技术规格一致性</h3>
<p class="card-desc">检查技术参数性能指标质量标准等</p>
</div>
</div>
<div
class="dimension-card"
:class="{ selected: selectedDimensions.includes('commercial') }"
@click="toggleDimension('commercial')"
>
<div class="card-header">
<CheckCircleOutlined v-if="selectedDimensions.includes('commercial')" class="check-icon" />
商务条款
</div>
<div class="card-body">
<h3>商务条款一致性</h3>
<p class="card-desc">检查价格付款方式结算条件等</p>
</div>
</div>
<div
class="dimension-card"
:class="{ selected: selectedDimensions.includes('service') }"
@click="toggleDimension('service')"
>
<div class="card-header">
<CheckCircleOutlined v-if="selectedDimensions.includes('service')" class="check-icon" />
服务要求
</div>
<div class="card-body">
<h3>服务要求一致性</h3>
<p class="card-desc">检查服务内容服务标准售后保障等</p>
</div>
</div>
<div
class="dimension-card"
:class="{ selected: selectedDimensions.includes('delivery') }"
@click="toggleDimension('delivery')"
>
<div class="card-header">
<CheckCircleOutlined v-if="selectedDimensions.includes('delivery')" class="check-icon" />
交付条件
</div>
<div class="card-body">
<h3>交付条件一致性</h3>
<p class="card-desc">检查交付时间交付地点验收标准等</p>
</div>
</div>
</div>
</div>
<!-- 偏离检查级别 -->
<div class="section deviation-section">
<h3 class="section-title">偏离检查级别</h3>
<p class="section-description">设置对偏离情况的检查严格程度</p>
<div class="deviation-options">
<div
class="deviation-card"
:class="{ selected: selectedDeviationLevel === 'strict' }"
@click="selectDeviationLevel('strict')"
>
<div class="card-icon">
<ExclamationCircleOutlined />
</div>
<div class="card-title">严格检查</div>
<div class="card-desc">任何偏离都需要标记和说明</div>
</div>
<div
class="deviation-card"
:class="{ selected: selectedDeviationLevel === 'standard' }"
@click="selectDeviationLevel('standard')"
>
<div class="card-icon">
<WarningOutlined />
</div>
<div class="card-title">标准检查</div>
<div class="card-desc">检查重要偏离和实质性变更</div>
</div>
<div
class="deviation-card"
:class="{ selected: selectedDeviationLevel === 'flexible' }"
@click="selectDeviationLevel('flexible')"
>
<div class="card-icon">
<InfoCircleOutlined />
</div>
<div class="card-title">灵活检查</div>
<div class="card-desc">主要关注关键要素的偏离</div>
</div>
</div>
</div>
<!-- 关键要素优先级 -->
<div class="section priority-section">
<h3 class="section-title">关键要素优先级</h3>
<p class="section-description">设置不同要素的检查优先级</p>
<div class="priority-settings">
<div class="priority-item">
<div class="priority-label">价格金额</div>
<div class="priority-selector">
<div class="priority-buttons">
<div
class="priority-btn"
:class="{ active: priorities.price === 'high' }"
@click="setPriority('price', 'high')"
>
</div>
<div
class="priority-btn"
:class="{ active: priorities.price === 'medium' }"
@click="setPriority('price', 'medium')"
>
</div>
<div
class="priority-btn"
:class="{ active: priorities.price === 'low' }"
@click="setPriority('price', 'low')"
>
</div>
</div>
</div>
</div>
<div class="priority-item">
<div class="priority-label">技术参数</div>
<div class="priority-selector">
<div class="priority-buttons">
<div
class="priority-btn"
:class="{ active: priorities.technical === 'high' }"
@click="setPriority('technical', 'high')"
>
</div>
<div
class="priority-btn"
:class="{ active: priorities.technical === 'medium' }"
@click="setPriority('technical', 'medium')"
>
</div>
<div
class="priority-btn"
:class="{ active: priorities.technical === 'low' }"
@click="setPriority('technical', 'low')"
>
</div>
</div>
</div>
</div>
<div class="priority-item">
<div class="priority-label">交付时间</div>
<div class="priority-selector">
<div class="priority-buttons">
<div
class="priority-btn"
:class="{ active: priorities.delivery === 'high' }"
@click="setPriority('delivery', 'high')"
>
</div>
<div
class="priority-btn"
:class="{ active: priorities.delivery === 'medium' }"
@click="setPriority('delivery', 'medium')"
>
</div>
<div
class="priority-btn"
:class="{ active: priorities.delivery === 'low' }"
@click="setPriority('delivery', 'low')"
>
</div>
</div>
</div>
</div>
<div class="priority-item">
<div class="priority-label">服务承诺</div>
<div class="priority-selector">
<div class="priority-buttons">
<div
class="priority-btn"
:class="{ active: priorities.service === 'high' }"
@click="setPriority('service', 'high')"
>
</div>
<div
class="priority-btn"
:class="{ active: priorities.service === 'medium' }"
@click="setPriority('service', 'medium')"
>
</div>
<div
class="priority-btn"
:class="{ active: priorities.service === 'low' }"
@click="setPriority('service', 'low')"
>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 特别关注点 -->
<div class="section focus-section">
<h3 class="section-title">特别关注点可选</h3>
<p class="section-description">指定需要特别关注的一致性检查要点</p>
<div class="focus-input">
<Input.TextArea
v-model:value="specialFocus"
placeholder="请输入特别关注的一致性要点,如特定技术指标、关键商务条款、重要服务承诺等..."
:rows="3"
:maxlength="500"
show-count
size="large"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { Input } from 'ant-design-vue';
import {
CheckCircleOutlined,
ExclamationCircleOutlined,
WarningOutlined,
InfoCircleOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
//
const selectedComparisons = ref<string[]>(['tender', 'bid']); //
const selectedDimensions = ref<string[]>(['technical', 'commercial', 'delivery']); //
const selectedDeviationLevel = ref<string>('standard'); //
const specialFocus = ref<string>(''); //
//
const priorities = reactive({
price: 'high',
technical: 'high',
delivery: 'medium',
service: 'medium'
});
//
const toggleComparison = (comparison: string) => {
const index = selectedComparisons.value.indexOf(comparison);
if (index > -1) {
selectedComparisons.value.splice(index, 1);
} else {
selectedComparisons.value.push(comparison);
}
};
//
const toggleDimension = (dimension: string) => {
const index = selectedDimensions.value.indexOf(dimension);
if (index > -1) {
selectedDimensions.value.splice(index, 1);
} else {
selectedDimensions.value.push(dimension);
}
};
//
const selectDeviationLevel = (level: string) => {
selectedDeviationLevel.value = level;
};
//
const setPriority = (key: string, level: string) => {
priorities[key] = level;
};
//
const getData = () => {
//
if (selectedComparisons.value.length === 0) {
message.warning('请至少选择一种对比文件类型');
return null;
}
//
if (selectedDimensions.value.length === 0) {
message.warning('请至少选择一个检查维度');
return null;
}
return {
type: 'consistency',
comparisons: selectedComparisons.value,
dimensions: selectedDimensions.value,
deviationLevel: selectedDeviationLevel.value,
priorities: { ...priorities },
specialFocus: specialFocus.value || undefined,
};
};
// getData
defineExpose({
getData
});
</script>
<style lang="less" scoped>
.consistency-content {
padding: 16px;
}
//
.section {
margin-bottom: 24px;
border-bottom: 1px dashed #eee;
padding-bottom: 16px;
&:last-child {
border-bottom: none;
}
}
.section-title {
font-size: 16px;
margin-bottom: 6px;
font-weight: 500;
color: #333;
}
.section-description {
color: #666;
margin-bottom: 16px;
font-size: 14px;
}
//
.comparison-options {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.comparison-card {
flex: 1;
min-width: 280px;
border: 2px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
transition: all 0.3s;
cursor: pointer;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #13c2c2;
transform: translateY(-2px);
}
&.selected {
border-color: #13c2c2;
box-shadow: 0 0 0 2px rgba(19, 194, 194, 0.2);
background-color: #e6fffb;
}
}
//
.dimension-options {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.dimension-card {
border: 2px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
transition: all 0.3s;
cursor: pointer;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #13c2c2;
transform: translateY(-2px);
}
&.selected {
border-color: #13c2c2;
box-shadow: 0 0 0 2px rgba(19, 194, 194, 0.2);
background-color: #e6fffb;
}
}
.card-header {
background-color: #f7f7f7;
padding: 8px;
font-weight: 500;
font-size: 14px;
text-align: center;
color: #333;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
.comparison-card.selected &,
.dimension-card.selected & {
background-color: #13c2c2;
color: white;
}
.check-icon {
font-size: 16px;
}
}
.card-body {
padding: 12px;
text-align: center;
h3 {
margin: 0 0 6px 0;
font-size: 14px;
font-weight: 500;
color: #333;
}
.card-desc {
margin: 0;
font-size: 12px;
color: #666;
line-height: 1.4;
}
}
//
.deviation-options {
display: flex;
gap: 12px;
justify-content: center;
}
.deviation-card {
flex: 1;
max-width: 250px;
border: 2px solid #e8e8e8;
border-radius: 6px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #13c2c2;
box-shadow: 0 2px 8px rgba(19, 194, 194, 0.15);
}
&.selected {
border-color: #13c2c2;
background-color: #e6fffb;
box-shadow: 0 0 0 2px rgba(19, 194, 194, 0.2);
}
}
.card-icon {
font-size: 24px;
color: #13c2c2;
margin-bottom: 8px;
}
.card-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
color: #333;
}
.card-desc {
font-size: 12px;
color: #666;
line-height: 1.4;
}
//
.priority-settings {
background-color: #f9f9f9;
border-radius: 6px;
padding: 16px;
}
.priority-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
.priority-label {
font-size: 14px;
font-weight: 500;
color: #333;
min-width: 80px;
}
.priority-buttons {
display: flex;
gap: 6px;
}
.priority-btn {
padding: 4px 12px;
border: 2px solid #e8e8e8;
border-radius: 16px;
cursor: pointer;
transition: all 0.3s;
font-size: 12px;
font-weight: 500;
color: #666;
&:hover {
border-color: #13c2c2;
color: #13c2c2;
}
&.active {
border-color: #13c2c2;
background-color: #13c2c2;
color: white;
}
}
//
.focus-input {
margin-top: 16px;
:deep(.ant-input) {
font-size: 14px !important;
border-radius: 6px !important;
&:focus {
border-color: #13c2c2;
box-shadow: 0 0 0 2px rgba(19, 194, 194, 0.2);
}
}
:deep(.ant-input-data-count) {
color: #999;
font-size: 12px;
}
}
</style>

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

@ -48,15 +48,22 @@
<!-- 底部按钮 -->
<div class="action-buttons">
<AButton type="primary" class="start-button" @click="startReview" :disabled="uploading">
<AButton
type="primary"
class="start-button"
@click="startReview"
:disabled="uploading"
>
开始审查
</AButton>
</div>
<!-- 审查弹窗 -->
<ReviewDialog
<!-- 审查配置弹窗 -->
<ReviewConfigDialog
@register="registerReviewDialog"
@success="handleReviewSuccess"
@cancel="handleReviewCancel"
:reviewTypes="reviewTypes"
/>
</div>
</template>
@ -70,8 +77,20 @@
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';
import ReviewConfigDialog from './ReviewConfigDialog.vue';
// props
interface Props {
reviewTypes?: string[];
}
const props = withDefaults(defineProps<Props>(), {
reviewTypes: () => []
});
//
const emit = defineEmits(['success', 'register', 'cancel']);
//
const AButton = Button;
@ -83,7 +102,7 @@
const uploadPercent = ref(0);
const currentOssId = ref<string | null>(null);
//
//
const [registerReviewDialog, { openModal: openReviewDialog }] = useModal();
//
@ -227,17 +246,27 @@
return;
}
// OssId
//
openReviewDialog(true, {
ossId: currentOssId.value
ossId: currentOssId.value,
reviewTypes: props.reviewTypes
});
}
//
//
function handleReviewSuccess(data: any) {
console.log('Review completed with data:', data);
//
message.success('审查设置已完成,开始分析合同');
console.log('Review configuration completed with data:', data);
// ossId
emit('success', {
ossId: currentOssId.value,
...data
});
}
//
function handleReviewCancel() {
console.log('Review configuration cancelled');
emit('cancel');
}
</script>

319
src/views/contractReview/ContractualTasks/components/ReviewConfigDialog.vue

@ -0,0 +1,319 @@
<template>
<BasicModal
v-bind="$attrs"
@register="register"
title="合同审查配置"
:width="1200"
:maskClosable="false"
:keyboard="false"
:destroyOnClose="false"
:forceRender="true"
@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 class="review-config-container">
<!-- 实质性审查区域 -->
<div v-if="shouldShowReviewType('substantive')" class="review-section substantive-section">
<div class="section-header">
<h3 class="section-title">实质性审查</h3>
</div>
<SubstantiveContent ref="substantiveRef" />
</div>
<!-- 合规性审查区域 -->
<div v-if="shouldShowReviewType('compliance')" class="review-section compliance-section">
<div class="section-header">
<h3 class="section-title">合规性审查</h3>
</div>
<ComplianceContent ref="complianceRef" />
</div>
<!-- 一致性审查区域 -->
<div v-if="shouldShowReviewType('consistency')" class="review-section consistency-section">
<div class="section-header">
<h3 class="section-title">一致性审查</h3>
</div>
<ConsistencyContent ref="consistencyRef" />
</div>
</div>
</div>
</BasicModal>
</template>
<script setup lang="ts">
import { ref, nextTick, watch } from 'vue';
import { BasicModal, useModalInner } from '@/components/Modal';
import { LoadingOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { AnalyzeContract } from '@/api/contractReview/ContractualTasks';
import SubstantiveContent from './SubstantiveContent.vue';
import ComplianceContent from './ComplianceContent.vue';
import ConsistencyContent from './ConsistencyContent.vue';
// props
interface Props {
reviewTypes?: string[];
}
const props = withDefaults(defineProps<Props>(), {
reviewTypes: () => []
});
//
const emit = defineEmits(['success', 'register', 'cancel']);
//
const analyzing = ref(true);
//
const substantiveRef = ref();
const complianceRef = ref();
const consistencyRef = ref();
// 使modalInnermodal
const [register, { closeModal, redoModalHeight }] = useModalInner((data) => {
//
analyzing.value = true;
console.log('ReviewConfigDialog 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);
//
analyzing.value = false;
// DOMModal
nextTick(() => {
if (redoModalHeight) {
redoModalHeight();
}
});
message.success({ content: '合同分析完成', key: 'analyzing' });
})
.catch((error) => {
console.error('Contract analysis failed:', error);
analyzing.value = false;
//
nextTick(() => {
if (redoModalHeight) {
redoModalHeight();
}
});
message.error({ content: '合同分析失败: ' + (error.message || '未知错误'), key: 'analyzing' });
});
}
});
// analyzingModal
watch(analyzing, () => {
nextTick(() => {
if (redoModalHeight) {
redoModalHeight();
}
});
});
//
function shouldShowReviewType(type: string) {
//
if (!props.reviewTypes || props.reviewTypes.length === 0) {
return type === 'substantive';
}
return props.reviewTypes.includes(type);
}
//
function handleCancel() {
closeModal();
emit('cancel');
}
// -
function handleConfirm() {
const reviewData: any = {};
try {
//
if (shouldShowReviewType('substantive') && substantiveRef.value) {
const substantiveData = substantiveRef.value.getData();
if (!substantiveData) {
message.warning('请完成实质性审查配置');
return;
}
reviewData.substantive = substantiveData;
}
//
if (shouldShowReviewType('compliance') && complianceRef.value) {
const complianceData = complianceRef.value.getData();
if (!complianceData) {
message.warning('请完成合规性审查配置');
return;
}
reviewData.compliance = complianceData;
}
//
if (shouldShowReviewType('consistency') && consistencyRef.value) {
const consistencyData = consistencyRef.value.getData();
if (!consistencyData) {
message.warning('请完成一致性审查配置');
return;
}
reviewData.consistency = consistencyData;
}
//
closeModal();
emit('success', {
reviewTypes: props.reviewTypes,
reviewData: reviewData
});
message.success('审查配置完成,开始分析合同');
} catch (error) {
console.error('收集审查数据失败:', error);
message.error('配置验证失败,请检查填写内容');
}
}
</script>
<style lang="less" scoped>
.review-dialog-content {
padding: 20px;
min-height: 500px;
}
//
.loading-container {
display: flex;
align-items: center;
justify-content: center;
padding: 30px;
min-height: 450px;
width: 100%;
}
.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;
}
}
.review-config-container {
min-height: 450px;
width: 100%;
}
.review-section {
border: 2px solid #ddd;
border-radius: 8px;
margin-bottom: 20px;
overflow: hidden;
}
.review-section:last-child {
margin-bottom: 0;
}
.substantive-section {
border-color: #52c41a;
}
.compliance-section {
border-color: #722ed1;
}
.consistency-section {
border-color: #13c2c2;
}
.section-header {
padding: 16px 20px;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.substantive-section .section-header {
background-color: #f6ffed;
border-bottom-color: #52c41a;
}
.compliance-section .section-header {
background-color: #f9f0ff;
border-bottom-color: #722ed1;
}
.consistency-section .section-header {
background-color: #e6fffb;
border-bottom-color: #13c2c2;
}
.section-title {
font-size: 18px;
font-weight: 500;
margin: 0;
color: #333;
}
.substantive-section .section-title {
color: #52c41a;
}
.compliance-section .section-title {
color: #722ed1;
}
.consistency-section .section-title {
color: #13c2c2;
}
// Modal
:deep(.ant-modal-content) {
overflow: visible;
}
:deep(.ant-modal-body) {
max-height: 80vh;
overflow-y: auto;
}
</style>

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

@ -1,564 +0,0 @@
<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>
</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,
PlusOutlined,
RobotOutlined,
FileTextOutlined
} 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 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 = '';
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 handleCancel() {
closeModal();
emit('cancel');
}
//
function handleConfirm() {
//
if (!selectedPosition.value) {
//
message.warning('请选择您的立场');
return;
}
//
closeModal();
emit('success', {
position: selectedPosition.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-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>

481
src/views/contractReview/ContractualTasks/components/SubstantiveContent.vue

@ -0,0 +1,481 @@
<template>
<div class="substantive-content">
<!-- 选择立场 -->
<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>甲方立场</h3>
<p class="card-desc">从甲方角度审查合同</p>
</div>
</div>
<div
class="position-card"
:class="{ selected: selectedPosition === 'neutral' }"
@click="selectPosition('neutral')"
>
<div class="card-header">中立</div>
<div class="card-body">
<h3>中立立场</h3>
<p class="card-desc">客观中立地审查合同</p>
</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>乙方立场</h3>
<p class="card-desc">从乙方角度审查合同</p>
</div>
</div>
</div>
</div>
<!-- 审查要点类型选择 -->
<div class="section checklist-section">
<h3 class="section-title">选择审查要点类型</h3>
<p class="section-description">选择生成审查要点的方式系统将根据您的选择生成相应的审查清单</p>
<div class="review-type-selector">
<div class="review-type-options">
<div
class="review-type-card"
:class="{ selected: selectedReviewType === 'ai' }"
@click="selectReviewType('ai')"
>
<div class="card-icon">
<RobotOutlined />
</div>
<div class="card-title">AI自动生成</div>
<div class="card-desc">智能分析合同内容生成审查清单</div>
</div>
<div
class="review-type-card"
:class="{ selected: selectedReviewType === 'ai-contract' }"
@click="selectReviewType('ai-contract')"
>
<div class="card-icon">
<RobotOutlined />
<FileTextOutlined />
</div>
<div class="card-title">AI生成+合同类型</div>
<div class="card-desc">基于合同类型模板结合AI智能优化</div>
</div>
<div
class="review-type-card"
:class="{ selected: selectedReviewType === 'contract-type' }"
@click="selectReviewType('contract-type')"
>
<div class="card-icon">
<FileTextOutlined />
</div>
<div class="card-title">合同类型</div>
<div class="card-desc">使用预设的合同类型审查要点</div>
</div>
</div>
<!-- 合同类型选择器 -->
<div v-if="showContractTypeSelector" class="contract-type-selector">
<label class="selector-label">选择合同类型</label>
<Select
v-model:value="selectedContractTypeIds"
mode="multiple"
style="width: 100%"
placeholder="请搜索并选择合同类型..."
:loading="contractTypeLoading"
size="large"
show-search
:filter-option="false"
@search="handleContractTypeSearch"
@change="handleContractTypeChange"
@focus="handleContractTypeFocus"
:dropdownMatchSelectWidth="false"
dropdownClassName="contract-type-dropdown"
:show-arrow="true"
>
<Select.Option
v-for="contractType in filteredContractTypes"
:key="contractType.id"
:value="String(contractType.id)"
>
<div class="contract-type-option">
<span class="contract-name">{{ contractType.contractName }}</span>
<span class="contract-desc" v-if="contractType.remark">{{ contractType.remark }}</span>
</div>
</Select.Option>
</Select>
</div>
</div>
</div>
<!-- 特别说明 -->
<div class="section special-note-section">
<h3 class="section-title">特别说明可选</h3>
<p class="section-description">您可以在此添加针对本次合同审查的特别要求或关注点</p>
<div class="special-note-input">
<Input.TextArea
v-model:value="specialNote"
placeholder="请输入特别说明,如特定的审查重点、风险关注点、合规要求等..."
:rows="3"
:maxlength="500"
show-count
size="large"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { Input, Select } from 'ant-design-vue';
import {
RobotOutlined,
FileTextOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { ContractualTaskTypeList } from '@/api/contractReview/ContractualTaskType';
import type { SelectValue } from 'ant-design-vue/es/select';
import type { ContractualTaskTypeVO } from '@/api/contractReview/ContractualTaskType/model';
//
const selectedPosition = ref('');
const selectedReviewType = ref<string>('ai'); // AI
const selectedContractTypeIds = ref<string[]>([]);
const specialNote = ref<string>(''); //
//
const contractTypeLoading = ref(false);
const contractTypes = ref<ContractualTaskTypeVO[]>([]);
const filteredContractTypes = ref<ContractualTaskTypeVO[]>([]);
const contractTypeSearchValue = ref('');
//
const showContractTypeSelector = computed(() => {
return selectedReviewType.value === 'ai-contract' || selectedReviewType.value === 'contract-type';
});
//
const loadContractTypes = async () => {
contractTypeLoading.value = true;
try {
const res = await ContractualTaskTypeList({});
const data = res as any;
if (data && data.length > 0) {
contractTypes.value = data;
filteredContractTypes.value = data;
} else {
contractTypes.value = [];
filteredContractTypes.value = [];
}
} catch (error) {
console.error('加载合同类型失败:', error);
message.error('加载合同类型失败');
} finally {
contractTypeLoading.value = false;
}
};
//
function selectPosition(position: string) {
selectedPosition.value = position;
}
//
const selectReviewType = (type: string) => {
selectedReviewType.value = type;
//
if (type === 'ai') {
selectedContractTypeIds.value = [];
}
};
//
const handleContractTypeSearch = (value: string) => {
contractTypeSearchValue.value = value;
if (!value) {
filteredContractTypes.value = contractTypes.value;
} else {
filteredContractTypes.value = contractTypes.value.filter(item =>
item.contractName.toLowerCase().includes(value.toLowerCase()) ||
(item.remark && item.remark.toLowerCase().includes(value.toLowerCase()))
);
}
};
//
const handleContractTypeChange = (values: SelectValue) => {
let valueArray: string[] = [];
if (Array.isArray(values)) {
valueArray = values.map(v => String(v));
} else if (values !== undefined && values !== null) {
valueArray = [String(values)];
}
//
contractTypeSearchValue.value = '';
filteredContractTypes.value = contractTypes.value;
};
//
const handleContractTypeFocus = () => {
//
contractTypeSearchValue.value = '';
filteredContractTypes.value = contractTypes.value;
};
//
const getData = () => {
//
if (!selectedPosition.value) {
message.warning('请选择您的立场');
return null;
}
//
if (!selectedReviewType.value) {
message.warning('请选择审查要点类型');
return null;
}
//
if (showContractTypeSelector.value && selectedContractTypeIds.value.length === 0) {
message.warning('请选择合同类型');
return null;
}
return {
position: selectedPosition.value,
reviewType: selectedReviewType.value,
contractTypeIds: selectedContractTypeIds.value || undefined,
specialNote: specialNote.value || undefined,
};
};
// getData
defineExpose({
getData
});
//
onMounted(() => {
loadContractTypes();
});
</script>
<style lang="less" scoped>
.substantive-content {
padding: 16px;
}
//
.section {
margin-bottom: 24px;
border-bottom: 1px dashed #eee;
padding-bottom: 16px;
&:last-child {
border-bottom: none;
}
}
.section-title {
font-size: 16px;
margin-bottom: 6px;
font-weight: 500;
color: #333;
}
.section-description {
color: #666;
margin-bottom: 16px;
font-size: 14px;
}
//
.position-options {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 15px;
}
.position-card {
flex: 1;
border: 2px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
transition: all 0.3s;
cursor: pointer;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #52c41a;
transform: translateY(-2px);
}
&.selected {
border-color: #52c41a;
box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.2);
background-color: #f6ffed;
}
}
.card-header {
background-color: #f7f7f7;
padding: 8px;
font-weight: 500;
font-size: 14px;
text-align: center;
color: #333;
.position-card.selected & {
background-color: #52c41a;
color: white;
}
}
.card-body {
padding: 12px;
text-align: center;
h3 {
margin: 0 0 6px 0;
font-size: 14px;
font-weight: 500;
color: #333;
}
.card-desc {
margin: 0;
font-size: 12px;
color: #666;
line-height: 1.4;
}
}
//
.review-type-selector {
padding: 16px;
}
.review-type-options {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.review-type-card {
flex: 1;
border: 2px solid #e8e8e8;
border-radius: 6px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #52c41a;
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.15);
}
&.selected {
border-color: #52c41a;
background-color: #f6ffed;
box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.2);
}
}
.card-icon {
font-size: 24px;
color: #52c41a;
margin-bottom: 8px;
.anticon + .anticon {
margin-left: 6px;
}
}
.card-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
color: #333;
}
.card-desc {
font-size: 12px;
color: #666;
line-height: 1.4;
}
//
.contract-type-selector {
margin-top: 16px;
padding: 16px;
background-color: #f9f9f9;
border-radius: 6px;
.selector-label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
}
.contract-type-option {
display: flex;
flex-direction: column;
gap: 4px;
.contract-name {
font-weight: 500;
color: #333;
}
.contract-desc {
font-size: 12px;
color: #666;
}
}
//
.special-note-input {
margin-top: 16px;
:deep(.ant-input) {
font-size: 14px !important;
border-radius: 6px !important;
&:focus {
border-color: #52c41a;
box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.2);
}
}
:deep(.ant-input-data-count) {
color: #999;
font-size: 12px;
}
}
</style>

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

@ -6,43 +6,65 @@
<h1 class="markup-title">大模型驱动的新一代合同审查工具</h1>
</div>
<!-- 审查选项卡 -->
<div class="review-tabs">
<div class="tab-buttons">
<!-- 审查选项 -->
<div class="review-options">
<div class="options-title">选择审查任务类型可多选:</div>
<div class="option-buttons">
<div
:class="['tab-button', activeTab === 'inference' ? 'active' : '']"
@click="setActiveTab('inference')"
:class="['option-button', selectedOptions.includes('substantive') ? 'selected' : '']"
@click="toggleOption('substantive')"
>
<span class="tab-icon">📄</span> 推理审查
<span class="option-icon">📄</span>
<div class="option-content">
<div class="option-name">实质性审查</div>
<div class="option-desc">对合同条款的合理性完整性进行深度分析</div>
</div>
</div>
<div
:class="['tab-button', activeTab === 'comparison' ? 'active' : '']"
@click="setActiveTab('comparison')"
:class="['option-button', selectedOptions.includes('compliance') ? 'selected' : '']"
@click="toggleOption('compliance')"
>
<span class="tab-icon">🔄</span> 对比审查
<span class="option-icon"></span>
<div class="option-content">
<div class="option-name">合规性审查</div>
<div class="option-desc">检查合同是否符合相关法律法规要求</div>
</div>
</div>
<div
:class="['option-button', selectedOptions.includes('consistency') ? 'selected' : '']"
@click="toggleOption('consistency')"
>
<span class="option-icon">🔄</span>
<div class="option-content">
<div class="option-name">一致性审查</div>
<div class="option-desc">检查合同与投标文件或招标文件是否一致</div>
</div>
</div>
</div>
</div>
<!-- 根据选项卡切换不同的组件 -->
<InferenceReview v-if="activeTab === 'inference'" />
<ComparisonReview v-else />
<!-- 审查界面 -->
<InferenceReview :reviewTypes="selectedOptions" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import InferenceReview from './components/InferenceReview.vue';
import ComparisonReview from './components/ComparisonReview.vue';
defineOptions({ name: 'ContractualTasks' });
//
const activeTab = ref('inference');
const selectedOptions = ref<string[]>(['substantive']);
//
function setActiveTab(tab) {
activeTab.value = tab;
//
function toggleOption(option: string) {
const index = selectedOptions.value.indexOf(option);
if (index > -1) {
selectedOptions.value.splice(index, 1);
} else {
selectedOptions.value.push(option);
}
}
</script>
@ -76,43 +98,72 @@
margin: 0;
}
.review-tabs {
.review-options {
margin-bottom: 30px;
}
.tab-buttons {
display: flex;
width: 450px;
margin: 0 auto;
background-color: #f0f2f5;
border-radius: 30px;
padding: 4px;
overflow: hidden;
}
.tab-button {
flex: 1;
.options-title {
text-align: center;
padding: 10px 0;
font-size: 16px;
font-size: 18px;
font-weight: 600;
color: #333;
cursor: pointer;
border-radius: 30px;
transition: all 0.3s;
position: relative;
background-color: transparent;
margin-bottom: 20px;
}
.option-buttons {
display: flex;
align-items: center;
gap: 20px;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 30px;
}
.tab-button.active {
.option-button {
width: 300px;
min-height: 120px;
display: flex;
align-items: center;
padding: 20px;
background-color: #fff;
color: #1890ff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 2px solid #e8e8e8;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.option-button:hover {
border-color: #1890ff;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
transform: translateY(-2px);
}
.option-button.selected {
border-color: #1890ff;
background-color: #f6ffed;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.25);
}
.option-icon {
font-size: 24px;
margin-right: 15px;
flex-shrink: 0;
}
.option-content {
flex: 1;
}
.option-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.tab-icon {
margin-right: 5px;
.option-desc {
font-size: 14px;
color: #666;
line-height: 1.4;
}
</style>

Loading…
Cancel
Save