15 changed files with 2942 additions and 1635 deletions
@ -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[]; |
@ -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 },); |
|||
} |
@ -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; |
|||
} |
@ -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', |
|||
} |
|||
]; |
@ -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> |
@ -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> |
|||
|
@ -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">仅支持doc、docx格式文件</p> |
|||
</div> |
|||
</div> |
|||
</AUpload> |
|||
</template> |
|||
</div> |
|||
|
|||
<!-- 右侧上传区域 --> |
|||
<div class="upload-box" :class="{'file-preview': referenceFileList && referenceFileList.length > 0}"> |
|||
<template v-if="referenceFileList && referenceFileList.length > 0"> |
|||
<!-- 右侧文件预览 --> |
|||
<div class="file-info"> |
|||
<FileTextOutlined class="file-icon" /> |
|||
<div class="file-details"> |
|||
<p class="file-name">{{ referenceFileList[0].name }}</p> |
|||
<div class="file-progress" v-if="referenceUploading"> |
|||
<Progress :percent="referenceUploadPercent" size="small" status="active" /> |
|||
</div> |
|||
<p class="file-status" v-else> |
|||
<CheckCircleFilled class="status-icon success" /> 上传成功 |
|||
<AButton type="link" class="remove-btn" @click="removeReferenceFile">删除</AButton> |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<template v-else> |
|||
<!-- 右侧上传 --> |
|||
<AUpload |
|||
:fileList="referenceFileList" |
|||
:customRequest="handleReferenceUpload" |
|||
:beforeUpload="beforeUpload" |
|||
:showUploadList="false" |
|||
:maxCount="1" |
|||
:multiple="false" |
|||
name="file" |
|||
accept=".doc,.docx" |
|||
draggable |
|||
> |
|||
<div class="upload-content"> |
|||
<div class="upload-icon"> |
|||
<UpOutlined class="upload-arrow-icon" /> |
|||
</div> |
|||
<div class="upload-text-container"> |
|||
<p class="upload-text"> |
|||
拖拽对比合同文件至此,或 <a class="upload-link">选择文件</a> |
|||
</p> |
|||
<p class="upload-hint">仅支持doc、docx格式文件</p> |
|||
</div> |
|||
</div> |
|||
</AUpload> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 底部按钮 --> |
|||
<div class="action-buttons"> |
|||
<AButton |
|||
type="primary" |
|||
class="start-button" |
|||
@click="startComparisonReview" |
|||
:disabled="toReviewUploading || referenceUploading" |
|||
> |
|||
开始审查 |
|||
</AButton> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref } from 'vue'; |
|||
import { message, Progress } from 'ant-design-vue'; |
|||
import { UploadOutlined as UpOutlined, FileTextOutlined, CheckCircleFilled } from '@ant-design/icons-vue'; |
|||
import { Button, Upload } from 'ant-design-vue'; |
|||
import type { UploadProps } from 'ant-design-vue'; |
|||
import { uploadDocument } from '@/api/documentReview/DocumentTasks'; |
|||
import { ossRemove } from '@/api/system/oss'; |
|||
import { UploadFileParams } from '#/axios'; |
|||
|
|||
// 注册组件 |
|||
const AButton = Button; |
|||
const AUpload = Upload; |
|||
|
|||
// 状态变量 |
|||
const toReviewFileList = ref<UploadProps['fileList']>([]); |
|||
const referenceFileList = ref<UploadProps['fileList']>([]); |
|||
const toReviewUploading = ref(false); |
|||
const referenceUploading = ref(false); |
|||
const toReviewUploadPercent = ref(0); |
|||
const referenceUploadPercent = ref(0); |
|||
const toReviewOssId = ref<string | null>(null); |
|||
const referenceOssId = ref<string | null>(null); |
|||
|
|||
// 待审查合同上传处理 |
|||
function handleToReviewUpload(options: any) { |
|||
const { file, onSuccess, onError } = options; |
|||
|
|||
// 设置上传状态 |
|||
toReviewUploading.value = true; |
|||
toReviewUploadPercent.value = 0; |
|||
|
|||
// 创建上传参数 |
|||
const uploadParams: UploadFileParams = { |
|||
name: 'file', |
|||
file: file, |
|||
data: { |
|||
// 可以设置额外参数 |
|||
fileType: 'contract_review_to_review', |
|||
}, |
|||
}; |
|||
|
|||
// 显示初始文件(上传中状态) |
|||
toReviewFileList.value = [ |
|||
{ |
|||
uid: '1', |
|||
name: file.name, |
|||
status: 'uploading', |
|||
url: URL.createObjectURL(file), |
|||
} as any, |
|||
]; |
|||
|
|||
// 调用真实的上传API |
|||
uploadDocument( |
|||
uploadParams, |
|||
(progressEvent) => { |
|||
// 处理上传进度 |
|||
if (progressEvent.total) { |
|||
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100); |
|||
toReviewUploadPercent.value = percent; |
|||
} |
|||
} |
|||
).then((res) => { |
|||
// 上传成功处理 |
|||
if (toReviewFileList.value && toReviewFileList.value.length > 0) { |
|||
toReviewFileList.value[0].status = 'done'; |
|||
toReviewFileList.value[0].response = res; // 保存返回数据 |
|||
// 保存OSS ID,方便后续删除 |
|||
if (res && res.ossId) { |
|||
toReviewOssId.value = res.ossId; |
|||
} |
|||
} |
|||
toReviewUploading.value = false; |
|||
toReviewUploadPercent.value = 100; |
|||
message.success(`待审查合同上传成功`); |
|||
onSuccess(res); |
|||
}).catch((err) => { |
|||
// 上传失败处理 |
|||
toReviewUploading.value = false; |
|||
toReviewFileList.value = []; |
|||
toReviewOssId.value = null; |
|||
message.error(`待审查合同上传失败: ${err.message || '未知错误'}`); |
|||
onError(err); |
|||
}); |
|||
} |
|||
|
|||
// 对比合同上传处理 |
|||
function handleReferenceUpload(options: any) { |
|||
const { file, onSuccess, onError } = options; |
|||
|
|||
// 设置上传状态 |
|||
referenceUploading.value = true; |
|||
referenceUploadPercent.value = 0; |
|||
|
|||
// 创建上传参数 |
|||
const uploadParams: UploadFileParams = { |
|||
name: 'file', |
|||
file: file, |
|||
data: { |
|||
// 可以设置额外参数 |
|||
fileType: 'contract_review_reference', |
|||
}, |
|||
}; |
|||
|
|||
// 显示初始文件(上传中状态) |
|||
referenceFileList.value = [ |
|||
{ |
|||
uid: '1', |
|||
name: file.name, |
|||
status: 'uploading', |
|||
url: URL.createObjectURL(file), |
|||
} as any, |
|||
]; |
|||
|
|||
// 调用真实的上传API |
|||
uploadDocument( |
|||
uploadParams, |
|||
(progressEvent) => { |
|||
// 处理上传进度 |
|||
if (progressEvent.total) { |
|||
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100); |
|||
referenceUploadPercent.value = percent; |
|||
} |
|||
} |
|||
).then((res) => { |
|||
// 上传成功处理 |
|||
if (referenceFileList.value && referenceFileList.value.length > 0) { |
|||
referenceFileList.value[0].status = 'done'; |
|||
referenceFileList.value[0].response = res; // 保存返回数据 |
|||
// 保存OSS ID,方便后续删除 |
|||
if (res && res.ossId) { |
|||
referenceOssId.value = res.ossId; |
|||
} |
|||
} |
|||
referenceUploading.value = false; |
|||
referenceUploadPercent.value = 100; |
|||
message.success(`对比合同上传成功`); |
|||
onSuccess(res); |
|||
}).catch((err) => { |
|||
// 上传失败处理 |
|||
referenceUploading.value = false; |
|||
referenceFileList.value = []; |
|||
referenceOssId.value = null; |
|||
message.error(`对比合同上传失败: ${err.message || '未知错误'}`); |
|||
onError(err); |
|||
}); |
|||
} |
|||
|
|||
// 删除待审查合同 |
|||
function removeToReviewFile() { |
|||
if (toReviewUploading.value) { |
|||
message.warning('文件正在上传中,请稍后再试'); |
|||
return; |
|||
} |
|||
|
|||
// 使用保存的OSS ID进行删除 |
|||
if (toReviewOssId.value) { |
|||
// 传入数组形式的参数,符合IDS类型要求 |
|||
ossRemove([toReviewOssId.value]) |
|||
.then(() => { |
|||
toReviewFileList.value = []; |
|||
toReviewUploadPercent.value = 0; |
|||
toReviewOssId.value = null; |
|||
message.success('待审查合同已删除'); |
|||
}) |
|||
.catch((err) => { |
|||
message.error(`文件删除失败: ${err.message || '未知错误'}`); |
|||
}); |
|||
} else { |
|||
// 如果没有ossId,直接清空本地状态 |
|||
toReviewFileList.value = []; |
|||
toReviewUploadPercent.value = 0; |
|||
message.success('待审查合同已删除'); |
|||
} |
|||
} |
|||
|
|||
// 删除对比合同 |
|||
function removeReferenceFile() { |
|||
if (referenceUploading.value) { |
|||
message.warning('文件正在上传中,请稍后再试'); |
|||
return; |
|||
} |
|||
|
|||
// 使用保存的OSS ID进行删除 |
|||
if (referenceOssId.value) { |
|||
// 传入数组形式的参数,符合IDS类型要求 |
|||
ossRemove([referenceOssId.value]) |
|||
.then(() => { |
|||
referenceFileList.value = []; |
|||
referenceUploadPercent.value = 0; |
|||
referenceOssId.value = null; |
|||
message.success('对比合同已删除'); |
|||
}) |
|||
.catch((err) => { |
|||
message.error(`文件删除失败: ${err.message || '未知错误'}`); |
|||
}); |
|||
} else { |
|||
// 如果没有ossId,直接清空本地状态 |
|||
referenceFileList.value = []; |
|||
referenceUploadPercent.value = 0; |
|||
message.success('对比合同已删除'); |
|||
} |
|||
} |
|||
|
|||
function beforeUpload(file: File) { |
|||
const isDocx = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || |
|||
file.type === 'application/msword'; |
|||
if (!isDocx) { |
|||
message.error('只能上传 doc/docx 格式的文件!'); |
|||
return false; |
|||
} |
|||
|
|||
// 文件大小限制(500MB) |
|||
const isLt500M = file.size / 1024 / 1024 < 500; |
|||
if (!isLt500M) { |
|||
message.error('文件大小不能超过 500MB!'); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
// 开始对比审查 |
|||
function startComparisonReview() { |
|||
if (toReviewUploading.value || referenceUploading.value) { |
|||
message.warning('文件正在上传中,请稍后再试'); |
|||
return; |
|||
} |
|||
|
|||
if (!toReviewFileList.value || toReviewFileList.value.length === 0) { |
|||
message.warning('请先上传待审查合同'); |
|||
return; |
|||
} |
|||
if (!referenceFileList.value || referenceFileList.value.length === 0) { |
|||
message.warning('请先上传对比合同'); |
|||
return; |
|||
} |
|||
|
|||
// 使用保存的OSS ID进行审查 |
|||
if (!toReviewOssId.value || !referenceOssId.value) { |
|||
message.warning('文件上传异常,请重新上传'); |
|||
return; |
|||
} |
|||
|
|||
// TODO: 调用对比审查API,传入文件ID等信息 |
|||
message.success('开始对比审查'); |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.comparison-container { |
|||
background-color: #fff; |
|||
border-radius: 12px; |
|||
padding: 30px; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
|||
margin-top: 30px; |
|||
max-width: 1100px; |
|||
margin-left: auto; |
|||
margin-right: auto; |
|||
} |
|||
|
|||
.comparison-header { |
|||
display: flex; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.comparison-title { |
|||
flex: 1; |
|||
display: flex; |
|||
align-items: center; |
|||
font-size: 16px; |
|||
color: #333; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.dot-indicator { |
|||
width: 8px; |
|||
height: 8px; |
|||
background-color: #52c41a; |
|||
border-radius: 50%; |
|||
margin-right: 8px; |
|||
} |
|||
|
|||
.comparison-upload-area { |
|||
display: flex; |
|||
gap: 20px; |
|||
margin-bottom: 30px; |
|||
} |
|||
|
|||
.upload-box { |
|||
flex: 1; |
|||
border: 1px dashed #ddd; |
|||
border-radius: 8px; |
|||
padding: 20px; |
|||
height: 240px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background-color: #f9f9f9; |
|||
} |
|||
|
|||
.upload-box.file-preview { |
|||
border: 2px solid #e6f7ff; |
|||
background-color: #f0f8ff; |
|||
padding: 20px; |
|||
} |
|||
|
|||
.upload-box:hover { |
|||
border-color: #1890ff; |
|||
background-color: rgba(24, 144, 255, 0.02); |
|||
} |
|||
|
|||
.upload-content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
width: 100%; |
|||
} |
|||
|
|||
.upload-icon { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
margin-bottom: 20px; |
|||
background-color: #e6f7ff; |
|||
width: 60px; |
|||
height: 60px; |
|||
border-radius: 50%; |
|||
} |
|||
|
|||
.upload-arrow-icon { |
|||
font-size: 28px; |
|||
color: #1890ff; |
|||
} |
|||
|
|||
.upload-text-container { |
|||
text-align: center; |
|||
} |
|||
|
|||
.upload-text { |
|||
font-size: 18px; |
|||
color: #333; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.upload-link { |
|||
color: #1890ff; |
|||
cursor: pointer; |
|||
text-decoration: none; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
.upload-link:hover { |
|||
text-decoration: underline; |
|||
} |
|||
|
|||
.upload-hint { |
|||
color: #888; |
|||
font-size: 14px; |
|||
margin-top: 8px; |
|||
margin-bottom: 15px; |
|||
} |
|||
|
|||
.file-info { |
|||
display: flex; |
|||
align-items: center; |
|||
width: 100%; |
|||
} |
|||
|
|||
.file-icon { |
|||
font-size: 36px; |
|||
color: #1890ff; |
|||
margin-right: 20px; |
|||
} |
|||
|
|||
.file-details { |
|||
flex: 1; |
|||
} |
|||
|
|||
.file-name { |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
margin: 0 0 10px 0; |
|||
color: #333; |
|||
} |
|||
|
|||
.file-progress { |
|||
margin: 0 0 10px 0; |
|||
width: 100%; |
|||
} |
|||
|
|||
.file-status { |
|||
font-size: 14px; |
|||
color: #666; |
|||
margin: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.status-icon { |
|||
margin-right: 8px; |
|||
} |
|||
|
|||
.status-icon.success { |
|||
color: #52c41a; |
|||
} |
|||
|
|||
.remove-btn { |
|||
padding: 0; |
|||
margin-left: 15px; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.action-buttons { |
|||
display: flex; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.start-button { |
|||
width: 180px; |
|||
height: 44px; |
|||
background-color: #52c41a; |
|||
border-color: #52c41a; |
|||
font-size: 16px; |
|||
border-radius: 22px; |
|||
font-weight: 500; |
|||
} |
|||
</style> |
@ -0,0 +1,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> |
@ -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> |
@ -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(); |
|||
|
|||
// 使用modalInner注册modal,并在弹窗打开时启动分析过程 |
|||
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; |
|||
|
|||
// 等待DOM更新后重新计算Modal高度 |
|||
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' }); |
|||
}); |
|||
} |
|||
}); |
|||
|
|||
// 监听analyzing状态变化,自动调整Modal高度 |
|||
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> |
@ -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('新建清单功能开发中'); |
|||
}; |
|||
|
|||
// 使用modalInner注册modal,并在弹窗打开时启动分析过程 |
|||
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> |
@ -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> |
Loading…
Reference in new issue