Browse Source

新增合同法规合同模板前端

ai_dev
zhouhaibin 3 weeks ago
parent
commit
56a0ae7722
  1. 98
      src/api/contractReview/ContractualCaseFiles/index.ts
  2. 76
      src/api/contractReview/ContractualCaseFiles/model.ts
  3. 98
      src/api/contractReview/ContractualRegulationNames/index.ts
  4. 98
      src/api/contractReview/ContractualRegulationNames/model.ts
  5. 133
      src/api/contractReview/ContractualTaskResults/index.ts
  6. 99
      src/api/contractReview/ContractualTaskResults/model.ts
  7. 14
      src/api/contractReview/ContractualTasks/index.ts
  8. 111
      src/api/contractReview/ContractualTasks/model.ts
  9. 9
      src/api/documentReview/DocumentTaskResults/index.ts
  10. 6
      src/assets/images/markup-logo.svg
  11. 497
      src/views/contractReview/ContractualCaseFiles/AddModal.vue
  12. 460
      src/views/contractReview/ContractualCaseFiles/EditModal.vue
  13. 382
      src/views/contractReview/ContractualCaseFiles/ViewModal.vue
  14. 458
      src/views/contractReview/ContractualCaseFiles/index.vue
  15. 476
      src/views/contractReview/ContractualRegulationNames/AddModal.vue
  16. 108
      src/views/contractReview/ContractualRegulationNames/ContractualRegulationNames.data.ts
  17. 450
      src/views/contractReview/ContractualRegulationNames/EditModal.vue
  18. 382
      src/views/contractReview/ContractualRegulationNames/ViewModal.vue
  19. 442
      src/views/contractReview/ContractualRegulationNames/index.vue
  20. 275
      src/views/contractReview/ContractualTasks/ContractualResultDetailDrawer.vue
  21. 418
      src/views/contractReview/ContractualTasks/components/ComplianceContent.vue
  22. 751
      src/views/contractReview/ContractualTasks/components/ConsistencyContent.vue
  23. 45
      src/views/contractReview/ContractualTasks/components/InferenceReview.vue
  24. 265
      src/views/contractReview/ContractualTasks/components/ReviewConfigDialog.vue
  25. 285
      src/views/contractReview/ContractualTasks/components/SubstantiveContent.vue
  26. 3
      src/views/contractReview/ContractualTasks/index.vue
  27. 39
      src/views/contractReview/ContractualTasks/list.vue
  28. 32
      src/views/documentReview/DocumentTasks/DocumentTasksTable.vue
  29. 13
      src/views/documentReview/DocumentTasks/ResultDetailDrawer.vue
  30. 29
      src/views/schemeEvaluation/SchemeEvaluation/index.vue
  31. 15
      src/views/tenderReview/TenderTask/index.vue
  32. 17
      src/views/tenderReview/inconsistency/index.vue

98
src/api/contractReview/ContractualCaseFiles/index.ts

@ -0,0 +1,98 @@
import { defHttp } from '@/utils/http/axios';
import { ID, IDS, commonExport } from '@/api/base';
import { ContractualCaseFilesVO, ContractualCaseFilesForm, ContractualCaseFilesQuery } from './model';
/**
*
* @param params
* @returns
*/
export function ContractualCaseFilesList(params?: ContractualCaseFilesQuery) {
return defHttp.get<ContractualCaseFilesVO[]>({ url: '/productManagement/ContractualCaseFiles/list', params });
}
/**
*
* @param params
* @returns
*/
export function ContractualCaseFilesExport(params?: ContractualCaseFilesQuery) {
return commonExport('/productManagement/ContractualCaseFiles/export', params ?? {});
}
/**
*
* @param id id
* @returns
*/
export function ContractualCaseFilesInfo(id: ID) {
return defHttp.get<ContractualCaseFilesVO>({ url: '/productManagement/ContractualCaseFiles/' + id });
}
/**
*
* @param data
* @returns
*/
export function ContractualCaseFilesAdd(data: ContractualCaseFilesForm) {
return defHttp.postWithMsg<void>({ url: '/productManagement/ContractualCaseFiles', data });
}
/**
*
* @param data
* @returns
*/
export function ContractualCaseFilesUpdate(data: ContractualCaseFilesForm) {
return defHttp.putWithMsg<void>({ url: '/productManagement/ContractualCaseFiles', data });
}
/**
*
* @param id
* @param status
* @returns
*/
export function ContractualCaseFilesUpdateStatus(id: ID, status: string) {
return defHttp.putWithMsg<void>({
url: '/productManagement/ContractualCaseFiles/updateStatus',
data: { id, isEffective: status }
});
}
/**
*
* @param id id
* @returns
*/
export function ContractualCaseFilesRemove(id: ID | IDS) {
return defHttp.deleteWithMsg<void>({ url: '/productManagement/ContractualCaseFiles/' + id },);
}
/**
*
* @param id ID
* @returns
*/
export function ContractualCaseFilesArticles(id: ID) {
return defHttp.get<any[]>({ url: '/productManagement/ContractualCaseFiles/articles/' + id });
}
/**
* PDF文档
* @param id ID
* @returns PDF响应数据
*/
export function ContractualCaseFilesViewPdf(id: ID) {
return defHttp.get(
{
url: '/productManagement/ContractualCaseFiles/view/' + id,
responseType: 'blob',
timeout: 1000 * 60 * 10,
headers: {
Accept: 'application/pdf',
}
},
{ isReturnNativeResponse: true }
);
}

76
src/api/contractReview/ContractualCaseFiles/model.ts

@ -0,0 +1,76 @@
import type { BaseEntity, PageQuery } from '@/api/base';
export interface ContractualCaseFilesVO extends BaseEntity {
/** 主键ID */
id?: string | number;
/** 案例名称 */
caseName?: string;
/** 案例描述 */
caseDescription?: string;
/** 案例类型 */
caseType?: string;
/** 发布日期 */
publishDate?: string;
/** 是否有效(Y:有效 N:无效) */
isEffective?: string;
/** 处理状态 */
progressStatus?: string;
/** OSS文件ID */
ossId?: string | number;
}
export interface ContractualCaseFilesForm extends BaseEntity {
/** 主键ID */
id?: string | number;
/** 案例名称 */
caseName?: string;
/** 案例描述 */
caseDescription?: string;
/** 案例类型 */
caseType?: string;
/** 发布日期 */
publishDate?: string;
/** 是否有效(Y:有效 N:无效) */
isEffective?: string;
/** 处理状态 */
progressStatus?: string;
/** OSS文件ID */
ossId?: string | number;
}
export interface ContractualCaseFilesQuery extends PageQuery {
/** 案例名称 */
caseName?: string;
/** 案例类型 */
caseType?: string;
/** 发布日期 */
publishDate?: string;
/** 日期范围查询-开始日期 */
publishDateStart?: string;
/** 日期范围查询-结束日期 */
publishDateEnd?: string;
/** 是否有效(Y:有效 N:无效) */
isEffective?: string;
/** 处理状态 */
progressStatus?: string;
}

98
src/api/contractReview/ContractualRegulationNames/index.ts

@ -0,0 +1,98 @@
import { defHttp } from '@/utils/http/axios';
import { ID, IDS, commonExport } from '@/api/base';
import { ContractualRegulationNamesVO, ContractualRegulationNamesForm, ContractualRegulationNamesQuery } from './model';
/**
*
* @param params
* @returns
*/
export function ContractualRegulationNamesList(params?: ContractualRegulationNamesQuery) {
return defHttp.get<ContractualRegulationNamesVO[]>({ url: '/productManagement/ContractualRegulationNames/list', params });
}
/**
*
* @param params
* @returns
*/
export function ContractualRegulationNamesExport(params?: ContractualRegulationNamesQuery) {
return commonExport('/productManagement/ContractualRegulationNames/export', params ?? {});
}
/**
*
* @param id id
* @returns
*/
export function ContractualRegulationNamesInfo(id: ID) {
return defHttp.get<ContractualRegulationNamesVO>({ url: '/productManagement/ContractualRegulationNames/' + id });
}
/**
*
* @param data
* @returns
*/
export function ContractualRegulationNamesAdd(data: ContractualRegulationNamesForm) {
return defHttp.postWithMsg<void>({ url: '/productManagement/ContractualRegulationNames', data });
}
/**
*
* @param data
* @returns
*/
export function ContractualRegulationNamesUpdate(data: ContractualRegulationNamesForm) {
return defHttp.putWithMsg<void>({ url: '/productManagement/ContractualRegulationNames', data });
}
/**
*
* @param id
* @param status
* @returns
*/
export function ContractualRegulationNamesUpdateStatus(id: ID, status: string) {
return defHttp.putWithMsg<void>({
url: '/productManagement/ContractualRegulationNames/updateStatus',
data: { id, isEffective: status }
});
}
/**
*
* @param id id
* @returns
*/
export function ContractualRegulationNamesRemove(id: ID | IDS) {
return defHttp.deleteWithMsg<void>({ url: '/productManagement/ContractualRegulationNames/' + id },);
}
/**
*
* @param id ID
* @returns
*/
export function ContractualRegulationNamesArticles(id: ID) {
return defHttp.get<any[]>({ url: '/productManagement/ContractualRegulationNames/articles/' + id });
}
/**
* PDF文档
* @param id ID
* @returns PDF响应数据
*/
export function ContractualRegulationNamesViewPdf(id: ID) {
return defHttp.get(
{
url: '/productManagement/ContractualRegulationNames/view/' + id,
responseType: 'blob',
timeout: 1000 * 60 * 10,
headers: {
Accept: 'application/pdf',
}
},
{ isReturnNativeResponse: true }
);
}

98
src/api/contractReview/ContractualRegulationNames/model.ts

@ -0,0 +1,98 @@
import { BaseEntity, PageQuery } from '@/api/base';
export interface ContractualRegulationNamesVO {
/**
* ID
*/
id: string | number;
/**
*
*/
regulationName: string;
/**
*
*/
regulationDescription: string;
/**
*
*/
publishDate: string;
/**
* (Y:有效 N:无效)
*/
isEffective: string;
/**
*
*/
progressStatus?: string;
/**
* OSS文件ID
*/
ossId?: string | number;
}
export interface ContractualRegulationNamesForm extends BaseEntity {
/**
* ID
*/
id?: string | number;
/**
*
*/
regulationName?: string;
/**
*
*/
regulationDescription?: string;
/**
*
*/
publishDate?: string;
/**
* (Y:有效 N:无效)
*/
isEffective?: string;
/**
*
*/
progressStatus?: string;
/**
* OSS文件ID
*/
ossId?: string | number;
}
export interface ContractualRegulationNamesQuery extends PageQuery {
/**
*
*/
regulationName?: string;
/**
*
*/
publishDate?: string;
/**
* (Y:有效 N:无效)
*/
isEffective?: string;
/**
*
*/
params?: any;
}

133
src/api/contractReview/ContractualTaskResults/index.ts

@ -0,0 +1,133 @@
import { defHttp } from '@/utils/http/axios';
import { ID, IDS } from '@/api/base';
import { ContractualTaskResultDetailVO } from './model';
import { message } from 'ant-design-vue';
/**
*
* @param url
* @param onError
* @returns Promise<boolean>
*/
export async function useDownload(
url: string,
onError?: (error: any) => void
): Promise<boolean> {
try {
const response = await defHttp.get(
{
url,
responseType: 'blob',
timeout: 60000, // 设置较长的超时时间
},
{
isReturnNativeResponse: true,
// 自定义错误处理
errorMessageMode: 'none',
}
);
// 检查响应类型
const contentType = response.headers['content-type'];
if (contentType && contentType.includes('application/json')) {
// 如果返回的是JSON(通常是错误信息),转换并抛出
const reader = new FileReader();
reader.onload = () => {
const error = JSON.parse(reader.result as string);
message.error(error.message || '下载失败');
onError?.(error);
};
reader.readAsText(response.data);
return false;
}
// 获取文件名
const contentDisposition = response.headers['content-disposition'];
let fileName = '';
if (contentDisposition) {
const matches = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (matches && matches[1]) {
fileName = decodeURIComponent(matches[1].replace(/['"]/g, ''));
}
}
// 创建Blob对象
const blob = new Blob([response.data], {
type: contentType || 'application/octet-stream'
});
if ((window.navigator as any).msSaveOrOpenBlob) {
// 针对IE的处理
(window.navigator as any).msSaveOrOpenBlob(blob, fileName);
} else {
// 现代浏览器的处理
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
return true;
} catch (error: any) {
console.error('下载失败:', error);
message.error(error.message || '下载失败,请稍后重试');
onError?.(error);
return false;
}
}
/**
* id查询详细分类的合同任务结果
* @param taskId ID
* @returns
*/
export function getDetailResultsByTaskId(taskId: ID) {
return defHttp.get<ContractualTaskResultDetailVO[]>({ url: '/productManagement/ContractualTaskResults/taskDetail/' + taskId });
}
/**
*
* @param id id
* @returns
*/
export function ContractualTaskResultDownload(id: ID | IDS) {
return useDownload(`/productManagement/ContractualTaskResults/downloadResult/${id}`);
}
/**
* PDF文件流
* @param taskId ID
* @returns Promise<Blob>
*/
export function getPdfStream(taskId: ID): Promise<Blob> {
return defHttp.get(
{
url: `/productManagement/ContractualTaskResults/getPdfStream/${taskId}`,
responseType: 'blob',
timeout:600000
},
{
isReturnNativeResponse: true,
errorMessageMode: 'none',
}
).then(response => response.data);
}
/**
* /
* @param id ID
* @param field isRead/isAdopted
* @param value 0/1
* @returns
*/
export function updateResultItemStatus(id: ID, field: 'isRead' | 'isAdopted', value: '0' | '1') {
return defHttp.putWithMsg<void>({
url: `/productManagement/ContractualTaskResults/updateResultItemStatus/${id}/${field}/${value}`
});
}

99
src/api/contractReview/ContractualTaskResults/model.ts

@ -0,0 +1,99 @@
import { BaseEntity, PageQuery } from '@/api/base';
export interface ContractualTaskResultsVO {
/**
*
*/
result?: string;
}
export interface ContractualTaskResultDetailVO {
/**
*
*/
name: string;
/**
*
*/
results: {
/**
* ID
*/
id?: string;
/**
*
*/
serialNumber?: number;
/**
*
*/
issueName?: string;
/**
*
*/
originalText?: string;
/**
*
*/
comparedText?: string;
/**
*
*/
modifiedContent?: string;
/**
*
*/
modificationDisplay?: string;
/**
*
*/
existingIssues?: string;
/**
*
*/
reviewBasis?: {
/**
*
*/
reviewContent?: string;
/**
*
*/
reviewPoints?: string[];
};
/**
*
*/
isRead?: string;
/**
*
*/
isAdopted?: string;
}[];
}
export interface ContractualTaskResultsForm extends BaseEntity {
}
export interface ContractualTaskResultsQuery extends PageQuery {
/**
* id
*/
id?: string | number;
/**
* id
*/
contractualTaskId?: string | number;
/**
*
*/
result?: string;
/**
*
*/
params?: any;
}

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

@ -1,6 +1,6 @@
import { defHttp } from '@/utils/http/axios';
import { ID, IDS, commonExport } from '@/api/base';
import { ContractualTasksVO, ContractualTasksForm, ContractualTasksQuery } from './model';
import { ContractualTasksVO, ContractualTasksForm, ContractualTasksQuery, StartContractReviewRequest } from './model';
/**
*
@ -64,3 +64,15 @@ export function ContractualTasksRemove(id: ID | IDS) {
export function AnalyzeContract(ossId: string) {
return defHttp.get<any>({ url: '/productManagement/ContractualTasks/analyzeContract', params: { ossId } });
}
export function StartReview(data: StartContractReviewRequest) {
return defHttp.postWithMsg<any>({
url: '/productManagement/ContractualTasks/startReview',
data,
timeout: 300000 // 5分钟超时,因为可能需要等待Python处理
});
}
// 导出类型定义
export type { StartContractReviewRequest } from './model';

111
src/api/contractReview/ContractualTasks/model.ts

@ -113,3 +113,114 @@ export interface ContractualTasksQuery extends PageQuery {
*/
params?: any;
}
/**
*
* @param data
* @returns
*/
export interface StartContractReviewRequest {
ossId: string;
reviewTypes: string[];
reviewData: ReviewData;
visitedTabs: string[];
}
/**
*
*/
export interface ReviewData {
/**
*
*/
substantive?: SubstantiveData;
/**
*
*/
compliance?: ComplianceData;
/**
*
*/
consistency?: ConsistencyData;
}
/**
*
*/
export interface SubstantiveData {
/**
* ID列表
*/
contractTypeIds?: string[];
/**
* /
*/
position?: string;
/**
*
*/
reviewType?: string;
/**
*
*/
specialNote?: string;
}
/**
*
*/
export interface ComplianceData {
/**
*
*/
focusPoints?: string[];
/**
*
*/
industry?: string;
/**
*
*/
level?: string;
/**
*
*/
regulations?: string[];
/**
*
*/
type?: string;
}
/**
*
*/
export interface ConsistencyData {
/**
*
*/
fileTypes?: string[];
/**
*
*/
dimensions?: string[];
/**
*
*/
deviationLevel?: string;
/**
*
*/
specialNote?: string;
}

9
src/api/documentReview/DocumentTaskResults/index.ts

@ -114,14 +114,7 @@ export function DocumentTaskResultsInfo(id: ID) {
return defHttp.get<DocumentTaskResultsVO>({ url: '/productManagement/DocumentTaskResults/' + id });
}
/**
* id查询文档任务结果详细
* @param id id
* @returns
*/
export function DocumentTaskResultsInfoByTaskId(id: ID) {
return defHttp.get<DocumentTaskResultsVO>({ url: '/productManagement/DocumentTaskResults/task/' + id });
}
/**
* id查询详细分类的文档任务结果

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

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

Before

Width:  |  Height:  |  Size: 368 B

497
src/views/contractReview/ContractualCaseFiles/AddModal.vue

@ -0,0 +1,497 @@
<template>
<Modal
:open="visible"
title="新增合同案例文件"
@ok="handleSubmit"
@cancel="handleCancel"
:confirmLoading="loading"
width="600px"
>
<Form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<!-- 文件上传 -->
<FormItem label="案例文件" name="caseFile" :required="!hasUploadedFile">
<div class="upload-box" :class="{'file-preview': fileList && fileList.length > 0}">
<template v-if="fileList && fileList.length > 0">
<!-- 文件预览 -->
<div class="file-info">
<FileTextOutlined class="file-icon" />
<div class="file-details">
<p class="file-name">{{ fileList[0].name }}</p>
<div class="file-progress" v-if="uploading">
<Progress :percent="uploadPercent" size="small" status="active" />
</div>
<p class="file-status" v-else>
<CheckCircleFilled class="status-icon success" /> 上传成功
<Button type="link" class="remove-btn" @click="removeFile">删除</Button>
</p>
</div>
</div>
</template>
<template v-else>
<!-- 上传区域 -->
<Upload
:fileList="fileList"
:customRequest="customUploadRequest"
:beforeUpload="beforeUpload"
:showUploadList="false"
:maxCount="1"
:multiple="false"
name="file"
accept=".doc,.docx"
:disabled="uploading"
>
<div class="upload-content">
<div class="upload-icon">
<UploadOutlined class="upload-arrow-icon" />
</div>
<div class="upload-text-container">
<p class="upload-text">
<a class="upload-link">选择案例文件</a>
</p>
<p class="upload-hint">支持DOCDOCX格式文件最大500MB</p>
</div>
</div>
</Upload>
</template>
</div>
</FormItem>
<!-- 案例名称 -->
<FormItem label="案例名称" name="caseName" required>
<Input
v-model:value="formData.caseName"
:disabled="caseNameDisabled"
:placeholder="caseNamePlaceholder"
/>
</FormItem>
<!-- 案例描述 -->
<FormItem label="案例描述" name="caseDescription" required>
<TextArea
v-model:value="formData.caseDescription"
placeholder="请输入案例描述"
:rows="4"
show-count
:maxlength="500"
/>
</FormItem>
<!-- 案例类型 -->
<FormItem label="案例类型" name="caseType">
<Input v-model:value="formData.caseType" placeholder="请输入案例类型" />
</FormItem>
<!-- 发布日期 -->
<FormItem label="发布日期" name="publishDate" required>
<DatePicker
v-model:value="formData.publishDate"
format="YYYY-MM-DD"
valueFormat="YYYY-MM-DD"
style="width: 100%"
/>
</FormItem>
<!-- 是否有效 -->
<FormItem label="是否有效" name="isEffective">
<Select
v-model:value="formData.isEffective"
:options="effectiveOptions"
/>
</FormItem>
</Form>
</Modal>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue';
import {
Modal,
Form,
FormItem,
Input,
DatePicker,
Select,
Upload,
Button,
Progress,
message
} from 'ant-design-vue';
import {
UploadOutlined,
FileTextOutlined,
CheckCircleFilled
} from '@ant-design/icons-vue';
import { ContractualCaseFilesAdd } from '@/api/contractReview/ContractualCaseFiles';
import { uploadApi } from '@/api/upload';
import { ossRemove } from '@/api/system/oss';
import { getDictOptions } from '@/utils/dict';
import { UploadFileParams } from '#/axios';
import dayjs from 'dayjs';
const { TextArea } = Input;
interface Props {
visible: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits(['update:visible', 'success']);
//
const loading = ref(false);
const fileList = ref<any[]>([]);
const uploading = ref(false);
const uploadPercent = ref(0);
const currentOssId = ref<string | null>(null);
const formRef = ref();
//
const formData = reactive({
caseName: '',
caseDescription: '',
caseType: '',
publishDate: dayjs(),
isEffective: 'Y',
progressStatus: 'PENDING'
});
//
const hasUploadedFile = computed(() => {
return !!currentOssId.value;
});
const caseNameDisabled = computed(() => {
return !currentOssId.value;
});
const caseNamePlaceholder = computed(() => {
return currentOssId.value ? '可编辑案例名称' : '请先上传案例文件';
});
// -
const rules = computed(() => ({
caseFile: hasUploadedFile.value ? [] : [{ required: true, message: '请上传案例文件' }],
caseName: [{ required: true, message: '请输入案例名称' }],
caseDescription: [{ required: true, message: '请输入案例描述' }],
publishDate: [{ required: true, message: '请选择发布日期' }],
}));
//
const effectiveOptions = getDictOptions('sys_yes_no');
//
watch(() => props.visible, (newVal) => {
if (!newVal) {
resetForm();
}
});
//
function customUploadRequest(options: any) {
const { file, onSuccess, onError } = options;
uploading.value = true;
uploadPercent.value = 0;
const uploadParams: UploadFileParams = {
name: 'file',
file: file,
data: {},
};
fileList.value = [
{
uid: '1',
name: file.name,
status: 'uploading',
url: URL.createObjectURL(file),
},
];
uploadApi(
uploadParams,
(progressEvent) => {
if (progressEvent.total) {
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
uploadPercent.value = percent;
}
}
).then((res) => {
if (fileList.value && fileList.value.length > 0) {
fileList.value[0].status = 'done';
fileList.value[0].response = res;
if (res && res.ossId) {
currentOssId.value = res.ossId;
const fileName = file.name;
const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')) || fileName;
formData.caseName = nameWithoutExt;
//
formRef.value?.clearValidate('caseFile');
}
}
uploading.value = false;
uploadPercent.value = 100;
message.success(`${file.name} 文件上传成功`);
onSuccess(res);
}).catch((err) => {
uploading.value = false;
fileList.value = [];
currentOssId.value = null;
message.error(`文件上传失败: ${err.message || '未知错误'}`);
onError(err);
});
}
function beforeUpload(file: File) {
const isDoc = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
file.type === 'application/msword';
if (!isDoc) {
message.error('只能上传 DOC/DOCX 格式的文件!');
return false;
}
const isLt500M = file.size / 1024 / 1024 < 500;
if (!isLt500M) {
message.error('文件大小不能超过 500MB!');
return false;
}
return true;
}
function removeFile() {
if (uploading.value) {
message.warning('文件正在上传中,请稍后再试');
return;
}
if (currentOssId.value) {
ossRemove([currentOssId.value])
.then(() => {
fileList.value = [];
uploadPercent.value = 0;
currentOssId.value = null;
formData.caseName = '';
message.success('文件已删除');
})
.catch((err) => {
message.error(`文件删除失败: ${err.message || '未知错误'}`);
});
} else {
fileList.value = [];
uploadPercent.value = 0;
formData.caseName = '';
message.success('文件已删除');
}
}
//
function filterEmptyParams(params: any) {
const filteredParams: any = {};
Object.keys(params).forEach(key => {
const value = params[key];
if (value !== null && value !== undefined && value !== '') {
filteredParams[key] = value;
}
});
return filteredParams;
}
//
async function handleSubmit() {
try {
await formRef.value?.validate();
} catch (error) {
return;
}
if (!currentOssId.value) {
message.warning('请上传案例文件');
return;
}
loading.value = true;
try {
const baseData = {
...formData,
publishDate: formData.publishDate ? formData.publishDate.format('YYYY-MM-DD') : '',
ossId: currentOssId.value
};
const submitData = filterEmptyParams(baseData);
await ContractualCaseFilesAdd(submitData);
message.success('新增成功');
// TODO: - API
// await parseCaseFile(currentOssId.value);
emit('update:visible', false);
emit('success');
resetForm();
} catch (error) {
message.error('新增失败');
} finally {
loading.value = false;
}
}
//
function handleCancel() {
emit('update:visible', false);
resetForm();
}
//
function resetForm() {
Object.assign(formData, {
caseName: '',
caseDescription: '',
caseType: '',
publishDate: dayjs(),
isEffective: 'Y',
progressStatus: 'PENDING'
});
fileList.value = [];
uploadPercent.value = 0;
currentOssId.value = null;
formRef.value?.clearValidate();
}
</script>
<style lang="less" scoped>
/* 文件上传样式 */
.upload-box {
border: 1px dashed #ddd;
border-radius: 8px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: border-color 0.3s;
width: 100%;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f9f9f9;
&:hover {
border-color: #13c2c2;
background-color: rgba(19, 194, 194, 0.02);
}
&.file-preview {
border: 2px solid #e6fffb;
background-color: #f0f8ff;
cursor: default;
}
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
}
.upload-icon {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 16px;
background-color: #e6fffb;
width: 50px;
height: 50px;
border-radius: 50%;
}
.upload-arrow-icon {
font-size: 24px;
color: #13c2c2;
}
.upload-text-container {
text-align: center;
}
.upload-text {
font-size: 16px;
color: #333;
margin-bottom: 8px;
}
.upload-link {
color: #13c2c2;
cursor: pointer;
text-decoration: none;
font-weight: 600;
}
.upload-link:hover {
text-decoration: underline;
}
.upload-hint {
color: #888;
font-size: 14px;
margin-top: 8px;
}
.file-info {
display: flex;
align-items: center;
width: 100%;
max-width: 800px;
}
.file-icon {
font-size: 32px;
color: #13c2c2;
margin-right: 16px;
}
.file-details {
flex: 1;
}
.file-name {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px 0;
color: #333;
}
.file-progress {
margin: 0 0 8px 0;
width: 100%;
}
.file-status {
font-size: 14px;
color: #666;
margin: 0;
display: flex;
align-items: center;
}
.status-icon {
margin-right: 6px;
}
.status-icon.success {
color: #52c41a;
}
.remove-btn {
padding: 0;
margin-left: 12px;
font-size: 14px;
}
</style>

460
src/views/contractReview/ContractualCaseFiles/EditModal.vue

@ -0,0 +1,460 @@
<template>
<Drawer
v-model:open="visible"
title="修改合同案例"
width="80%"
:destroyOnClose="true"
class="edit-case-drawer"
placement="right"
>
<template #extra>
<Space>
<Button @click="handleCancel">取消</Button>
<Button type="primary" @click="handleSubmit" :loading="loading">保存</Button>
</Space>
</template>
<div class="edit-container">
<!-- 左侧PDF预览 -->
<div class="pdf-preview-section">
<div class="section-title">
<FileTextOutlined />
<span>案例文档预览</span>
<Button type="link" size="small" @click="refreshPdf" :loading="pdfLoading">
<ReloadOutlined />
</Button>
</div>
<div class="pdf-container">
<div v-if="pdfLoading" class="loading-container">
<Spin size="large" tip="正在加载文档..." />
</div>
<div v-else-if="pdfError" class="error-container">
<Result
status="error"
:title="pdfError"
sub-title="请检查网络连接或联系管理员"
>
<template #extra>
<Button type="primary" @click="refreshPdf">重新加载</Button>
</template>
</Result>
</div>
<div v-else-if="pdfUrl" class="pdf-embed-container">
<embed
:src="pdfUrl"
type="application/pdf"
width="100%"
height="100%"
style="border: none; display: block;"
/>
</div>
<div v-else class="no-pdf-container">
<Empty description="暂无PDF文档" />
</div>
</div>
</div>
<!-- 右侧修改表单 -->
<div class="edit-form-section">
<div class="section-title">
<EditOutlined />
<span>修改信息</span>
</div>
<div class="form-container">
<Form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
class="edit-form"
>
<!-- 案例名称 -->
<FormItem label="案例名称" name="caseName" required>
<Input
v-model:value="formData.caseName"
placeholder="请输入案例名称"
size="large"
/>
</FormItem>
<!-- 案例描述 -->
<FormItem label="案例描述" name="caseDescription">
<TextArea
v-model:value="formData.caseDescription"
placeholder="请输入案例描述"
:rows="6"
show-count
:maxlength="500"
size="large"
/>
</FormItem>
<!-- 案例类型 -->
<FormItem label="案例类型" name="caseType">
<Input
v-model:value="formData.caseType"
placeholder="请输入案例类型"
size="large"
/>
</FormItem>
<!-- 发布日期 -->
<FormItem label="发布日期" name="publishDate">
<DatePicker
v-model:value="formData.publishDate"
format="YYYY-MM-DD"
valueFormat="YYYY-MM-DD"
style="width: 100%"
size="large"
/>
</FormItem>
<!-- 是否有效 -->
<FormItem label="是否有效" name="isEffective">
<Select
v-model:value="formData.isEffective"
:options="effectiveOptions"
size="large"
/>
</FormItem>
</Form>
</div>
</div>
</div>
</Drawer>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onUnmounted } from 'vue';
import {
Drawer,
Form,
FormItem,
Input,
DatePicker,
Select,
Button,
Space,
Spin,
Result,
Empty,
message
} from 'ant-design-vue';
import {
FileTextOutlined,
EditOutlined,
ReloadOutlined
} from '@ant-design/icons-vue';
import {
ContractualCaseFilesInfo,
ContractualCaseFilesUpdate,
ContractualCaseFilesViewPdf
} from '@/api/contractReview/ContractualCaseFiles';
import { getDictOptions } from '@/utils/dict';
import dayjs from 'dayjs';
const { TextArea } = Input;
interface Props {
visible: boolean;
caseFileId?: number | string;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
caseFileId: undefined
});
const emit = defineEmits<{
'update:visible': [value: boolean];
'success': [];
}>();
//
const loading = ref(false);
const pdfLoading = ref(false);
const pdfError = ref('');
const pdfUrl = ref('');
const formRef = ref();
//
const formData = reactive({
id: undefined as string | number | undefined,
caseName: '',
caseDescription: '',
caseType: '',
publishDate: '',
isEffective: 'Y'
});
//
const visible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
});
//
const rules = computed(() => ({
caseName: [{ required: true, message: '请输入案例名称', trigger: 'blur' }],
}));
//
const effectiveOptions = getDictOptions('sys_yes_no');
//
watch(() => props.visible, (newVisible) => {
if (newVisible && props.caseFileId) {
loadCaseData();
loadPdf();
} else if (!newVisible) {
resetForm();
cleanupPdfUrl();
}
});
//
async function loadCaseData() {
if (!props.caseFileId) return;
try {
const result = await ContractualCaseFilesInfo(props.caseFileId);
if (result) {
formData.id = result.id;
formData.caseName = result.caseName || '';
formData.caseDescription = result.caseDescription || '';
formData.caseType = result.caseType || '';
formData.publishDate = result.publishDate || '';
formData.isEffective = result.isEffective || 'Y';
}
} catch (error: any) {
message.error('加载案例信息失败: ' + (error.message || '未知错误'));
}
}
// PDF
async function loadPdf() {
if (!props.caseFileId) return;
pdfLoading.value = true;
pdfError.value = '';
try {
const pdfResponse = await ContractualCaseFilesViewPdf(props.caseFileId);
if (pdfResponse?.data) {
// URL
cleanupPdfUrl();
// Blob URL
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' });
pdfUrl.value = URL.createObjectURL(blob);
} else {
pdfError.value = 'PDF数据格式错误';
}
} catch (err: any) {
pdfError.value = err.message || '加载PDF失败';
console.error('加载PDF失败:', err);
} finally {
pdfLoading.value = false;
}
}
// PDF
function refreshPdf() {
loadPdf();
}
//
async function handleSubmit() {
try {
await formRef.value?.validate();
} catch (error) {
return;
}
loading.value = true;
try {
const submitData = {
...formData,
publishDate: formData.publishDate ? dayjs(formData.publishDate).format('YYYY-MM-DD') : ''
};
await ContractualCaseFilesUpdate(submitData);
message.success('修改成功');
emit('update:visible', false);
emit('success');
} catch (error: any) {
message.error('修改失败: ' + (error.message || '未知错误'));
} finally {
loading.value = false;
}
}
//
function handleCancel() {
emit('update:visible', false);
}
//
function resetForm() {
Object.assign(formData, {
id: undefined,
caseName: '',
caseDescription: '',
caseType: '',
publishDate: '',
isEffective: 'Y'
});
formRef.value?.clearValidate();
pdfError.value = '';
}
// Blob URL
function cleanupPdfUrl() {
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(pdfUrl.value);
pdfUrl.value = '';
}
}
//
onUnmounted(() => {
cleanupPdfUrl();
});
</script>
<style scoped>
.edit-case-drawer {
:deep(.ant-drawer-content) {
height: 100vh;
}
:deep(.ant-drawer-body) {
padding: 0;
height: calc(100vh - 55px);
}
}
.edit-container {
display: flex;
height: 100%;
gap: 1px;
background: #f0f2f5;
}
.pdf-preview-section {
flex: 1;
background: #fff;
display: flex;
flex-direction: column;
}
.edit-form-section {
width: 400px;
background: #fff;
display: flex;
flex-direction: column;
border-left: 1px solid #e8e8e8;
}
.section-title {
padding: 16px 20px;
font-size: 16px;
font-weight: 500;
color: #262626;
border-bottom: 1px solid #e8e8e8;
display: flex;
align-items: center;
gap: 8px;
background: #fafafa;
span {
flex: 1;
}
}
.pdf-container {
flex: 1;
position: relative;
background: #f5f5f5;
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.error-container {
padding: 40px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.pdf-embed-container {
height: 100%;
padding: 16px;
embed {
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.no-pdf-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
}
.form-container {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.edit-form {
.ant-form-item {
margin-bottom: 24px;
}
.ant-form-item-label > label {
font-weight: 500;
color: #262626;
}
}
/* 响应式设计 */
@media (max-width: 1400px) {
.edit-form-section {
width: 380px;
}
}
@media (max-width: 1200px) {
.edit-container {
flex-direction: column;
}
.pdf-preview-section {
height: 50%;
}
.edit-form-section {
width: 100%;
height: 50%;
border-left: none;
border-top: 1px solid #e8e8e8;
}
}
</style>

382
src/views/contractReview/ContractualCaseFiles/ViewModal.vue

@ -0,0 +1,382 @@
<template>
<Drawer
v-model:open="visible"
:title="modalTitle"
width="70%"
:destroyOnClose="true"
class="case-view-drawer"
placement="right"
>
<template #extra>
<Space>
<Button type="primary" @click="downloadPdf" :loading="downloading">
<template #icon><DownloadOutlined /></template>
下载PDF
</Button>
<Button @click="refreshPdf">
<template #icon><ReloadOutlined /></template>
刷新
</Button>
</Space>
</template>
<div class="case-view-container">
<!-- PDF预览区域 -->
<div class="pdf-container">
<div v-if="loading" class="loading-container">
<Spin size="large" tip="正在加载案例文档..." />
</div>
<div v-else-if="error" class="error-container">
<Result
status="error"
:title="error"
sub-title="请检查网络连接或联系管理员"
>
<template #extra>
<Button type="primary" @click="refreshPdf">重新加载</Button>
</template>
</Result>
</div>
<div v-else class="pdf-embed-container">
<embed
:src="pdfUrl"
type="application/pdf"
width="100%"
height="100%"
style="border: none; display: block;"
/>
<!-- 备选方案如果embed不支持显示iframe -->
<iframe
v-if="showIframe"
:src="pdfUrl"
width="100%"
height="100%"
style="border: none; display: block;"
></iframe>
</div>
</div>
<!-- 条款列表备选展示方式 -->
<div v-if="showArticlesList" class="articles-list">
<Divider>案例条款详情</Divider>
<div v-for="(article, index) in articles" :key="article.id" class="article-item">
<div class="article-title">{{ index + 1 }}</div>
<div class="article-content">{{ article.articleContent }}</div>
</div>
</div>
</div>
</Drawer>
</template>
<script setup lang="ts">
import { ref, computed, watch, onUnmounted } from 'vue';
import {
Drawer,
Button,
Space,
Spin,
Result,
Divider,
message
} from 'ant-design-vue';
import {
DownloadOutlined,
ReloadOutlined
} from '@ant-design/icons-vue';
import {
ContractualCaseFilesViewPdf,
ContractualCaseFilesArticles
} from '@/api/contractReview/ContractualCaseFiles';
interface Props {
visible: boolean;
caseFileId?: number | string;
caseName?: string;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
caseFileId: undefined,
caseName: ''
});
const emit = defineEmits<{
'update:visible': [value: boolean];
}>();
//
const loading = ref(false);
const downloading = ref(false);
const error = ref('');
const articles = ref<any[]>([]);
const pdfUrl = ref('');
const showIframe = ref(false);
const showArticlesList = ref(false);
//
const visible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
});
const modalTitle = computed(() => {
return props.caseName ? `案例详情 - ${props.caseName}` : '案例详情';
});
//
watch(() => props.visible, (newVisible) => {
if (newVisible && props.caseFileId) {
loadCaseDetail();
} else if (!newVisible) {
//
cleanupPdfUrl();
error.value = '';
showIframe.value = false;
showArticlesList.value = false;
}
});
//
async function loadCaseDetail() {
if (!props.caseFileId) return;
loading.value = true;
error.value = '';
try {
// PDF
const [pdfResponse, articlesResult] = await Promise.all([
ContractualCaseFilesViewPdf(props.caseFileId),
ContractualCaseFilesArticles(props.caseFileId)
]);
console.log('PDF响应:', pdfResponse);
// PDF Blob
if (pdfResponse?.data) {
console.log('PDF加载成功, 创建Blob');
// URL
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(pdfUrl.value);
}
// Blob URL
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' });
pdfUrl.value = URL.createObjectURL(blob);
console.log('PDF URL创建成功:', pdfUrl.value);
} else {
throw new Error('PDF数据格式错误');
}
//
articles.value = articlesResult || [];
} catch (err: any) {
error.value = err.message || '加载案例详情失败';
console.error('加载案例详情失败:', err);
} finally {
loading.value = false;
}
}
// PDF
async function refreshPdf() {
if (!props.caseFileId) return;
loading.value = true;
error.value = '';
try {
const pdfResponse = await ContractualCaseFilesViewPdf(props.caseFileId);
if (pdfResponse?.data) {
// URL
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(pdfUrl.value);
}
// Blob URL
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' });
pdfUrl.value = URL.createObjectURL(blob);
} else {
throw new Error('PDF数据格式错误');
}
} catch (err: any) {
error.value = err.message || '刷新PDF失败';
console.error('刷新PDF失败:', err);
} finally {
loading.value = false;
}
}
// PDF
async function downloadPdf() {
if (!props.caseFileId) return;
downloading.value = true;
try {
const pdfResponse = await ContractualCaseFilesViewPdf(props.caseFileId);
if (pdfResponse?.data) {
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${props.caseName || '案例文档'}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// URL
URL.revokeObjectURL(url);
message.success('PDF下载已开始');
} else {
throw new Error('PDF数据格式错误');
}
} catch (err: any) {
message.error('下载失败: ' + (err.message || '未知错误'));
console.error('下载PDF失败:', err);
} finally {
downloading.value = false;
}
}
//
function toggleDisplayMode() {
showArticlesList.value = !showArticlesList.value;
}
// PDF
function handlePdfError() {
showIframe.value = true;
}
// Blob URL
function cleanupPdfUrl() {
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(pdfUrl.value);
pdfUrl.value = '';
}
}
//
onUnmounted(() => {
cleanupPdfUrl();
});
</script>
<style scoped>
.case-view-drawer {
:deep(.ant-drawer-content) {
height: 100vh;
}
:deep(.ant-drawer-body) {
padding: 0;
height: calc(100vh - 55px);
display: flex;
flex-direction: column;
}
.case-view-container {
height: 100%;
display: flex;
flex-direction: column;
padding: 16px;
}
.toolbar {
margin-bottom: 16px;
padding: 12px;
background: #fafafa;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.pdf-container {
flex: 1;
position: relative;
min-height: 0;
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.error-container {
padding: 40px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.pdf-embed-container {
border: 1px solid #d9d9d9;
border-radius: 6px;
overflow: hidden;
height: 100%;
embed, iframe {
display: block;
height: 100% !important;
}
}
}
.articles-list {
margin-top: 24px;
flex-shrink: 0;
max-height: 300px;
overflow-y: auto;
.article-item {
margin-bottom: 24px;
padding: 16px;
border: 1px solid #e8e8e8;
border-radius: 6px;
background: #fafafa;
.article-title {
font-weight: bold;
font-size: 16px;
color: #262626;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e8e8e8;
}
.article-content {
line-height: 1.8;
color: #595959;
text-align: justify;
white-space: pre-wrap;
}
}
}
}
/* 响应式设计 */
@media (max-width: 1200px) {
.case-view-drawer {
:deep(.ant-drawer) {
width: 80% !important;
}
}
}
@media (max-width: 768px) {
.case-view-drawer {
:deep(.ant-drawer) {
width: 90% !important;
}
}
}
</style>

458
src/views/contractReview/ContractualCaseFiles/index.vue

@ -0,0 +1,458 @@
<template>
<PageWrapper dense>
<!-- 搜索表单 -->
<Card class="search-card">
<Form
:model="searchForm"
layout="inline"
class="search-form"
>
<FormItem label="案例名称">
<Input
v-model:value="searchForm.caseName"
placeholder="请输入案例名称"
style="width: 200px"
allowClear
/>
</FormItem>
<FormItem label="案例类型">
<Input
v-model:value="searchForm.caseType"
placeholder="请输入案例类型"
style="width: 150px"
allowClear
/>
</FormItem>
<FormItem label="发布日期">
<RangePicker
v-model:value="searchForm.publishDateRange"
format="YYYY-MM-DD"
valueFormat="YYYY-MM-DD"
style="width: 240px"
allowClear
:placeholder="['开始日期', '结束日期']"
/>
</FormItem>
<FormItem label="是否有效">
<Select
v-model:value="searchForm.isEffective"
placeholder="请选择状态"
style="width: 120px"
allowClear
:options="effectiveOptions"
/>
</FormItem>
<FormItem>
<Space>
<Button type="primary" @click="handleSearch">查询</Button>
<Button @click="resetSearch">重置</Button>
</Space>
</FormItem>
</Form>
</Card>
<!-- 表格区域 -->
<Card class="table-card">
<!-- 表格标题和操作按钮 -->
<div class="table-header">
<div class="table-title">合同案例文件列表</div>
<div class="table-actions">
<Space>
<Button
type="primary"
danger
@click="multipleRemove"
:disabled="!selectedRowKeys.length"
v-auth="'productManagement:ContractualCaseFiles:remove'"
>
删除
</Button>
<Button type="primary" @click="handleAdd" v-auth="'productManagement:ContractualCaseFiles:add'">
新增
</Button>
<Button @click="handleRefresh">
<template #icon><ReloadOutlined /></template>
</Button>
</Space>
</div>
</div>
<!-- 数据表格 -->
<Table
:dataSource="dataSource"
:columns="tableColumns"
:loading="loading"
:pagination="pagination"
:rowSelection="rowSelection"
rowKey="id"
@change="handleTableChange"
size="middle"
/>
</Card>
<!-- 新增弹窗 -->
<AddModal
v-model:visible="modalVisible"
@success="loadData"
/>
<!-- 查看弹窗 -->
<ViewModal
v-model:visible="viewModalVisible"
:caseFileId="selectedRecord?.id"
:caseName="selectedRecord?.caseName"
/>
<!-- 编辑弹窗 -->
<EditModal
v-model:visible="editModalVisible"
:caseFileId="selectedRecord?.id"
:caseName="selectedRecord?.caseName"
@success="loadData"
/>
</PageWrapper>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, h } from 'vue';
import { PageWrapper } from '@/components/Page';
import {
Table,
Button,
Space,
Card,
Form,
FormItem,
Input,
DatePicker,
Select,
Switch,
Modal,
message
} from 'ant-design-vue';
import {
ReloadOutlined,
EyeOutlined,
DeleteOutlined,
EditOutlined
} from '@ant-design/icons-vue';
import {
ContractualCaseFilesList,
ContractualCaseFilesRemove,
ContractualCaseFilesUpdateStatus
} from '@/api/contractReview/ContractualCaseFiles';
import { getDictOptions } from '@/utils/dict';
import AddModal from './AddModal.vue';
import ViewModal from './ViewModal.vue';
import EditModal from './EditModal.vue';
const { RangePicker } = DatePicker;
import { useRender } from '@/hooks/component/useRender';
const { renderDict } = useRender();
defineOptions({ name: 'ContractualCaseFiles' });
//
const loading = ref(false);
const dataSource = ref<any[]>([]);
const selectedRowKeys = ref<any[]>([]);
const modalVisible = ref(false);
const viewModalVisible = ref(false);
const editModalVisible = ref(false);
const selectedRecord = ref<any>(null);
const searchForm = reactive({
caseName: '',
caseType: '',
publishDateRange: null as any,
isEffective: undefined as string | undefined
});
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total} 条数据`,
});
//
const rowSelection = {
selectedRowKeys: selectedRowKeys.value,
onChange: (keys: any[]) => {
selectedRowKeys.value = keys;
},
};
//
const effectiveOptions = getDictOptions('sys_yes_no');
//
async function handleSwitchChange(record: any) {
if (record._switching) return;
record._switching = true;
try {
const newStatus = record.isEffective === 'Y' ? 'N' : 'Y';
await ContractualCaseFilesUpdateStatus(record.id, newStatus);
record.isEffective = newStatus;
message.success('状态更新成功');
} catch (error) {
message.error('状态更新失败');
} finally {
record._switching = false;
}
}
//
const tableColumns = [
{
title: '案例名称',
dataIndex: 'caseName',
key: 'caseName',
width: 180,
},
{
title: '案例描述',
dataIndex: 'caseDescription',
key: 'caseDescription',
width: 200,
ellipsis: true,
},
{
title: '案例类型',
dataIndex: 'caseType',
key: 'caseType',
width: 120,
},
{
title: '发布日期',
dataIndex: 'publishDate',
key: 'publishDate',
width: 120,
},
{
title: '是否有效',
dataIndex: 'isEffective',
key: 'isEffective',
width: 120,
customRender: ({ record }: any) => {
return h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, [
h(Switch, {
checked: record.isEffective === 'Y',
loading: record._switching,
onChange: () => handleSwitchChange(record)
}),
h('span', {}, record.isEffective === 'Y' ? '有效' : '无效')
]);
},
},
{
title: '当前状态',
dataIndex: 'progressStatus',
key: 'progressStatus',
width: 120,
customRender: ({ value }) => renderDict(value, 'file_progress_status'),
},
{
title: '操作',
key: 'action',
fixed: 'right' as const,
width: 180,
customRender: ({ record }: any) => {
return h(Space, {}, [
h(Button, {
type: 'primary',
size: 'small',
icon: h(EyeOutlined),
onClick: () => handleView(record)
}, '查看'),
h(Button, {
type: 'default',
size: 'small',
icon: h(EditOutlined),
onClick: () => handleEdit(record)
}, '修改'),
h(Button, {
type: 'primary',
danger: true,
size: 'small',
icon: h(DeleteOutlined),
onClick: () => handleDelete(record)
}, '删除')
]);
},
}
];
//
function filterEmptyParams(params: any) {
const filteredParams: any = {};
Object.keys(params).forEach(key => {
const value = params[key];
if (value !== null && value !== undefined && value !== '') {
filteredParams[key] = value;
}
});
return filteredParams;
}
//
async function loadData() {
loading.value = true;
try {
const baseParams: any = {
caseName: searchForm.caseName,
caseType: searchForm.caseType,
pageNum: pagination.current,
pageSize: pagination.pageSize,
};
//
if (searchForm.publishDateRange && searchForm.publishDateRange.length === 2) {
baseParams.publishDateStart = searchForm.publishDateRange[0];
baseParams.publishDateEnd = searchForm.publishDateRange[1];
}
//
const params = filterEmptyParams(baseParams);
const result: any = await ContractualCaseFilesList(params);
dataSource.value = result.rows || [];
pagination.total = result.total || 0;
} catch (error) {
message.error('加载数据失败');
} finally {
loading.value = false;
}
}
//
function handleTableChange(pag: any) {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
loadData();
}
//
function handleSearch() {
pagination.current = 1;
loadData();
}
//
function resetSearch() {
Object.assign(searchForm, {
caseName: '',
caseType: '',
publishDateRange: null,
isEffective: undefined
});
pagination.current = 1;
loadData();
}
//
function handleRefresh() {
loadData();
}
//
function handleAdd() {
modalVisible.value = true;
}
//
function handleView(record: any) {
selectedRecord.value = record;
viewModalVisible.value = true;
}
//
function handleEdit(record: any) {
selectedRecord.value = record;
editModalVisible.value = true;
}
//
async function handleDelete(record: any) {
Modal.confirm({
title: '删除确认',
content: `是否删除案例《${record.caseName}》?`,
onOk: async () => {
try {
await ContractualCaseFilesRemove([record.id]);
message.success('删除成功');
loadData();
} catch (error) {
message.error('删除失败');
}
},
});
}
//
async function multipleRemove() {
if (!selectedRowKeys.value.length) {
message.warning('请选择要删除的记录');
return;
}
const selectedRecords = dataSource.value.filter((item: any) =>
selectedRowKeys.value.includes(item.id)
);
const caseNames = selectedRecords.map((item: any) => item.caseName).join('、');
Modal.confirm({
title: '删除确认',
content: `是否删除案例《${caseNames}》?`,
onOk: async () => {
try {
await ContractualCaseFilesRemove(selectedRowKeys.value);
message.success('删除成功');
selectedRowKeys.value = [];
loadData();
} catch (error) {
message.error('删除失败');
}
},
});
}
//
onMounted(() => {
loadData();
});
</script>
<style scoped>
.search-card {
margin-bottom: 16px;
}
.search-form {
margin-bottom: 0;
}
.table-card {
background: #fff;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.table-title {
font-size: 16px;
font-weight: 500;
}
:deep(.ant-table) {
.ant-table-tbody > tr > td {
padding: 12px 8px;
}
}
</style>

476
src/views/contractReview/ContractualRegulationNames/AddModal.vue

@ -0,0 +1,476 @@
<template>
<Modal
:open="visible"
title="新增合同法规名称"
@ok="handleSubmit"
@cancel="handleCancel"
:confirmLoading="loading"
width="600px"
>
<Form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<!-- 文件上传 -->
<FormItem label="法规文件" name="regulationFile" :required="!hasUploadedFile">
<div class="upload-box" :class="{'file-preview': fileList && fileList.length > 0}">
<template v-if="fileList && fileList.length > 0">
<!-- 文件预览 -->
<div class="file-info">
<FileTextOutlined class="file-icon" />
<div class="file-details">
<p class="file-name">{{ fileList[0].name }}</p>
<div class="file-progress" v-if="uploading">
<Progress :percent="uploadPercent" size="small" status="active" />
</div>
<p class="file-status" v-else>
<CheckCircleFilled class="status-icon success" /> 上传成功
<Button type="link" class="remove-btn" @click="removeFile">删除</Button>
</p>
</div>
</div>
</template>
<template v-else>
<!-- 上传区域 -->
<Upload
:fileList="fileList"
:customRequest="customUploadRequest"
:beforeUpload="beforeUpload"
:showUploadList="false"
:maxCount="1"
:multiple="false"
name="file"
accept=".doc,.docx"
:disabled="uploading"
>
<div class="upload-content">
<div class="upload-icon">
<UploadOutlined class="upload-arrow-icon" />
</div>
<div class="upload-text-container">
<p class="upload-text">
<a class="upload-link">选择法规文件</a>
</p>
<p class="upload-hint">支持DOCDOCX格式文件最大500MB</p>
</div>
</div>
</Upload>
</template>
</div>
</FormItem>
<!-- 法规名称 -->
<FormItem label="法规名称" name="regulationName" required>
<Input
v-model:value="formData.regulationName"
:disabled="regulationNameDisabled"
:placeholder="regulationNamePlaceholder"
/>
</FormItem>
<!-- 法规描述 -->
<FormItem label="法规描述" name="regulationDescription" required>
<TextArea
v-model:value="formData.regulationDescription"
placeholder="请输入法规描述"
:rows="4"
show-count
:maxlength="500"
/>
</FormItem>
<!-- 发布日期 -->
<FormItem label="发布日期" name="publishDate" required>
<DatePicker
v-model:value="formData.publishDate"
format="YYYY-MM-DD"
valueFormat="YYYY-MM-DD"
style="width: 100%"
/>
</FormItem>
<!-- 是否有效 -->
<FormItem label="是否有效" name="isEffective">
<Select
v-model:value="formData.isEffective"
:options="effectiveOptions"
/>
</FormItem>
</Form>
</Modal>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue';
import {
Modal,
Form,
FormItem,
Input,
DatePicker,
Select,
Upload,
Button,
Progress,
message
} from 'ant-design-vue';
import {
UploadOutlined,
FileTextOutlined,
CheckCircleFilled
} from '@ant-design/icons-vue';
import { ContractualRegulationNamesAdd } from '@/api/contractReview/ContractualRegulationNames';
import { uploadApi } from '@/api/upload';
import { ossRemove } from '@/api/system/oss';
import { getDictOptions } from '@/utils/dict';
import { UploadFileParams } from '#/axios';
import dayjs from 'dayjs';
const { TextArea } = Input;
interface Props {
visible: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits(['update:visible', 'success']);
//
const loading = ref(false);
const fileList = ref<any[]>([]);
const uploading = ref(false);
const uploadPercent = ref(0);
const currentOssId = ref<string | null>(null);
const formRef = ref();
//
const formData = reactive({
regulationName: '',
regulationDescription: '',
publishDate: dayjs(),
isEffective: 'Y',
progressStatus: 'PENDING'
});
//
const hasUploadedFile = computed(() => {
return !!currentOssId.value;
});
const regulationNameDisabled = computed(() => {
return !currentOssId.value;
});
const regulationNamePlaceholder = computed(() => {
return currentOssId.value ? '可编辑法规名称' : '请先上传法规文件';
});
// -
const rules = computed(() => ({
regulationFile: hasUploadedFile.value ? [] : [{ required: true, message: '请上传法规文件' }],
regulationName: [{ required: true, message: '请输入法规名称' }],
regulationDescription: [{ required: true, message: '请输入法规描述' }],
publishDate: [{ required: true, message: '请选择发布日期' }],
}));
//
const effectiveOptions = getDictOptions('sys_yes_no');
//
watch(() => props.visible, (newVal) => {
if (!newVal) {
resetForm();
}
});
//
function customUploadRequest(options: any) {
const { file, onSuccess, onError } = options;
uploading.value = true;
uploadPercent.value = 0;
const uploadParams: UploadFileParams = {
name: 'file',
file: file,
data: {},
};
fileList.value = [
{
uid: '1',
name: file.name,
status: 'uploading',
url: URL.createObjectURL(file),
},
];
uploadApi(
uploadParams,
(progressEvent) => {
if (progressEvent.total) {
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
uploadPercent.value = percent;
}
}
).then((res) => {
if (fileList.value && fileList.value.length > 0) {
fileList.value[0].status = 'done';
fileList.value[0].response = res;
if (res && res.ossId) {
currentOssId.value = res.ossId;
const fileName = file.name;
const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')) || fileName;
formData.regulationName = nameWithoutExt;
//
formRef.value?.clearValidate('regulationFile');
}
}
uploading.value = false;
uploadPercent.value = 100;
message.success(`${file.name} 文件上传成功`);
onSuccess(res);
}).catch((err) => {
uploading.value = false;
fileList.value = [];
currentOssId.value = null;
message.error(`文件上传失败: ${err.message || '未知错误'}`);
onError(err);
});
}
function beforeUpload(file: File) {
const isDoc = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
file.type === 'application/msword';
if (!isDoc) {
message.error('只能上传 DOC/DOCX 格式的文件!');
return false;
}
const isLt500M = file.size / 1024 / 1024 < 500;
if (!isLt500M) {
message.error('文件大小不能超过 500MB!');
return false;
}
return true;
}
function removeFile() {
if (uploading.value) {
message.warning('文件正在上传中,请稍后再试');
return;
}
if (currentOssId.value) {
ossRemove([currentOssId.value])
.then(() => {
fileList.value = [];
uploadPercent.value = 0;
currentOssId.value = null;
formData.regulationName = '';
message.success('文件已删除');
})
.catch((err) => {
message.error(`文件删除失败: ${err.message || '未知错误'}`);
});
} else {
fileList.value = [];
uploadPercent.value = 0;
formData.regulationName = '';
message.success('文件已删除');
}
}
//
async function handleSubmit() {
try {
await formRef.value?.validate();
} catch (error) {
return;
}
if (!currentOssId.value) {
message.warning('请上传法规文件');
return;
}
loading.value = true;
try {
const submitData = {
...formData,
publishDate: formData.publishDate ? formData.publishDate.format('YYYY-MM-DD') : '',
ossId: currentOssId.value
};
await ContractualRegulationNamesAdd(submitData);
message.success('新增成功');
// TODO: - API
// await parseRegulationFile(currentOssId.value);
emit('update:visible', false);
emit('success');
resetForm();
} catch (error) {
message.error('新增失败');
} finally {
loading.value = false;
}
}
//
function handleCancel() {
emit('update:visible', false);
resetForm();
}
//
function resetForm() {
Object.assign(formData, {
regulationName: '',
regulationDescription: '',
publishDate: dayjs(),
isEffective: 'Y',
progressStatus: 'PENDING'
});
fileList.value = [];
uploadPercent.value = 0;
currentOssId.value = null;
formRef.value?.clearValidate();
}
</script>
<style lang="less" scoped>
/* 文件上传样式 */
.upload-box {
border: 1px dashed #ddd;
border-radius: 8px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: border-color 0.3s;
width: 100%;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f9f9f9;
&:hover {
border-color: #13c2c2;
background-color: rgba(19, 194, 194, 0.02);
}
&.file-preview {
border: 2px solid #e6fffb;
background-color: #f0f8ff;
cursor: default;
}
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
}
.upload-icon {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 16px;
background-color: #e6fffb;
width: 50px;
height: 50px;
border-radius: 50%;
}
.upload-arrow-icon {
font-size: 24px;
color: #13c2c2;
}
.upload-text-container {
text-align: center;
}
.upload-text {
font-size: 16px;
color: #333;
margin-bottom: 8px;
}
.upload-link {
color: #13c2c2;
cursor: pointer;
text-decoration: none;
font-weight: 600;
}
.upload-link:hover {
text-decoration: underline;
}
.upload-hint {
color: #888;
font-size: 14px;
margin-top: 8px;
}
.file-info {
display: flex;
align-items: center;
width: 100%;
max-width: 800px;
}
.file-icon {
font-size: 32px;
color: #13c2c2;
margin-right: 16px;
}
.file-details {
flex: 1;
}
.file-name {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px 0;
color: #333;
}
.file-progress {
margin: 0 0 8px 0;
width: 100%;
}
.file-status {
font-size: 14px;
color: #666;
margin: 0;
display: flex;
align-items: center;
}
.status-icon {
margin-right: 6px;
}
.status-icon.success {
color: #52c41a;
}
.remove-btn {
padding: 0;
margin-left: 12px;
font-size: 14px;
}
</style>

108
src/views/contractReview/ContractualRegulationNames/ContractualRegulationNames.data.ts

@ -0,0 +1,108 @@
import { BasicColumn } from '@/components/Table';
import { FormSchema } from '@/components/Form';
import { getDictOptions } from '@/utils/dict';
import { h } from 'vue';
import { useRender } from '@/hooks/component/useRender';
const { renderDict } = useRender();
export const formSchemas: FormSchema[] = [
{
label: '法规名称',
field: 'regulationName',
component: 'Input',
},
{
label: '发布日期',
field: 'publishDate',
component: 'DatePicker',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
},
},
{
label: '是否有效',
field: 'isEffective',
component: 'Select',
componentProps: {
options: getDictOptions('sys_yes_no'),
},
},
];
export const columns: BasicColumn[] = [
{
title: '主键ID',
dataIndex: 'id',
ifShow: false,
},
{
title: '法规名称',
dataIndex: 'regulationName',
},
{
title: '发布日期',
dataIndex: 'publishDate',
},
{
title: '是否有效',
dataIndex: 'isEffective',
width: 120,
customRender: ({ record }) => {
return h('ASwitch', {
checked: record.isEffective === 'Y',
loading: record._switching,
onClick: () => handleSwitchChange(record)
});
},
},{
title: '当前状态',
dataIndex: 'progressStatus',
customRender: ({ value }) => renderDict(value, 'file_progress_status'),
}
];
export const modalSchemas: FormSchema[] = [
{
label: '主键ID',
field: 'id',
required: false,
component: 'Input',
show: false,
},
{
label: '法规名称',
field: 'regulationName',
required: true,
component: 'Input',
componentProps: {
disabled: true,
placeholder: '请先上传法规文件',
},
},
{
label: '发布日期',
field: 'publishDate',
required: true,
component: 'DatePicker',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
},
},
{
label: '是否有效',
field: 'isEffective',
required: false,
component: 'Select',
componentProps: {
options: getDictOptions('sys_yes_no'),
},
}
];
// 声明处理开关变化的函数(需要在使用的组件中实现)
declare function handleSwitchChange(record: any): void;

450
src/views/contractReview/ContractualRegulationNames/EditModal.vue

@ -0,0 +1,450 @@
<template>
<Drawer
v-model:open="visible"
title="修改合同法规"
width="80%"
:destroyOnClose="true"
class="edit-regulation-drawer"
placement="right"
>
<template #extra>
<Space>
<Button @click="handleCancel">取消</Button>
<Button type="primary" @click="handleSubmit" :loading="loading">保存</Button>
</Space>
</template>
<div class="edit-container">
<!-- 左侧PDF预览 -->
<div class="pdf-preview-section">
<div class="section-title">
<FileTextOutlined />
<span>法规文档预览</span>
<Button type="link" size="small" @click="refreshPdf" :loading="pdfLoading">
<ReloadOutlined />
</Button>
</div>
<div class="pdf-container">
<div v-if="pdfLoading" class="loading-container">
<Spin size="large" tip="正在加载文档..." />
</div>
<div v-else-if="pdfError" class="error-container">
<Result
status="error"
:title="pdfError"
sub-title="请检查网络连接或联系管理员"
>
<template #extra>
<Button type="primary" @click="refreshPdf">重新加载</Button>
</template>
</Result>
</div>
<div v-else-if="pdfUrl" class="pdf-embed-container">
<embed
:src="pdfUrl"
type="application/pdf"
width="100%"
height="100%"
style="border: none; display: block;"
/>
</div>
<div v-else class="no-pdf-container">
<Empty description="暂无PDF文档" />
</div>
</div>
</div>
<!-- 右侧修改表单 -->
<div class="edit-form-section">
<div class="section-title">
<EditOutlined />
<span>修改信息</span>
</div>
<div class="form-container">
<Form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
class="edit-form"
>
<!-- 法规名称 -->
<FormItem label="法规名称" name="regulationName" required>
<Input
v-model:value="formData.regulationName"
placeholder="请输入法规名称"
size="large"
/>
</FormItem>
<!-- 法规描述 -->
<FormItem label="法规描述" name="regulationDescription" required>
<TextArea
v-model:value="formData.regulationDescription"
placeholder="请输入法规描述"
:rows="6"
show-count
:maxlength="500"
size="large"
/>
</FormItem>
<!-- 发布日期 -->
<FormItem label="发布日期" name="publishDate" required>
<DatePicker
v-model:value="formData.publishDate"
format="YYYY-MM-DD"
valueFormat="YYYY-MM-DD"
style="width: 100%"
size="large"
/>
</FormItem>
<!-- 是否有效 -->
<FormItem label="是否有效" name="isEffective">
<Select
v-model:value="formData.isEffective"
:options="effectiveOptions"
size="large"
/>
</FormItem>
</Form>
</div>
</div>
</div>
</Drawer>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onUnmounted } from 'vue';
import {
Drawer,
Form,
FormItem,
Input,
DatePicker,
Select,
Button,
Space,
Spin,
Result,
Empty,
message
} from 'ant-design-vue';
import {
FileTextOutlined,
EditOutlined,
ReloadOutlined
} from '@ant-design/icons-vue';
import {
ContractualRegulationNamesInfo,
ContractualRegulationNamesUpdate,
ContractualRegulationNamesViewPdf
} from '@/api/contractReview/ContractualRegulationNames';
import { getDictOptions } from '@/utils/dict';
import dayjs from 'dayjs';
const { TextArea } = Input;
interface Props {
visible: boolean;
regulationId?: number | string;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
regulationId: undefined
});
const emit = defineEmits<{
'update:visible': [value: boolean];
'success': [];
}>();
//
const loading = ref(false);
const pdfLoading = ref(false);
const pdfError = ref('');
const pdfUrl = ref('');
const formRef = ref();
//
const formData = reactive({
id: undefined as string | number | undefined,
regulationName: '',
regulationDescription: '',
publishDate: '',
isEffective: 'Y'
});
//
const visible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
});
//
const rules = computed(() => ({
regulationName: [{ required: true, message: '请输入法规名称', trigger: 'blur' }],
regulationDescription: [{ required: true, message: '请输入法规描述', trigger: 'blur' }],
publishDate: [{ required: true, message: '请选择发布日期', trigger: 'change' }],
}));
//
const effectiveOptions = getDictOptions('sys_yes_no');
//
watch(() => props.visible, (newVisible) => {
if (newVisible && props.regulationId) {
loadRegulationData();
loadPdf();
} else if (!newVisible) {
resetForm();
cleanupPdfUrl();
}
});
//
async function loadRegulationData() {
if (!props.regulationId) return;
try {
const result = await ContractualRegulationNamesInfo(props.regulationId);
if (result) {
formData.id = result.id;
formData.regulationName = result.regulationName || '';
formData.regulationDescription = result.regulationDescription || '';
formData.publishDate = result.publishDate || '';
formData.isEffective = result.isEffective || 'Y';
}
} catch (error: any) {
message.error('加载法规信息失败: ' + (error.message || '未知错误'));
}
}
// PDF
async function loadPdf() {
if (!props.regulationId) return;
pdfLoading.value = true;
pdfError.value = '';
try {
const pdfResponse = await ContractualRegulationNamesViewPdf(props.regulationId);
if (pdfResponse?.data) {
// URL
cleanupPdfUrl();
// Blob URL
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' });
pdfUrl.value = URL.createObjectURL(blob);
} else {
pdfError.value = 'PDF数据格式错误';
}
} catch (err: any) {
pdfError.value = err.message || '加载PDF失败';
console.error('加载PDF失败:', err);
} finally {
pdfLoading.value = false;
}
}
// PDF
function refreshPdf() {
loadPdf();
}
//
async function handleSubmit() {
try {
await formRef.value?.validate();
} catch (error) {
return;
}
loading.value = true;
try {
const submitData = {
...formData,
publishDate: formData.publishDate ? dayjs(formData.publishDate).format('YYYY-MM-DD') : ''
};
await ContractualRegulationNamesUpdate(submitData);
message.success('修改成功');
emit('update:visible', false);
emit('success');
} catch (error: any) {
message.error('修改失败: ' + (error.message || '未知错误'));
} finally {
loading.value = false;
}
}
//
function handleCancel() {
emit('update:visible', false);
}
//
function resetForm() {
Object.assign(formData, {
id: undefined,
regulationName: '',
regulationDescription: '',
publishDate: '',
isEffective: 'Y'
});
formRef.value?.clearValidate();
pdfError.value = '';
}
// Blob URL
function cleanupPdfUrl() {
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(pdfUrl.value);
pdfUrl.value = '';
}
}
//
onUnmounted(() => {
cleanupPdfUrl();
});
</script>
<style scoped>
.edit-regulation-drawer {
:deep(.ant-drawer-content) {
height: 100vh;
}
:deep(.ant-drawer-body) {
padding: 0;
height: calc(100vh - 55px);
}
}
.edit-container {
display: flex;
height: 100%;
gap: 1px;
background: #f0f2f5;
}
.pdf-preview-section {
flex: 1;
background: #fff;
display: flex;
flex-direction: column;
}
.edit-form-section {
width: 400px;
background: #fff;
display: flex;
flex-direction: column;
border-left: 1px solid #e8e8e8;
}
.section-title {
padding: 16px 20px;
font-size: 16px;
font-weight: 500;
color: #262626;
border-bottom: 1px solid #e8e8e8;
display: flex;
align-items: center;
gap: 8px;
background: #fafafa;
span {
flex: 1;
}
}
.pdf-container {
flex: 1;
position: relative;
background: #f5f5f5;
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.error-container {
padding: 40px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.pdf-embed-container {
height: 100%;
padding: 16px;
embed {
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.no-pdf-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
}
.form-container {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.edit-form {
.ant-form-item {
margin-bottom: 24px;
}
.ant-form-item-label > label {
font-weight: 500;
color: #262626;
}
}
/* 响应式设计 */
@media (max-width: 1400px) {
.edit-form-section {
width: 380px;
}
}
@media (max-width: 1200px) {
.edit-container {
flex-direction: column;
}
.pdf-preview-section {
height: 50%;
}
.edit-form-section {
width: 100%;
height: 50%;
border-left: none;
border-top: 1px solid #e8e8e8;
}
}
</style>

382
src/views/contractReview/ContractualRegulationNames/ViewModal.vue

@ -0,0 +1,382 @@
<template>
<Drawer
v-model:open="visible"
:title="modalTitle"
width="70%"
:destroyOnClose="true"
class="regulation-view-drawer"
placement="right"
>
<template #extra>
<Space>
<Button type="primary" @click="downloadPdf" :loading="downloading">
<template #icon><DownloadOutlined /></template>
下载PDF
</Button>
<Button @click="refreshPdf">
<template #icon><ReloadOutlined /></template>
刷新
</Button>
</Space>
</template>
<div class="regulation-view-container">
<!-- PDF预览区域 -->
<div class="pdf-container">
<div v-if="loading" class="loading-container">
<Spin size="large" tip="正在加载法规文档..." />
</div>
<div v-else-if="error" class="error-container">
<Result
status="error"
:title="error"
sub-title="请检查网络连接或联系管理员"
>
<template #extra>
<Button type="primary" @click="refreshPdf">重新加载</Button>
</template>
</Result>
</div>
<div v-else class="pdf-embed-container">
<embed
:src="pdfUrl"
type="application/pdf"
width="100%"
height="100%"
style="border: none; display: block;"
/>
<!-- 备选方案如果embed不支持显示iframe -->
<iframe
v-if="showIframe"
:src="pdfUrl"
width="100%"
height="100%"
style="border: none; display: block;"
></iframe>
</div>
</div>
<!-- 条款列表备选展示方式 -->
<div v-if="showArticlesList" class="articles-list">
<Divider>法规条款详情</Divider>
<div v-for="(article, index) in articles" :key="article.id" class="article-item">
<div class="article-title">{{ index + 1 }}</div>
<div class="article-content">{{ article.articleContent }}</div>
</div>
</div>
</div>
</Drawer>
</template>
<script setup lang="ts">
import { ref, computed, watch, onUnmounted } from 'vue';
import {
Drawer,
Button,
Space,
Spin,
Result,
Divider,
message
} from 'ant-design-vue';
import {
DownloadOutlined,
ReloadOutlined
} from '@ant-design/icons-vue';
import {
ContractualRegulationNamesViewPdf,
ContractualRegulationNamesArticles
} from '@/api/contractReview/ContractualRegulationNames';
interface Props {
visible: boolean;
regulationId?: number | string;
regulationName?: string;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
regulationId: undefined,
regulationName: ''
});
const emit = defineEmits<{
'update:visible': [value: boolean];
}>();
//
const loading = ref(false);
const downloading = ref(false);
const error = ref('');
const articles = ref<any[]>([]);
const pdfUrl = ref('');
const showIframe = ref(false);
const showArticlesList = ref(false);
//
const visible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
});
const modalTitle = computed(() => {
return props.regulationName ? `法规详情 - ${props.regulationName}` : '法规详情';
});
//
watch(() => props.visible, (newVisible) => {
if (newVisible && props.regulationId) {
loadRegulationDetail();
} else if (!newVisible) {
//
cleanupPdfUrl();
error.value = '';
showIframe.value = false;
showArticlesList.value = false;
}
});
//
async function loadRegulationDetail() {
if (!props.regulationId) return;
loading.value = true;
error.value = '';
try {
// PDF
const [pdfResponse, articlesResult] = await Promise.all([
ContractualRegulationNamesViewPdf(props.regulationId),
ContractualRegulationNamesArticles(props.regulationId)
]);
console.log('PDF响应:', pdfResponse);
// PDF Blob
if (pdfResponse?.data) {
console.log('PDF加载成功, 创建Blob');
// URL
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(pdfUrl.value);
}
// Blob URL
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' });
pdfUrl.value = URL.createObjectURL(blob);
console.log('PDF URL创建成功:', pdfUrl.value);
} else {
throw new Error('PDF数据格式错误');
}
//
articles.value = articlesResult || [];
} catch (err: any) {
error.value = err.message || '加载法规详情失败';
console.error('加载法规详情失败:', err);
} finally {
loading.value = false;
}
}
// PDF
async function refreshPdf() {
if (!props.regulationId) return;
loading.value = true;
error.value = '';
try {
const pdfResponse = await ContractualRegulationNamesViewPdf(props.regulationId);
if (pdfResponse?.data) {
// URL
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(pdfUrl.value);
}
// Blob URL
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' });
pdfUrl.value = URL.createObjectURL(blob);
} else {
throw new Error('PDF数据格式错误');
}
} catch (err: any) {
error.value = err.message || '刷新PDF失败';
console.error('刷新PDF失败:', err);
} finally {
loading.value = false;
}
}
// PDF
async function downloadPdf() {
if (!props.regulationId) return;
downloading.value = true;
try {
const pdfResponse = await ContractualRegulationNamesViewPdf(props.regulationId);
if (pdfResponse?.data) {
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${props.regulationName || '法规文档'}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// URL
URL.revokeObjectURL(url);
message.success('PDF下载已开始');
} else {
throw new Error('PDF数据格式错误');
}
} catch (err: any) {
message.error('下载失败: ' + (err.message || '未知错误'));
console.error('下载PDF失败:', err);
} finally {
downloading.value = false;
}
}
//
function toggleDisplayMode() {
showArticlesList.value = !showArticlesList.value;
}
// PDF
function handlePdfError() {
showIframe.value = true;
}
// Blob URL
function cleanupPdfUrl() {
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(pdfUrl.value);
pdfUrl.value = '';
}
}
//
onUnmounted(() => {
cleanupPdfUrl();
});
</script>
<style scoped>
.regulation-view-drawer {
:deep(.ant-drawer-content) {
height: 100vh;
}
:deep(.ant-drawer-body) {
padding: 0;
height: calc(100vh - 55px);
display: flex;
flex-direction: column;
}
.regulation-view-container {
height: 100%;
display: flex;
flex-direction: column;
padding: 16px;
}
.toolbar {
margin-bottom: 16px;
padding: 12px;
background: #fafafa;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.pdf-container {
flex: 1;
position: relative;
min-height: 0;
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.error-container {
padding: 40px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.pdf-embed-container {
border: 1px solid #d9d9d9;
border-radius: 6px;
overflow: hidden;
height: 100%;
embed, iframe {
display: block;
height: 100% !important;
}
}
}
.articles-list {
margin-top: 24px;
flex-shrink: 0;
max-height: 300px;
overflow-y: auto;
.article-item {
margin-bottom: 24px;
padding: 16px;
border: 1px solid #e8e8e8;
border-radius: 6px;
background: #fafafa;
.article-title {
font-weight: bold;
font-size: 16px;
color: #262626;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e8e8e8;
}
.article-content {
line-height: 1.8;
color: #595959;
text-align: justify;
white-space: pre-wrap;
}
}
}
}
/* 响应式设计 */
@media (max-width: 1200px) {
.regulation-view-drawer {
:deep(.ant-drawer) {
width: 80% !important;
}
}
}
@media (max-width: 768px) {
.regulation-view-drawer {
:deep(.ant-drawer) {
width: 90% !important;
}
}
}
</style>

442
src/views/contractReview/ContractualRegulationNames/index.vue

@ -0,0 +1,442 @@
<template>
<PageWrapper dense>
<!-- 搜索表单 -->
<Card class="search-card">
<Form
:model="searchForm"
layout="inline"
class="search-form"
>
<FormItem label="法规名称">
<Input
v-model:value="searchForm.regulationName"
placeholder="请输入法规名称"
style="width: 200px"
allowClear
/>
</FormItem>
<FormItem label="发布日期">
<RangePicker
v-model:value="searchForm.publishDateRange"
format="YYYY-MM-DD"
valueFormat="YYYY-MM-DD"
style="width: 240px"
allowClear
:placeholder="['开始日期', '结束日期']"
/>
</FormItem>
<FormItem label="是否有效">
<Select
v-model:value="searchForm.isEffective"
placeholder="请选择状态"
style="width: 120px"
allowClear
:options="effectiveOptions"
/>
</FormItem>
<FormItem>
<Space>
<Button type="primary" @click="handleSearch">查询</Button>
<Button @click="resetSearch">重置</Button>
</Space>
</FormItem>
</Form>
</Card>
<!-- 表格区域 -->
<Card class="table-card">
<!-- 表格标题和操作按钮 -->
<div class="table-header">
<div class="table-title">合同法规文件列表</div>
<div class="table-actions">
<Space>
<Button
type="primary"
danger
@click="multipleRemove"
:disabled="!selectedRowKeys.length"
v-auth="'productManagement:ContractualRegulationNames:remove'"
>
删除
</Button>
<Button type="primary" @click="handleAdd" v-auth="'productManagement:ContractualRegulationNames:add'">
新增
</Button>
<Button @click="handleRefresh">
<template #icon><ReloadOutlined /></template>
</Button>
</Space>
</div>
</div>
<!-- 数据表格 -->
<Table
:dataSource="dataSource"
:columns="tableColumns"
:loading="loading"
:pagination="pagination"
:rowSelection="rowSelection"
rowKey="id"
@change="handleTableChange"
size="middle"
/>
</Card>
<!-- 新增弹窗 -->
<AddModal
v-model:visible="modalVisible"
@success="loadData"
/>
<!-- 查看弹窗 -->
<ViewModal
v-model:visible="viewModalVisible"
:regulationId="selectedRecord?.id"
:regulationName="selectedRecord?.regulationName"
/>
<!-- 修改弹窗 -->
<EditModal
v-model:visible="editModalVisible"
:regulationId="selectedRecord?.id"
@success="loadData"
/>
</PageWrapper>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, h } from 'vue';
import { PageWrapper } from '@/components/Page';
import {
Table,
Button,
Space,
Card,
Form,
FormItem,
Input,
DatePicker,
Select,
Switch,
Modal,
message
} from 'ant-design-vue';
import {
ReloadOutlined,
EyeOutlined,
DeleteOutlined,
EditOutlined
} from '@ant-design/icons-vue';
import {
ContractualRegulationNamesList,
ContractualRegulationNamesRemove,
ContractualRegulationNamesUpdateStatus
} from '@/api/contractReview/ContractualRegulationNames';
import { getDictOptions } from '@/utils/dict';
import AddModal from './AddModal.vue';
import ViewModal from './ViewModal.vue';
import EditModal from './EditModal.vue';
const { RangePicker } = DatePicker;
import { useRender } from '@/hooks/component/useRender';
const { renderDict } = useRender();
defineOptions({ name: 'ContractualRegulationNames' });
//
const loading = ref(false);
const dataSource = ref<any[]>([]);
const selectedRowKeys = ref<any[]>([]);
const modalVisible = ref(false);
const viewModalVisible = ref(false);
const editModalVisible = ref(false);
const selectedRecord = ref<any>(null);
const searchForm = reactive({
regulationName: '',
publishDateRange: null as any,
isEffective: undefined as string | undefined
});
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total} 条数据`,
});
//
const rowSelection = {
selectedRowKeys: selectedRowKeys.value,
onChange: (keys: any[]) => {
selectedRowKeys.value = keys;
},
};
//
const effectiveOptions = getDictOptions('sys_yes_no');
//
async function handleSwitchChange(record: any) {
if (record._switching) return;
record._switching = true;
try {
const newStatus = record.isEffective === 'Y' ? 'N' : 'Y';
await ContractualRegulationNamesUpdateStatus(record.id, newStatus);
record.isEffective = newStatus;
message.success('状态更新成功');
} catch (error) {
message.error('状态更新失败');
} finally {
record._switching = false;
}
}
// -
const tableColumns = [
{
title: '法规名称',
dataIndex: 'regulationName',
key: 'regulationName',
width: 180,
},
{
title: '法规描述',
dataIndex: 'regulationDescription',
key: 'regulationDescription',
width: 200,
ellipsis: true,
},
{
title: '发布日期',
dataIndex: 'publishDate',
key: 'publishDate',
width: 150,
},
{
title: '是否有效',
dataIndex: 'isEffective',
key: 'isEffective',
width: 120,
customRender: ({ record }: any) => {
return h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, [
h(Switch, {
checked: record.isEffective === 'Y',
loading: record._switching,
onChange: () => handleSwitchChange(record)
}),
h('span', {}, record.isEffective === 'Y' ? '有效' : '无效')
]);
},
},
{
title: '当前状态',
dataIndex: 'progressStatus',
key: 'progressStatus',
width: 120,
customRender: ({ value }) => renderDict(value, 'file_progress_status'),
},
{
title: '操作',
key: 'action',
fixed: 'right' as const,
width: 180,
customRender: ({ record }: any) => {
return h(Space, {}, [
h(Button, {
type: 'primary',
size: 'small',
icon: h(EyeOutlined),
onClick: () => handleView(record)
}, '查看'),
h(Button, {
type: 'default',
size: 'small',
icon: h(EditOutlined),
onClick: () => handleEdit(record)
}, '修改'),
h(Button, {
type: 'primary',
danger: true,
size: 'small',
icon: h(DeleteOutlined),
onClick: () => handleDelete(record)
}, '删除')
]);
},
}
];
//
function filterEmptyParams(params: any) {
const filteredParams: any = {};
Object.keys(params).forEach(key => {
const value = params[key];
if (value !== null && value !== undefined && value !== '') {
filteredParams[key] = value;
}
});
return filteredParams;
}
//
async function loadData() {
loading.value = true;
try {
const baseParams: any = {
regulationName: searchForm.regulationName,
isEffective: searchForm.isEffective,
pageNum: pagination.current,
pageSize: pagination.pageSize,
};
//
if (searchForm.publishDateRange && searchForm.publishDateRange.length === 2) {
baseParams.publishDateStart = searchForm.publishDateRange[0];
baseParams.publishDateEnd = searchForm.publishDateRange[1];
}
//
const params = filterEmptyParams(baseParams);
const result: any = await ContractualRegulationNamesList(params);
dataSource.value = result.rows || [];
pagination.total = result.total || 0;
} catch (error) {
message.error('加载数据失败');
} finally {
loading.value = false;
}
}
//
function handleTableChange(pag: any) {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
loadData();
}
//
function handleSearch() {
pagination.current = 1;
loadData();
}
//
function resetSearch() {
Object.assign(searchForm, {
regulationName: '',
publishDateRange: null,
isEffective: undefined
});
pagination.current = 1;
loadData();
}
//
function handleRefresh() {
loadData();
}
//
function handleAdd() {
modalVisible.value = true;
}
//
function handleView(record: any) {
selectedRecord.value = record;
viewModalVisible.value = true;
}
//
function handleEdit(record: any) {
selectedRecord.value = record;
editModalVisible.value = true;
}
//
async function handleDelete(record: any) {
Modal.confirm({
title: '删除确认',
content: `是否删除《${record.regulationName}》?`,
onOk: async () => {
try {
await ContractualRegulationNamesRemove([record.id]);
message.success('删除成功');
loadData();
} catch (error) {
message.error('删除失败');
}
},
});
}
//
async function multipleRemove() {
if (!selectedRowKeys.value.length) {
message.warning('请选择要删除的记录');
return;
}
const selectedRecords = dataSource.value.filter((item: any) =>
selectedRowKeys.value.includes(item.id)
);
const regulationNames = selectedRecords.map((item: any) => item.regulationName).join('、');
Modal.confirm({
title: '删除确认',
content: `是否删除《${regulationNames}》?`,
onOk: async () => {
try {
await ContractualRegulationNamesRemove(selectedRowKeys.value);
message.success('删除成功');
selectedRowKeys.value = [];
loadData();
} catch (error) {
message.error('删除失败');
}
},
});
}
//
onMounted(() => {
loadData();
});
</script>
<style scoped>
.search-card {
margin-bottom: 16px;
}
.search-form {
margin-bottom: 0;
}
.table-card {
background: #fff;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.table-title {
font-size: 16px;
font-weight: 500;
}
:deep(.ant-table) {
.ant-table-tbody > tr > td {
padding: 12px 8px;
}
}
</style>

275
src/views/documentReview/DocumentTasks/ResultDetailDrawer copy.vue → src/views/contractReview/ContractualTasks/ContractualResultDetailDrawer.vue

@ -90,20 +90,27 @@
<CopyOutlined /> 复制
</Button>
</div>
<!-- 特殊处理审查依据字段 -->
<div v-if="section.field === 'reviewBasis' && section.type === 'reviewPointsList'" class="section-content markdown-content">
<div v-if="getParsedReviewBasis(item.reviewBasis)?.review_points?.length">
<!-- <ul class="review-points">
<li v-for="(point, pointIdx) in getParsedReviewBasis(item.reviewBasis).review_points" :key="pointIdx">
{{ point }}
</li>
</ul> -->
</div>
<div v-if="getParsedReviewBasis(item.reviewBasis)?.review_content">
{{ getParsedReviewBasis(item.reviewBasis).review_points[0] }}{{ getParsedReviewBasis(item.reviewBasis).review_content }}
<!-- <div v-html="renderMarkdown(getParsedReviewBasis(item.reviewBasis).review_content)" @click="locateByText(getParsedReviewBasis(item.reviewBasis).review_content)"></div> -->
</div>
</div>
<!-- 其他普通字段 -->
<div
v-else
class="section-content markdown-content"
v-html="renderContent(section, getItemValue(item, section.field))"
v-if="getItemValue(item, section.field)"
@click="locateByText(getItemValue(item, section.field))"
></div>
<div v-if="section.type === 'reviewPointsList' && section.field === 'reviewBasis' && item.reviewBasis && item.reviewBasis.reviewPoints && item.reviewBasis.reviewPoints.length">
<Divider style="margin: 8px 0" />
<ul class="review-points">
<li v-for="(point, pointIdx) in item.reviewBasis.reviewPoints" :key="pointIdx">
{{ point }}
</li>
</ul>
</div>
</div>
</template>
</div>
@ -148,16 +155,16 @@
import { ref, watch, onMounted, computed, onBeforeUnmount, nextTick, h, resolveComponent } from 'vue';
import { Drawer, Button, Card, Switch, Divider, Tabs, TabPane, Modal } from 'ant-design-vue';
import { DownOutlined, UpOutlined, CopyOutlined, LeftOutlined, RightOutlined, AimOutlined } from '@ant-design/icons-vue';
import { DocumentTaskResultDetailVO } from '@/api/documentReview/DocumentTaskResults/model';
import { updateResultItemStatus, getPdfStream } from '@/api/documentReview/DocumentTaskResults';
import { ContractualTaskResultDetailVO } from '@/api/contractReview/ContractualTaskResults/model';
import { updateResultItemStatus, getPdfStream } from '@/api/contractReview/ContractualTaskResults';
import { message } from 'ant-design-vue';
import MarkdownIt from 'markdown-it';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api';
import PageSelectModal from './PageSelectModal.vue';
import PageSelectModal from '@/views/documentReview/DocumentTasks/PageSelectModal.vue';
// PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = '../../../node_modules/pdfjs-dist/build/pdf.worker.min.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js';
const props = defineProps({
visible: {
@ -166,14 +173,14 @@
},
title: {
type: String,
default: '文档审核结果'
default: '合同审核结果'
},
width: {
type: [String, Number],
default: '80%'
default: '95%'
},
taskResultDetail: {
type: Array as PropType<DocumentTaskResultDetailVO[]>,
type: Array as PropType<ContractualTaskResultDetailVO[]>,
default: () => []
},
taskInfo: {
@ -213,6 +220,8 @@
let currentRenderTask: any = null; //
const highlightText = ref<string | null>(null);
//
interface ContentSectionConfig {
field: string;
@ -238,38 +247,19 @@
//
const taskTypeContentConfig: Record<string, ContentSectionConfig[]> = {
//
'contract': [
{ field: 'originalText', title: '合同原文', type: 'markdown' },
{ field: 'legalIssues', title: '法律问题', type: 'markdown' },
{ field: 'suggestions', title: '修改建议', type: 'markdown' },
"contractualReview": [
{ field: 'originalText', title: '原文', type: 'markdown' },
{ field: 'modifiedContent', title: '修改建议', type: 'markdown' },
{ field: 'modificationDisplay', title: '修改情况展示', type: 'markdown' },
// { field: 'existingIssues', title: '', type: 'markdown' },
{
field: 'reviewBasis',
title: '法律依据',
title: '审查依据',
type: 'reviewPointsList',
nestedFields: ['reviewContent', 'reviewPoints']
}
],
"checkCompanyName": [
{ field: 'modificationDisplay', title: '相关原文', type: 'markdown' },
],
"checkTitleName": [
{ field: 'modificationDisplay', title: '相关原文', type: 'markdown' },
],
"checkPlaceName": [
{ field: 'modificationDisplay', title: '相关原文', type: 'markdown' },
],
"checkRepeatText": [
{ field: 'originalText', title: '第一段原文', type: 'markdown' },
{ field: 'comparedText', title: '第二段原文', type: 'markdown' },
{ field: 'modificationDisplay', title: '相似情况', type: 'markdown' },
],
"checkDocumentError": [
{ field: 'originalText', title: '原文', type: 'markdown' },
{ field: 'modifiedContent', title: '修改建议', type: 'markdown' },
{ field: 'modificationDisplay', title: '修改情况', type: 'markdown' },
],
// checkDocumentErrorschemEvaluation
};
//
@ -291,6 +281,13 @@
for (const part of parts) {
if (value && typeof value === 'object') {
value = value[part];
} else if (part === 'reviewBasis' && typeof value === 'string') {
// reviewBasisJSON
try {
value = JSON.parse(value);
} catch (e) {
return null;
}
} else {
return null;
}
@ -298,6 +295,16 @@
return value;
}
// 访reviewBasis
if (field === 'reviewBasis' && typeof item[field] === 'string') {
try {
return JSON.parse(item[field]);
} catch (e) {
//
return item[field];
}
}
return item[field];
};
@ -312,9 +319,31 @@
if (section.type === 'reviewPointsList' && section.nestedFields) {
let content = '';
// reviewContent
if (section.nestedFields.includes('reviewContent') && value.reviewContent) {
content += renderMarkdown(value.reviewContent);
// reviewBasis
if (section.field === 'reviewBasis') {
let reviewBasisObj = value;
// valueJSON
if (typeof value === 'string') {
try {
reviewBasisObj = JSON.parse(value);
} catch (e) {
//
return value.toString().replace(/\n/g, '<br>');
}
}
// review_content
if (section.nestedFields.includes('reviewContent') && reviewBasisObj.review_content) {
content += renderMarkdown(reviewBasisObj.review_content);
}
return content;
}
// reviewPointsList
if (section.nestedFields.includes('reviewContent') && value.review_content) {
content += renderMarkdown(value.review_content);
}
return content;
@ -420,7 +449,7 @@
};
//
const renderPage = async (num: number) => {
const renderPage = async (num: number, highlightStr?: string) => {
if (!pdfDoc || !pdfCanvas.value || !pdfContainer.value) return;
try {
@ -453,6 +482,11 @@
currentRenderTask = page.render(renderContext);
await currentRenderTask.promise;
currentRenderTask = null;
// ====== ======
if (highlightStr) {
await highlightTextOnPage(page, ctx, viewport, highlightStr);
}
} catch (error) {
currentRenderTask = null;
// RenderingCancelledException
@ -644,9 +678,10 @@
const handlePageSelect = async (page: number) => {
currentPage.value = page;
await renderPage(page);
// 使**
await renderPage(page, highlightText.value ?? undefined);
showPageSelectModal.value = false;
// message.success(`${page}`);
message.success(`已定位到第${page}`);
};
const handlePageModalClose = () => {
@ -655,29 +690,133 @@
const locateByText = async (text: string) => {
if (!pdfDoc || !text) return;
// Markdown**
const cleanText = text.replace(/\*\*/g, '');
const numPages = pdfDoc.numPages;
const foundPages: number[] = [];
//
for (let i = 1; i <= numPages; i++) {
const page = await pdfDoc.getPage(i);
const content = await page.getTextContent();
const pageText = content.items.map((item: any) => item.str).join('');
if (pageText.replace(/\s/g, '').includes(text.replace(/\s/g, ''))) {
//
//
const normalizeText = (text: string) => {
return text
.replace(/[\r\n\t\f\v]/g, '') //
.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '') //
.toLowerCase(); //
};
const normalizedPageText = normalizeText(pageText);
const normalizedSearchText = normalizeText(cleanText);
if (normalizedPageText.includes(normalizedSearchText)) {
foundPages.push(i);
}
}
if (foundPages.length === 0) {
message.warning('未在PDF中找到该文本');
return;
}
// **
highlightText.value = cleanText;
//
if (foundPages.length === 1) {
currentPage.value = foundPages[0];
await renderPage(foundPages[0]);
await renderPage(foundPages[0], cleanText);
message.success(`已定位到第${foundPages[0]}`);
return;
}
//
matchedPages.value = foundPages;
showPageSelectModal.value = true;
};
//
async function highlightTextOnPage(page, ctx, viewport, targetText) {
if (!targetText) return;
// **
const cleanTargetText = targetText.replace(/\*\*/g, '');
const textContent = await page.getTextContent();
const items = textContent.items as any[];
//
const pageTextWithSpaces = items.map(i => i.str).join('');
const pageText = pageTextWithSpaces.replace(/\s/g, '');
const target = cleanTargetText.replace(/\s/g, '');
//
const startIdx = pageText.indexOf(target);
if (startIdx === -1) return;
// item
let charCount = 0;
let highlightItems: any[] = [];
let highlightStarted = false;
let highlightLength = 0;
for (let item of items) {
const itemTextNoSpace = item.str.replace(/\s/g, '');
if (!highlightStarted && charCount + itemTextNoSpace.length > startIdx) {
highlightStarted = true;
}
if (highlightStarted && highlightLength < target.length) {
highlightItems.push(item);
highlightLength += itemTextNoSpace.length;
if (highlightLength >= target.length) break;
}
charCount += itemTextNoSpace.length;
}
//
ctx.save();
ctx.globalAlpha = 0.4;
ctx.fillStyle = '#ffd54f';
for (let item of highlightItems) {
const transform = item.transform;
const x = transform[4]; // e
const y = transform[5]; // f
// viewport
const pt = viewport.convertToViewportPoint(x, y);
// pdf.jsy线
const height = item.height || item.fontSize || 10;
const width = item.width || (item.str.length * (item.fontSize || 10) * 0.6);
ctx.fillRect(pt[0], pt[1] - height, width, height + 2); // 便
}
ctx.restore();
}
const getParsedReviewBasis = (reviewBasis: any) => {
if (typeof reviewBasis === 'string') {
try {
return JSON.parse(reviewBasis);
} catch (e) {
return null;
}
} else if (typeof reviewBasis === 'object') {
return reviewBasis;
} else {
return null;
}
};
</script>
<style lang="less" scoped>
@ -725,6 +864,13 @@
cursor: pointer;
}
// Ant Design Card
.item-card :deep(.ant-card-head-title) {
overflow: visible !important;
white-space: normal !important;
text-overflow: unset !important;
}
.expand-icon {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
@ -733,17 +879,17 @@
.item-header-content {
display: flex;
justify-content: space-between;
align-items: center;
align-items: flex-start;
width: 100%;
}
.item-info {
display: flex;
align-items: center;
align-items: flex-start;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow: visible;
text-overflow: unset;
white-space: normal;
}
.item-serial {
@ -756,14 +902,17 @@
font-weight: 500;
font-size: 14px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow: visible;
text-overflow: unset;
white-space: normal;
word-wrap: break-word;
word-break: break-word;
line-height: 1.5;
}
.item-actions {
display: flex;
align-items: center;
align-items: flex-start;
margin-left: 16px;
flex-shrink: 0;
}
@ -865,6 +1014,14 @@
}
}
.review-points-title,
.review-content-title {
font-weight: 600;
color: #1890ff;
margin-bottom: 8px;
font-size: 14px;
}
.ml-3 {
margin-left: 12px;
}

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

@ -5,51 +5,33 @@
<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 class="checkbox-group">
<Checkbox.Group v-model:value="selectedRegulations">
<Checkbox value="contract-law">
<div class="checkbox-option">
<div class="option-content">
<div class="option-title">中华人民共和国合同法</div>
<div class="option-desc">检查合同条款是否符合合同法基本要求</div>
</div>
<div class="card-body">
<h3>中华人民共和国合同法</h3>
<p class="card-desc">检查合同条款是否符合合同法基本要求</p>
</div>
</Checkbox>
<Checkbox value="labor-law">
<div class="checkbox-option">
<div class="option-content">
<div class="option-title">劳动合同相关法规</div>
<div class="option-desc">检查劳动合同条款合规性</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>
</Checkbox>
<Checkbox value="company-law">
<div class="checkbox-option">
<div class="option-content">
<div class="option-title">公司法相关规定</div>
<div class="option-desc">检查公司间合同的法律合规性</div>
</div>
</div>
</Checkbox>
</Checkbox.Group>
</div>
</div>
@ -58,56 +40,45 @@
<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 class="radio-group">
<Radio.Group v-model:value="selectedIndustry" size="large">
<Radio value="financial">
<div class="radio-option">
<BankOutlined class="option-icon" />
<div class="option-content">
<div class="option-title">金融服务</div>
<div class="option-desc">银行证券保险等金融行业合规要求</div>
</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 />
</Radio>
<Radio value="medical">
<div class="radio-option">
<MedicineBoxOutlined class="option-icon" />
<div class="option-content">
<div class="option-title">医疗健康</div>
<div class="option-desc">医疗器械药品健康服务等行业规范</div>
</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 />
</Radio>
<Radio value="technology">
<div class="radio-option">
<LaptopOutlined class="option-icon" />
<div class="option-content">
<div class="option-title">科技互联网</div>
<div class="option-desc">数据保护网络安全等科技行业要求</div>
</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>
</Radio>
<Radio value="general">
<div class="radio-option">
<GlobalOutlined class="option-icon" />
<div class="option-content">
<div class="option-title">通用行业</div>
<div class="option-desc">一般商业合同合规检查</div>
</div>
</div>
</Radio>
</Radio.Group>
</div>
</div>
@ -116,37 +87,37 @@
<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 class="radio-group">
<Radio.Group v-model:value="selectedLevel" size="large">
<Radio value="basic">
<div class="radio-option">
<span class="option-icon">🔍</span>
<div class="option-content">
<div class="option-title">基础检查</div>
<div class="option-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>
</Radio>
<Radio value="standard">
<div class="radio-option">
<span class="option-icon"></span>
<div class="option-content">
<div class="option-title">标准检查</div>
<div class="option-desc">全面的合规性审查覆盖常见风险点</div>
</div>
</div>
</Radio>
<Radio value="strict">
<div class="radio-option">
<span class="option-icon">🛡</span>
<div class="option-content">
<div class="option-title">严格检查</div>
<div class="option-desc">最高级别检查包含潜在风险分析</div>
</div>
</div>
</Radio>
</Radio.Group>
</div>
</div>
<!-- 特别关注点 -->
@ -170,9 +141,8 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Input } from 'ant-design-vue';
import { Input, Checkbox, Radio } from 'ant-design-vue';
import {
CheckCircleOutlined,
BankOutlined,
MedicineBoxOutlined,
LaptopOutlined,
@ -186,26 +156,6 @@
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 = () => {
//
@ -231,14 +181,14 @@
<style lang="less" scoped>
.compliance-content {
padding: 16px;
padding: 12px;
}
//
.section {
margin-bottom: 24px;
margin-bottom: 16px;
border-bottom: 1px dashed #eee;
padding-bottom: 16px;
padding-bottom: 12px;
&:last-child {
border-bottom: none;
@ -246,190 +196,136 @@
}
.section-title {
font-size: 16px;
margin-bottom: 6px;
font-size: 15px;
margin-bottom: 4px;
font-weight: 500;
color: #333;
}
.section-description {
color: #666;
margin-bottom: 16px;
font-size: 14px;
margin-bottom: 12px;
font-size: 13px;
line-height: 1.4;
}
//
.regulation-options {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
//
.checkbox-group {
:deep(.ant-checkbox-group) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 8px;
width: 100%;
}
.regulation-card {
flex: 1;
min-width: 280px;
border: 2px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
:deep(.ant-checkbox-wrapper) {
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
margin: 0;
transition: all 0.3s;
cursor: pointer;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #722ed1;
transform: translateY(-2px);
background-color: #f9f0ff;
}
&.selected {
&.ant-checkbox-wrapper-checked {
border-color: #722ed1;
box-shadow: 0 0 0 2px rgba(114, 46, 209, 0.2);
background-color: #f9f0ff;
box-shadow: 0 0 0 2px rgba(114, 46, 209, 0.2);
}
}
}
.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;
:deep(.ant-checkbox) {
.ant-checkbox-inner {
border-color: #722ed1;
}
.regulation-card.selected & {
&.ant-checkbox-checked .ant-checkbox-inner {
background-color: #722ed1;
color: white;
border-color: #722ed1;
}
.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;
//
.radio-group {
:deep(.ant-radio-group) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 8px;
width: 100%;
}
.card-desc {
:deep(.ant-radio-wrapper) {
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
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;
transition: all 0.3s;
&:hover {
border-color: #722ed1;
box-shadow: 0 2px 8px rgba(114, 46, 209, 0.15);
background-color: #f9f0ff;
}
&.selected {
&.ant-radio-wrapper-checked {
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 {
:deep(.ant-radio) {
.ant-radio-inner {
border-color: #722ed1;
box-shadow: 0 2px 8px rgba(114, 46, 209, 0.15);
}
&.selected {
&.ant-radio-checked .ant-radio-inner {
background-color: #722ed1;
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;
}
//
.checkbox-option,
.radio-option {
display: flex;
align-items: center;
gap: 8px;
.level-title {
.option-icon {
font-size: 16px;
color: #722ed1;
}
.option-content {
flex: 1;
.option-title {
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
color: #333;
}
margin-bottom: 2px;
}
.level-desc {
font-size: 12px;
.option-desc {
font-size: 11px;
color: #666;
line-height: 1.4;
line-height: 1.3;
}
}
}
//
.focus-input {
margin-top: 16px;
margin-top: 12px;
:deep(.ant-input) {
font-size: 14px !important;
border-radius: 6px !important;
font-size: 13px !important;
border-radius: 4px !important;
&:focus {
border-color: #722ed1;
@ -439,7 +335,7 @@
:deep(.ant-input-data-count) {
color: #999;
font-size: 12px;
font-size: 11px;
}
}
</style>

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

@ -1,291 +1,53 @@
<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="section file-upload-section">
<h3 class="section-title">上传招投标文件</h3>
<p class="section-description">上传需要与合同进行一致性对比的招投标文件</p>
<div class="upload-box" :class="{'file-preview': bidFileList && bidFileList.length > 0}">
<template v-if="bidFileList && bidFileList.length > 0">
<!-- 文件预览 -->
<div class="file-info">
<FileTextOutlined class="file-icon" />
<div class="file-details">
<p class="file-name">{{ bidFileList[0].name }}</p>
<div class="file-progress" v-if="uploading">
<Progress :percent="uploadPercent" size="small" status="active" />
</div>
<p class="file-status" v-else>
<CheckCircleFilled class="status-icon success" /> 上传成功
<AButton type="link" class="remove-btn" @click="removeBidFile">删除</AButton>
</p>
</div>
</div>
</template>
<template v-else>
<!-- 上传区域 -->
<AUpload
:fileList="bidFileList"
:customRequest="customUploadRequest"
:beforeUpload="beforeUpload"
:showUploadList="false"
:maxCount="1"
:multiple="false"
name="file"
accept=".pdf,.doc,.docx"
draggable
>
<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 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">支持PDFDOCDOCX格式文件</p>
</div>
</div>
</AUpload>
</template>
</div>
</div>
@ -309,81 +71,155 @@
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { Input } from 'ant-design-vue';
import { ref } from 'vue';
import { Input, Progress, Button, Upload } from 'ant-design-vue';
import {
CheckCircleOutlined,
ExclamationCircleOutlined,
WarningOutlined,
InfoCircleOutlined
UpOutlined,
FileTextOutlined,
CheckCircleFilled
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import type { UploadProps } from 'ant-design-vue';
import { uploadApi } from '@/api/upload';
import { ossRemove } from '@/api/system/oss';
import { UploadFileParams } from '#/axios';
//
const AButton = Button;
const AUpload = Upload;
//
const selectedComparisons = ref<string[]>(['tender', 'bid']); //
const selectedDimensions = ref<string[]>(['technical', 'commercial', 'delivery']); //
const selectedDeviationLevel = ref<string>('standard'); //
const bidFileList = ref<UploadProps['fileList']>([]);
const uploading = ref(false);
const uploadPercent = ref(0);
const currentBidOssId = ref<string | null>(null);
const specialFocus = ref<string>(''); //
//
const priorities = reactive({
price: 'high',
technical: 'high',
delivery: 'medium',
service: 'medium'
//
interface UploadResponse {
ossId: string;
url: string;
fileName: string;
}
//
function customUploadRequest(options: any) {
const { file, onSuccess, onError } = options;
//
uploading.value = true;
uploadPercent.value = 0;
//
const uploadParams: UploadFileParams = {
name: 'file',
file: file,
data: {},
};
//
bidFileList.value = [
{
uid: '1',
name: file.name,
status: 'uploading',
url: URL.createObjectURL(file),
} as any,
];
// API
uploadApi(
uploadParams,
(progressEvent) => {
//
if (progressEvent.total) {
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
uploadPercent.value = percent;
}
}
).then((res) => {
//
if (bidFileList.value && bidFileList.value.length > 0) {
bidFileList.value[0].status = 'done';
bidFileList.value[0].response = res;
// OSS ID
if (res && res.ossId) {
currentBidOssId.value = res.ossId;
}
}
uploading.value = false;
uploadPercent.value = 100;
message.success(`${file.name} 文件上传成功`);
onSuccess(res);
}).catch((err) => {
//
uploading.value = false;
bidFileList.value = [];
currentBidOssId.value = null;
message.error(`文件上传失败: ${err.message || '未知错误'}`);
onError(err);
});
}
//
const toggleComparison = (comparison: string) => {
const index = selectedComparisons.value.indexOf(comparison);
if (index > -1) {
selectedComparisons.value.splice(index, 1);
} else {
selectedComparisons.value.push(comparison);
function beforeUpload(file: File) {
const isPdf = file.type === 'application/pdf';
const isDoc = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
file.type === 'application/msword';
if (!isPdf && !isDoc) {
message.error('只能上传 PDF/DOC/DOCX 格式的文件!');
return false;
}
};
//
const toggleDimension = (dimension: string) => {
const index = selectedDimensions.value.indexOf(dimension);
if (index > -1) {
selectedDimensions.value.splice(index, 1);
} else {
selectedDimensions.value.push(dimension);
// 500MB
const isLt500M = file.size / 1024 / 1024 < 500;
if (!isLt500M) {
message.error('文件大小不能超过 500MB!');
return false;
}
};
//
const selectDeviationLevel = (level: string) => {
selectedDeviationLevel.value = level;
};
return true;
}
//
const setPriority = (key: string, level: string) => {
priorities[key] = level;
};
//
function removeBidFile() {
if (uploading.value) {
message.warning('文件正在上传中,请稍后再试');
return;
}
//
const getData = () => {
//
if (selectedComparisons.value.length === 0) {
message.warning('请至少选择一种对比文件类型');
return null;
// 使OSS ID
if (currentBidOssId.value) {
ossRemove([currentBidOssId.value])
.then(() => {
bidFileList.value = [];
uploadPercent.value = 0;
currentBidOssId.value = null;
message.success('招投标文件已删除');
})
.catch((err) => {
message.error(`文件删除失败: ${err.message || '未知错误'}`);
});
} else {
// ossId
bidFileList.value = [];
uploadPercent.value = 0;
message.success('招投标文件已删除');
}
}
//
if (selectedDimensions.value.length === 0) {
message.warning('请至少选择一个检查维度');
//
const getData = () => {
//
if (!currentBidOssId.value) {
message.warning('请上传招投标文件');
return null;
}
return {
type: 'consistency',
comparisons: selectedComparisons.value,
dimensions: selectedDimensions.value,
deviationLevel: selectedDeviationLevel.value,
priorities: { ...priorities },
specialFocus: specialFocus.value || undefined,
bidDocumentOssId: currentBidOssId.value, // ossId
specialNote: specialFocus.value || undefined, //
};
};
@ -395,14 +231,14 @@
<style lang="less" scoped>
.consistency-content {
padding: 16px;
padding: 12px;
}
//
.section {
margin-bottom: 24px;
border-bottom: 1px dashed #eee;
padding-bottom: 16px;
padding-bottom: 20px;
&:last-child {
border-bottom: none;
@ -411,7 +247,7 @@
.section-title {
font-size: 16px;
margin-bottom: 6px;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
@ -420,209 +256,142 @@
color: #666;
margin-bottom: 16px;
font-size: 14px;
line-height: 1.4;
}
//
.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;
//
.upload-box {
border: 1px dashed #ddd;
border-radius: 8px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: border-color 0.3s;
width: 100%;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f9f9f9;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #13c2c2;
transform: translateY(-2px);
background-color: rgba(19, 194, 194, 0.02);
}
&.selected {
border-color: #13c2c2;
box-shadow: 0 0 0 2px rgba(19, 194, 194, 0.2);
background-color: #e6fffb;
&.file-preview {
border: 2px solid #e6fffb;
background-color: #f0f8ff;
cursor: default;
}
}
.card-header {
background-color: #f7f7f7;
padding: 8px;
font-weight: 500;
font-size: 14px;
text-align: center;
color: #333;
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
.comparison-card.selected &,
.dimension-card.selected & {
background-color: #13c2c2;
color: white;
}
.check-icon {
font-size: 16px;
}
width: 100%;
}
.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 {
.upload-icon {
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;
align-items: center;
margin-bottom: 16px;
background-color: #e6fffb;
box-shadow: 0 0 0 2px rgba(19, 194, 194, 0.2);
}
width: 50px;
height: 50px;
border-radius: 50%;
}
.card-icon {
.upload-arrow-icon {
font-size: 24px;
color: #13c2c2;
margin-bottom: 8px;
}
.card-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
.upload-text-container {
text-align: center;
}
.upload-text {
font-size: 16px;
color: #333;
margin-bottom: 8px;
}
.card-desc {
font-size: 12px;
color: #666;
line-height: 1.4;
.upload-link {
color: #13c2c2;
cursor: pointer;
text-decoration: none;
font-weight: 600;
}
//
.priority-settings {
background-color: #f9f9f9;
border-radius: 6px;
padding: 16px;
.upload-link:hover {
text-decoration: underline;
}
.upload-hint {
color: #888;
font-size: 14px;
margin-top: 8px;
}
.priority-item {
/* 文件预览区域 */
.file-info {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
width: 100%;
max-width: 800px;
}
&:last-child {
margin-bottom: 0;
}
.file-icon {
font-size: 32px;
color: #13c2c2;
margin-right: 16px;
}
.priority-label {
font-size: 14px;
.file-details {
flex: 1;
}
.file-name {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px 0;
color: #333;
min-width: 80px;
}
.priority-buttons {
display: flex;
gap: 6px;
.file-progress {
margin: 0 0 8px 0;
width: 100%;
}
.priority-btn {
padding: 4px 12px;
border: 2px solid #e8e8e8;
border-radius: 16px;
cursor: pointer;
transition: all 0.3s;
font-size: 12px;
font-weight: 500;
.file-status {
font-size: 14px;
color: #666;
margin: 0;
display: flex;
align-items: center;
}
&:hover {
border-color: #13c2c2;
color: #13c2c2;
}
.status-icon {
margin-right: 6px;
}
&.active {
border-color: #13c2c2;
background-color: #13c2c2;
color: white;
}
.status-icon.success {
color: #52c41a;
}
.remove-btn {
padding: 0;
margin-left: 12px;
font-size: 14px;
}
//
.focus-input {
margin-top: 16px;
margin-top: 12px;
:deep(.ant-input) {
font-size: 14px !important;

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

@ -36,7 +36,7 @@
</div>
<div class="upload-text-container">
<p class="upload-text">
拖拽合同文件至此 <a class="upload-link">选择文件</a>
<a class="upload-link">选择文件</a>
</p>
<p class="upload-hint">仅支持docdocx格式文件</p>
<a class="sample-link" @click="useSampleFile">使用样例合同审查</a>
@ -79,6 +79,7 @@
import { UploadFileParams } from '#/axios';
import { useModal } from '@/components/Modal';
import ReviewConfigDialog from './ReviewConfigDialog.vue';
import { StartReview, StartContractReviewRequest } from '@/api/contractReview/ContractualTasks';
// props
interface Props {
@ -256,11 +257,51 @@
//
function handleReviewSuccess(data: any) {
console.log('Review configuration completed with data:', data);
// ossId
//
const reviewRequest: StartContractReviewRequest = {
ossId: currentOssId.value!,
reviewTypes: data.reviewTypes || props.reviewTypes,
reviewData: data.reviewData,
visitedTabs: data.visitedTabs
};
console.log('Sending review request:', reviewRequest);
//
message.loading({ content: '正在启动合同审查...', duration: 0, key: 'startReview' });
// API
StartReview(reviewRequest)
.then((response) => {
console.log('Review started successfully:', response);
//
message.success({
content: response.message || '合同审查已启动,正在后台处理',
key: 'startReview'
});
//
emit('success', {
taskId: response.taskId,
taskName: response.taskName,
status: response.status,
ossId: currentOssId.value,
...data
});
})
.catch((error) => {
console.error('Failed to start review:', error);
//
message.error({
content: '启动合同审查失败: ' + (error.message || '未知错误'),
key: 'startReview'
});
//
});
}
//

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

@ -3,14 +3,14 @@
v-bind="$attrs"
@register="register"
title="合同审查配置"
:width="1200"
:maskClosable="false"
:keyboard="false"
:width="1200"
:height="600"
:useWrapper="true"
:destroyOnClose="false"
:forceRender="true"
@cancel="handleCancel"
:okText="'开始分析'"
@ok="handleConfirm"
:showOkBtn="false"
:showCancelBtn="false"
>
<div class="review-dialog-content">
<!-- 加载状态 -->
@ -26,44 +26,99 @@
<!-- 审查配置内容 -->
<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>
<Tabs
v-model:activeKey="activeTab"
@change="handleTabChange"
:tab-bar-style="{ marginBottom: '20px' }"
>
<!-- 实质性审查标签页 -->
<TabPane
v-if="shouldShowReviewType('substantive')"
key="substantive"
:class="{ 'visited-tab': visitedTabs.includes('substantive') }"
>
<template #tab>
<span :class="{ 'visited-tab-title': visitedTabs.includes('substantive') }">
实质性审查
</span>
</template>
<div class="tab-content">
<SubstantiveContent ref="substantiveRef" />
</div>
</TabPane>
<!-- 合规性审查区域 -->
<div v-if="shouldShowReviewType('compliance')" class="review-section compliance-section">
<div class="section-header">
<h3 class="section-title">合规性审查</h3>
</div>
<!-- 合规性审查标签页 -->
<TabPane
v-if="shouldShowReviewType('compliance')"
key="compliance"
:class="{ 'visited-tab': visitedTabs.includes('compliance') }"
>
<template #tab>
<span :class="{ 'visited-tab-title': visitedTabs.includes('compliance') }">
合规性审查
</span>
</template>
<div class="tab-content">
<ComplianceContent ref="complianceRef" />
</div>
</TabPane>
<!-- 一致性审查区域 -->
<div v-if="shouldShowReviewType('consistency')" class="review-section consistency-section">
<div class="section-header">
<h3 class="section-title">一致性审查</h3>
</div>
<!-- 一致性审查标签页 -->
<TabPane
v-if="shouldShowReviewType('consistency')"
key="consistency"
:class="{ 'visited-tab': visitedTabs.includes('consistency') }"
>
<template #tab>
<span :class="{ 'visited-tab-title': visitedTabs.includes('consistency') }">
一致性审查
</span>
</template>
<div class="tab-content">
<ConsistencyContent ref="consistencyRef" />
</div>
</TabPane>
</Tabs>
</div>
</div>
<!-- 自定义底部按钮 -->
<template #footer>
<div class="modal-footer">
<a-button @click="handleCancel">取消</a-button>
<a-button
v-if="!isLastTab"
type="primary"
@click="goToNextTab(activeTab)"
>
下一页
</a-button>
<a-button
type="primary"
:disabled="!canStartAnalysis"
@click="handleConfirm"
>
开始分析
</a-button>
</div>
</template>
</BasicModal>
</template>
<script setup lang="ts">
import { ref, nextTick, watch } from 'vue';
import { ref, nextTick, watch, computed } from 'vue';
import { BasicModal, useModalInner } from '@/components/Modal';
import { LoadingOutlined } from '@ant-design/icons-vue';
import { Tabs } from 'ant-design-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';
//
const TabPane = Tabs.TabPane;
// props
interface Props {
reviewTypes?: string[];
@ -78,16 +133,45 @@
//
const analyzing = ref(true);
const activeTab = ref('');
const visitedTabs = ref<string[]>([]);
//
const substantiveRef = ref();
const complianceRef = ref();
const consistencyRef = ref();
//
const availableTabs = computed(() => {
const tabs: string[] = [];
if (shouldShowReviewType('substantive')) tabs.push('substantive');
if (shouldShowReviewType('compliance')) tabs.push('compliance');
if (shouldShowReviewType('consistency')) tabs.push('consistency');
return tabs;
});
//
const isLastTab = computed(() => {
const currentIndex = availableTabs.value.indexOf(activeTab.value);
return currentIndex >= availableTabs.value.length - 1;
});
// 访
const canStartAnalysis = computed(() => {
return availableTabs.value.every(tab => visitedTabs.value.includes(tab));
});
// 使modalInnermodal
const [register, { closeModal, redoModalHeight }] = useModalInner((data) => {
//
analyzing.value = true;
visitedTabs.value = [];
//
if (availableTabs.value.length > 0) {
activeTab.value = availableTabs.value[0];
visitedTabs.value = [availableTabs.value[0]];
}
console.log('ReviewConfigDialog Modal opened with data:', data);
@ -140,6 +224,34 @@
});
});
//
function handleTabChange(key: string | number) {
const keyStr = String(key);
activeTab.value = keyStr;
// 访
if (!visitedTabs.value.includes(keyStr)) {
visitedTabs.value.push(keyStr);
}
}
//
function getNextTab(currentTab: string) {
const currentIndex = availableTabs.value.indexOf(currentTab);
if (currentIndex >= 0 && currentIndex < availableTabs.value.length - 1) {
return availableTabs.value[currentIndex + 1];
}
return null;
}
//
function goToNextTab(currentTab: string) {
const nextTab = getNextTab(currentTab);
if (nextTab) {
activeTab.value = nextTab;
handleTabChange(nextTab);
}
}
//
function shouldShowReviewType(type: string) {
//
@ -157,6 +269,12 @@
// -
function handleConfirm() {
// 访
if (!canStartAnalysis.value) {
message.warning('请完成所有标签页的配置后再开始分析');
return;
}
const reviewData: any = {};
try {
@ -194,7 +312,8 @@
closeModal();
emit('success', {
reviewTypes: props.reviewTypes,
reviewData: reviewData
reviewData: reviewData,
visitedTabs: visitedTabs.value
});
message.success('审查配置完成,开始分析合同');
@ -208,7 +327,6 @@
<style lang="less" scoped>
.review-dialog-content {
padding: 20px;
min-height: 500px;
}
//
@ -244,67 +362,64 @@
width: 100%;
}
.review-section {
border: 2px solid #ddd;
border-radius: 8px;
margin-bottom: 20px;
overflow: hidden;
}
.review-section:last-child {
margin-bottom: 0;
//
.tab-content {
// padding: 20px;
min-height: 400px;
}
.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;
// 访
.visited-tab-title {
position: relative;
&::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #52c41a, #13c2c2);
border-radius: 2px;
}
}
.consistency-section .section-header {
background-color: #e6fffb;
border-bottom-color: #13c2c2;
//
:deep(.ant-tabs-tabpane) {
&.visited-tab {
background: linear-gradient(135deg, #f6ffed 0%, #e6fffb 100%);
border-radius: 8px;
margin: 0 -8px;
padding: 8px;
}
}
.section-title {
font-size: 18px;
//
:deep(.ant-tabs-tab) {
font-weight: 500;
margin: 0;
color: #333;
}
font-size: 16px;
.substantive-section .section-title {
color: #52c41a;
&.ant-tabs-tab-active {
.ant-tabs-tab-btn {
color: #1890ff;
font-weight: 600;
}
}
}
.compliance-section .section-title {
color: #722ed1;
:deep(.ant-tabs-ink-bar) {
height: 3px;
background: linear-gradient(90deg, #1890ff, #52c41a);
}
.consistency-section .section-title {
color: #13c2c2;
//
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px 24px;
border-top: 1px solid #f0f0f0;
background-color: #fafafa;
}
// Modal
@ -315,5 +430,11 @@
:deep(.ant-modal-body) {
max-height: 80vh;
overflow-y: auto;
padding-bottom: 0;
}
:deep(.ant-modal-footer) {
padding: 0;
border-top: none;
}
</style>

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

@ -5,42 +5,12 @@
<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 class="radio-group">
<Radio.Group v-model:value="selectedPosition" size="large">
<Radio value="party-a">甲方立场 - 从甲方角度审查合同</Radio>
<Radio value="neutral">中立立场 - 客观中立地审查合同</Radio>
<Radio value="party-b">乙方立场 - 从乙方角度审查合同</Radio>
</Radio.Group>
</div>
</div>
@ -50,43 +20,37 @@
<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 class="radio-group">
<Radio.Group v-model:value="selectedReviewType" size="large">
<Radio value="ai">
<div class="radio-option">
<RobotOutlined class="option-icon" />
<div class="option-content">
<div class="option-title">AI自动生成</div>
<div class="option-desc">智能分析合同内容生成审查清单</div>
</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 />
</Radio>
<Radio value="ai-contract">
<div class="radio-option">
<RobotOutlined class="option-icon" />
<FileTextOutlined class="option-icon" />
<div class="option-content">
<div class="option-title">AI生成+合同类型</div>
<div class="option-desc">基于合同类型模板结合AI智能优化</div>
</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 />
</Radio>
<Radio value="contract-type">
<div class="radio-option">
<FileTextOutlined class="option-icon" />
<div class="option-content">
<div class="option-title">合同类型</div>
<div class="option-desc">使用预设的合同类型审查要点</div>
</div>
<div class="card-title">合同类型</div>
<div class="card-desc">使用预设的合同类型审查要点</div>
</div>
</Radio>
</Radio.Group>
</div>
<!-- 合同类型选择器 -->
@ -145,7 +109,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { Input, Select } from 'ant-design-vue';
import { Input, Select, Radio } from 'ant-design-vue';
import {
RobotOutlined,
FileTextOutlined
@ -193,20 +157,6 @@
}
};
//
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;
@ -283,14 +233,14 @@
<style lang="less" scoped>
.substantive-content {
padding: 16px;
padding: 12px;
}
//
.section {
margin-bottom: 24px;
margin-bottom: 16px;
border-bottom: 1px dashed #eee;
padding-bottom: 16px;
padding-bottom: 12px;
&:last-child {
border-bottom: none;
@ -298,147 +248,105 @@
}
.section-title {
font-size: 16px;
margin-bottom: 6px;
font-size: 15px;
margin-bottom: 4px;
font-weight: 500;
color: #333;
}
.section-description {
color: #666;
margin-bottom: 16px;
font-size: 14px;
margin-bottom: 12px;
font-size: 13px;
line-height: 1.4;
}
//
.position-options {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 15px;
}
//
.radio-group {
:deep(.ant-radio-group) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 8px;
width: 100%;
}
.position-card {
flex: 1;
border: 2px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
:deep(.ant-radio-wrapper) {
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
margin: 0;
transition: all 0.3s;
cursor: pointer;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #52c41a;
transform: translateY(-2px);
background-color: #f6ffed;
}
&.selected {
&.ant-radio-wrapper-checked {
border-color: #52c41a;
box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.2);
background-color: #f6ffed;
box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.2);
}
}
.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;
:deep(.ant-radio) {
.ant-radio-inner {
border-color: #52c41a;
}
.card-desc {
margin: 0;
font-size: 12px;
color: #666;
line-height: 1.4;
&.ant-radio-checked .ant-radio-inner {
background-color: #52c41a;
border-color: #52c41a;
}
}
}
//
.review-type-selector {
padding: 16px;
}
.review-type-options {
//
.radio-option {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
align-items: center;
gap: 8px;
.review-type-card {
.option-icon {
font-size: 16px;
color: #52c41a;
}
.option-content {
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);
.option-title {
font-size: 13px;
font-weight: 500;
color: #333;
margin-bottom: 2px;
}
&.selected {
border-color: #52c41a;
background-color: #f6ffed;
box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.2);
.option-desc {
font-size: 11px;
color: #666;
line-height: 1.3;
}
}
.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;
//
.review-type-selector {
padding: 12px;
}
//
.contract-type-selector {
margin-top: 16px;
padding: 16px;
margin-top: 12px;
padding: 12px;
background-color: #f9f9f9;
border-radius: 6px;
border-radius: 4px;
.selector-label {
display: block;
font-size: 14px;
font-size: 13px;
font-weight: 500;
margin-bottom: 8px;
margin-bottom: 6px;
color: #333;
}
}
@ -446,26 +354,27 @@
.contract-type-option {
display: flex;
flex-direction: column;
gap: 4px;
gap: 2px;
.contract-name {
font-weight: 500;
color: #333;
font-size: 13px;
}
.contract-desc {
font-size: 12px;
font-size: 11px;
color: #666;
}
}
//
.special-note-input {
margin-top: 16px;
margin-top: 12px;
:deep(.ant-input) {
font-size: 14px !important;
border-radius: 6px !important;
font-size: 13px !important;
border-radius: 4px !important;
&:focus {
border-color: #52c41a;
@ -475,7 +384,7 @@
:deep(.ant-input-data-count) {
color: #999;
font-size: 12px;
font-size: 11px;
}
}
</style>

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

@ -2,8 +2,7 @@
<div class="markup-container">
<!-- 头部标题 -->
<div class="markup-header">
<img src="@/assets/images/markup-logo.svg" alt="Markup" class="markup-logo" />
<h1 class="markup-title">大模型驱动的新一代合同审查工具</h1>
<h1 class="markup-title">国研AI合同审查工具</h1>
</div>
<!-- 审查选项 -->

39
src/views/contractReview/ContractualTasks/list.vue

@ -90,7 +90,7 @@
</BasicTable>
<ContractualTasksModal @register="registerModal" @reload="reload" />
<DocsDrawer @register="registerDrawer" />
<ResultDetailDrawer
<ContractualResultDetailDrawer
:visible="resultDetailDrawerVisible"
:taskResultDetail="taskResultDetail"
:taskInfo="currentTaskInfo"
@ -114,21 +114,15 @@
import { DocumentTasksStop } from '@/api/documentReview/DocumentTasks';
import {
ContractualTasksList,
ContractualTasksExport,
ContractualTasksRemove,
} from '@/api/contractReview/ContractualTasks';
import {
DocumentTaskResultsInfoByTaskId,
DocumentTaskResultDownload,
getDetailResultsByTaskId,
} from '@/api/documentReview/DocumentTaskResults';
import { getDetailResultsByTaskId, ContractualTaskResultDownload } from '@/api/contractReview/ContractualTaskResults';
import { ref } from 'vue';
import ResultDetailDrawer from '@/views/documentReview/DocumentTasks/ResultDetailDrawer.vue';
import { DocumentTaskResultDetailVO } from '@/api/documentReview/DocumentTaskResults/model';
import ContractualResultDetailDrawer from './ContractualResultDetailDrawer.vue';
import { ContractualTaskResultDetailVO } from '@/api/contractReview/ContractualTaskResults/model';
const [registerDrawer, { openDrawer }] = useDrawer();
const resultDetailDrawerVisible = ref(false);
const taskResultDetail = ref<DocumentTaskResultDetailVO[]>([]);
const taskResultDetail = ref<ContractualTaskResultDetailVO[]>([]);
const currentTaskInfo = ref<Recordable>({});
defineOptions({ name: 'ContractualTasks' });
@ -163,10 +157,6 @@
const [registerModal, { openModal }] = useModal();
async function handleDetail(record: Recordable) {
try {
let res = await DocumentTaskResultsInfoByTaskId(record.id);
if (!res || !res.result) {
try {
const detailRes = await getDetailResultsByTaskId(record.id);
if (detailRes && detailRes.length > 0) {
@ -180,23 +170,6 @@
}
}
openDrawer(true, { value: res.result, type: 'markdown' });
} catch (ex) {
try {
const detailRes = await getDetailResultsByTaskId(record.id);
if (detailRes && detailRes.length > 0) {
taskResultDetail.value = detailRes;
currentTaskInfo.value = record;
resultDetailDrawerVisible.value = true;
return;
}
} catch (detailEx) {
console.error('获取详细结果也失败', detailEx);
openDrawer(true, { value: '加载失败,请刷新页面', type: 'markdown' });
}
}
}
function handleResultDetailDrawerClose() {
resultDetailDrawerVisible.value = false;
}
@ -212,7 +185,7 @@
async function handleDownload(record: Recordable) {
await DocumentTaskResultDownload([record.id]);
await ContractualTaskResultDownload([record.id]);
await reload();
}
</script>

32
src/views/documentReview/DocumentTasks/DocumentTasksTable.vue

@ -140,7 +140,6 @@
import { DocumentTasksList, DocumentTasksStop, DocumentTasksDeleteFile } from '@/api/documentReview/DocumentTasks';
import { getTasksPermissionsByUserId } from '@/api/taskPermissions/DocumentTasksPermissions';
import {
DocumentTaskResultsInfoByTaskId,
DocumentTaskResultDownload,
getDetailResultsByTaskId,
} from '@/api/documentReview/DocumentTaskResults';
@ -234,10 +233,6 @@
resultDetailDrawerVisible.value = false;
};
async function handleDetail(record: Recordable) {
try {
let res = await DocumentTaskResultsInfoByTaskId(record.id);
console.info("resresres",res,res.result,!res,!res.result)
if (!res || !res.result) {
try {
const detailRes = await getDetailResultsByTaskId(record.id);
if (detailRes && detailRes.length > 0) {
@ -251,33 +246,6 @@
}
}
if (record.taskName == 'schemEvaluation') {
const updatedHtmlText = res.result?.replace(
/文件名称:\S+/g,
`文件名称:${record.documentName}`,
);
openDrawer(true, { value: cleanHtml(updatedHtmlText), type: 'markdown' });
} else if (record.taskName == 'checkDocumentError') {
openDrawer(true, { value: res.result, type: 'markdown' });
} else {
openDrawer(true, { value: res.result, type: 'markdown' });
}
} catch (ex) {
try {
const detailRes = await getDetailResultsByTaskId(record.id);
if (detailRes && detailRes.length > 0) {
taskResultDetail.value = detailRes;
currentTaskInfo.value = record;
resultDetailDrawerVisible.value = true;
return;
}
} catch (detailEx) {
console.error('获取详细结果也失败', detailEx);
openDrawer(true, { value: '加载失败,请刷新页面', type: 'markdown' });
}
}
}
async function handleStop(record: Recordable) {
await DocumentTasksStop(record.id);
await reload();

13
src/views/documentReview/DocumentTasks/ResultDetailDrawer.vue

@ -240,18 +240,7 @@
//
const taskTypeContentConfig: Record<string, ContentSectionConfig[]> = {
//
'contract': [
{ field: 'originalText', title: '合同原文', type: 'markdown' },
{ field: 'legalIssues', title: '法律问题', type: 'markdown' },
{ field: 'suggestions', title: '修改建议', type: 'markdown' },
{
field: 'reviewBasis',
title: '法律依据',
type: 'reviewPointsList',
nestedFields: ['reviewContent', 'reviewPoints']
}
],
"checkCompanyName": [
{ field: 'modificationDisplay', title: '相关原文', type: 'markdown' },
],

29
src/views/schemeEvaluation/SchemeEvaluation/index.vue

@ -107,7 +107,6 @@
import { useDrawer } from '@/components/Drawer';
import { DocumentTasksStop } from '@/api/documentReview/DocumentTasks';
import {
DocumentTaskResultsInfoByTaskId,
DocumentTaskResultDownload,
} from '@/api/documentReview/DocumentTaskResults';
@ -159,21 +158,21 @@
return content.trim();
};
async function handleDetail(record: Recordable) {
try {
let res = await DocumentTaskResultsInfoByTaskId(record.id);
if (record.taskName == 'schemEvaluation') {
// res.result=generateTable(JSON.parse(res.result as string))
const updatedHtmlText = res.result?.replace(
/文件名称:\S+/g,
`文件名称:${record.documentName}`,
);
// try {
// let res = await DocumentTaskResultsInfoByTaskId(record.id);
// if (record.taskName == 'schemEvaluation') {
// // res.result=generateTable(JSON.parse(res.result as string))
// const updatedHtmlText = res.result?.replace(
// /\S+/g,
// `${record.documentName}`,
// );
openDrawer(true, { value: cleanHtml(updatedHtmlText), type: 'markdown' });
}
console.log('res', res);
} catch (ex) {
openDrawer(true, { value: '加载失败,请刷新页面', type: 'markdown' });
}
// openDrawer(true, { value: cleanHtml(updatedHtmlText), type: 'markdown' });
// }
// console.log('res', res);
// } catch (ex) {
// openDrawer(true, { value: '', type: 'markdown' });
// }
//record.id
}

15
src/views/tenderReview/TenderTask/index.vue

@ -112,7 +112,6 @@
import { useDrawer } from '@/components/Drawer';
import { DocumentTasksStop } from '@/api/documentReview/DocumentTasks';
import {
DocumentTaskResultsInfoByTaskId,
DocumentTaskResultDownload,
} from '@/api/documentReview/DocumentTaskResults';
const [registerDrawer, { openDrawer }] = useDrawer();
@ -149,14 +148,14 @@
const [registerModal, { openModal }] = useModal();
async function handleDetail(record: Recordable) {
try {
let res = await DocumentTaskResultsInfoByTaskId(record.id);
// try {
// let res = await DocumentTaskResultsInfoByTaskId(record.id);
openDrawer(true, { value: res.result, type: 'markdown' });
console.log('res', res);
} catch (ex) {
openDrawer(true, { value: '加载失败,请刷新页面', type: 'markdown' });
}
// openDrawer(true, { value: res.result, type: 'markdown' });
// console.log('res', res);
// } catch (ex) {
// openDrawer(true, { value: '', type: 'markdown' });
// }
//record.id
}

17
src/views/tenderReview/inconsistency/index.vue

@ -112,7 +112,6 @@
import { useDrawer } from '@/components/Drawer';
import { DocumentTasksStop } from '@/api/documentReview/DocumentTasks';
import {
DocumentTaskResultsInfoByTaskId,
DocumentTaskResultDownload,
} from '@/api/documentReview/DocumentTaskResults';
const [registerDrawer, { openDrawer }] = useDrawer();
@ -149,15 +148,15 @@
const [registerModal, { openModal }] = useModal();
async function handleDetail(record: Recordable) {
try {
let res = await DocumentTaskResultsInfoByTaskId(record.id);
// try {
// let res = await DocumentTaskResultsInfoByTaskId(record.id);
openDrawer(true, { value: res.result, type: 'markdown' });
console.log('res', res);
} catch (ex) {
openDrawer(true, { value: '加载失败,请刷新页面', type: 'markdown' });
}
//record.id
// openDrawer(true, { value: res.result, type: 'markdown' });
// console.log('res', res);
// } catch (ex) {
// openDrawer(true, { value: '', type: 'markdown' });
// }
// //record.id
}
async function handleStop(record: Recordable) {

Loading…
Cancel
Save