Browse Source

完成合同审查的优化和前端的改造

ai_dev
zhouhaibin 2 weeks ago
parent
commit
dde919231f
  1. 8
      src/api/contractReview/ContractualRegulationNames/index.ts
  2. 19
      src/api/contractReview/ContractualTaskResults/index.ts
  3. 62
      src/api/contractReview/ContractualTasks/model.ts
  4. 1
      src/components/Preview/index.ts
  5. 38
      src/components/Preview/src/PdfPreview/PageSelectModal.vue
  6. 499
      src/components/Preview/src/PdfPreview/PdfPreviewComponent.vue
  7. 247
      src/components/Preview/src/PdfPreview/ReviewPdfContainer.vue
  8. 2
      src/components/Preview/src/PdfPreview/index.ts
  9. 1344
      src/components/UniversalResultDrawer.vue
  10. 166
      src/configs/contractTaskConfigs.ts
  11. 229
      src/configs/documentTaskConfigs.ts
  12. 65
      src/configs/taskConfigTypes.ts
  13. 68
      src/configs/taskConfigs.ts
  14. 405
      src/configs/universalTaskConfigs.ts
  15. 1065
      src/views/contractReview/ContractualTasks/ContractualResultDetailDrawer.vue
  16. 62
      src/views/contractReview/ContractualTasks/ContractualTasks.data.ts
  17. 356
      src/views/contractReview/ContractualTasks/components/ComplianceContent.vue
  18. 4
      src/views/contractReview/ContractualTasks/components/ConsistencyContent.vue
  19. 14
      src/views/contractReview/ContractualTasks/components/InferenceReview.vue
  20. 190
      src/views/contractReview/ContractualTasks/configs/reviewTypeConfigs.ts
  21. 22
      src/views/contractReview/ContractualTasks/index.vue
  22. 20
      src/views/contractReview/ContractualTasks/list.vue
  23. 1002
      src/views/documentReview/DocumentTasks/ResultDetailDrawer.vue

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

@ -96,3 +96,11 @@ export function ContractualRegulationNamesViewPdf(id: ID) {
{ isReturnNativeResponse: true }
);
}
/**
*
* @returns
*/
export function ContractualRegulationNamesEffectiveList() {
return defHttp.get<ContractualRegulationNamesVO[]>({ url: '/productManagement/ContractualRegulationNames/effective/list' });
}

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

@ -119,6 +119,25 @@ export function getPdfStream(taskId: ID): Promise<Blob> {
).then(response => response.data);
}
/**
* PDF文件流
* @param taskId ID
* @returns Promise<Blob>
*/
export function getBidPdfStream(taskId: ID): Promise<Blob> {
return defHttp.get(
{
url: `/productManagement/ContractualTaskResults/getBidPdfStream/${taskId}`,
responseType: 'blob',
timeout:600000
},
{
isReturnNativeResponse: true,
errorMessageMode: 'none',
}
).then(response => response.data);
}
/**
* /
* @param id ID

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

@ -6,16 +6,6 @@ export interface ContractualTasksVO {
*/
id: string | number;
/**
*
*/
taskIndustry: string;
/**
*
*/
taskRegion: string;
/**
*
*/
@ -36,6 +26,11 @@ export interface ContractualTasksVO {
*/
progressStatus: string;
/**
*
*/
reviewTypes: string;
}
export interface ContractualTasksForm extends BaseEntity {
@ -44,16 +39,6 @@ export interface ContractualTasksForm extends BaseEntity {
*/
id?: string | number;
/**
*
*/
taskIndustry?: string;
/**
*
*/
taskRegion?: string;
/**
*
*/
@ -74,19 +59,14 @@ export interface ContractualTasksForm extends BaseEntity {
*/
progressStatus?: string;
}
export interface ContractualTasksQuery extends PageQuery {
/**
*
*
*/
taskIndustry?: string;
reviewTypes?: string;
/**
*
*/
taskRegion?: string;
}
export interface ContractualTasksQuery extends PageQuery {
/**
*
@ -108,6 +88,11 @@ export interface ContractualTasksQuery extends PageQuery {
*/
progressStatus?: string;
/**
*
*/
reviewTypes?: string;
/**
*
*/
@ -175,24 +160,19 @@ export interface SubstantiveData {
*/
export interface ComplianceData {
/**
*
*/
focusPoints?: string[];
/**
*
* ai: AI自动选择, manual: 人工选择法规
*/
industry?: string;
regulationMethod?: string;
/**
*
* ID列表regulationMethod为manual时使用
*/
level?: string;
regulationIds?: string[];
/**
*
*
*/
regulations?: string[];
specialNote?: string;
/**
*

1
src/components/Preview/index.ts

@ -1,2 +1,3 @@
export { default as ImagePreview } from './src/Preview.vue';
export { createImgPreview } from './src/functional';
export { PdfPreviewComponent, PageSelectModal } from './src/PdfPreview';

38
src/views/documentReview/DocumentTasks/PageSelectModal.vue → src/components/Preview/src/PdfPreview/PageSelectModal.vue

@ -2,11 +2,11 @@
<Modal
:visible="visible"
title="请选择要跳转的页码"
@cancel="onClose"
@cancel="handleClose"
:footer="null"
width="400"
maskClosable
closable
:maskClosable="true"
:closable="true"
>
<p>该文本在以下页面出现</p>
<div style="display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px;">
@ -27,19 +27,31 @@
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
import { Modal, Button } from 'ant-design-vue';
const props = defineProps({
visible: Boolean,
pages: {
type: Array as () => number[],
default: () => []
},
onSelect: Function,
onClose: Function
interface PageSelectModalProps {
visible: boolean;
pages: number[];
onSelect?: (page: number) => void;
onClose?: () => void;
}
const props = withDefaults(defineProps<PageSelectModalProps>(), {
visible: false,
pages: () => [],
});
const emit = defineEmits(['select', 'close']);
const emit = defineEmits<{
select: [page: number];
close: [];
}>();
function handleSelect(page: number) {
props.onSelect && props.onSelect(page);
props.onSelect?.(page);
emit('select', page);
}
function handleClose() {
props.onClose?.();
emit('close');
}
</script>

499
src/components/Preview/src/PdfPreview/PdfPreviewComponent.vue

@ -0,0 +1,499 @@
<template>
<div class="pdf-preview">
<div class="pdf-container" ref="pdfContainer">
<div v-if="loading" class="loading-overlay">
<Spin size="large" :tip="loadingTip" />
</div>
<canvas ref="pdfCanvas" :style="{ opacity: loading ? 0.3 : 1 }"></canvas>
</div>
<div class="pdf-controls">
<Button @click="zoomOut" :disabled="scale <= 0.5 || loading">-</Button>
<Button @click="prevPage" :disabled="currentPage <= 1 || loading" class="ml-2">
<LeftOutlined />
</Button>
<span class="page-info">{{ currentPage }} / {{ totalPages }}</span>
<Button @click="nextPage" :disabled="currentPage >= totalPages || loading" class="ml-2">
<RightOutlined />
</Button>
<Button @click="zoomIn" :disabled="scale >= 3 || loading" class="ml-2">+</Button>
</div>
<!-- 页面选择模态框 -->
<PageSelectModal
:visible="showPageSelectModal"
:pages="matchedPages"
:onSelect="handlePageSelect"
:onClose="handlePageModalClose"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, onBeforeUnmount, nextTick } from 'vue';
import { Button, Spin } from 'ant-design-vue';
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api';
import PageSelectModal from './PageSelectModal.vue';
// PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js';
interface PdfPreviewProps {
pdfData?: Blob | null;
taskId?: string;
getPdfStream?: (taskId: string) => Promise<Blob>;
}
interface PdfLoadedData {
totalPages: number;
}
interface PdfPreviewInstance {
locateByText: (text: string) => Promise<void>;
locateByTextAndPage: (text: string, pageNumber: number) => Promise<void>;
loadPdf: (data?: Blob | string) => Promise<void>;
renderPage: (num: number, highlightStr?: string) => Promise<void>;
prevPage: () => Promise<void>;
nextPage: () => Promise<void>;
zoomIn: () => void;
zoomOut: () => void;
}
const props = withDefaults(defineProps<PdfPreviewProps>(), {
pdfData: null,
taskId: '',
getPdfStream: undefined
});
const emit = defineEmits<{
pdfLoaded: [data: PdfLoadedData];
pdfError: [error: any];
}>();
const pdfContainer = ref<HTMLElement | null>(null);
const pdfCanvas = ref<HTMLCanvasElement | null>(null);
let pdfDoc: PDFDocumentProxy | null = null;
const currentPage = ref(1);
const totalPages = ref(0);
const scale = ref(1);
const showPageSelectModal = ref(false);
const matchedPages = ref<number[]>([]);
const highlightText = ref<string | null>(null);
// Loading
const loading = ref(false);
const loadingTip = ref('');
let currentRenderTask: any = null; //
// PDF
const loadPdf = async (data?: Blob | string) => {
loading.value = true;
loadingTip.value = '正在加载PDF文件...';
try {
let pdfBlob: Blob;
if (data instanceof Blob) {
pdfBlob = data;
} else if (typeof data === 'string' && props.getPdfStream) {
loadingTip.value = '正在获取PDF数据...';
pdfBlob = await props.getPdfStream(data);
} else if (props.taskId && props.getPdfStream) {
loadingTip.value = '正在获取PDF数据...';
pdfBlob = await props.getPdfStream(props.taskId);
} else {
throw new Error('无效的PDF数据源');
}
loadingTip.value = '正在解析PDF文件...';
const arrayBuffer = await pdfBlob.arrayBuffer();
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
const doc = await loadingTask.promise;
if (doc) {
pdfDoc = doc;
totalPages.value = doc.numPages;
currentPage.value = 1;
await nextTick();
loadingTip.value = '正在渲染第一页...';
await renderPage(1);
emit('pdfLoaded', { totalPages: doc.numPages });
}
} catch (error) {
console.error('加载PDF失败:', error);
message.error('加载PDF失败');
emit('pdfError', error);
} finally {
loading.value = false;
loadingTip.value = '';
}
};
//
const renderPage = async (num: number, highlightStr?: string) => {
if (!pdfDoc || !pdfCanvas.value || !pdfContainer.value) return;
try {
const page = await pdfDoc.getPage(num);
const canvas = pdfCanvas.value;
const ctx = canvas.getContext('2d');
if (!ctx) return;
//
const containerWidth = pdfContainer.value.clientWidth || 800;
const baseViewport = page.getViewport({ scale: 1 });
const scaleRatio = ((containerWidth - 48) / baseViewport.width) * scale.value;
const viewport = page.getViewport({ scale: scaleRatio });
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.style.width = `${viewport.width}px`;
canvas.style.height = `${viewport.height}px`;
//
if (currentRenderTask) {
try { currentRenderTask.cancel(); } catch (e) {}
}
const renderContext = {
canvasContext: ctx,
viewport: viewport,
};
currentRenderTask = page.render(renderContext);
await currentRenderTask.promise;
currentRenderTask = null;
//
if (highlightStr) {
await highlightTextOnPage(page, ctx, viewport, highlightStr);
}
} catch (error) {
currentRenderTask = null;
// RenderingCancelledException
if (error && error.name === 'RenderingCancelledException') {
return;
}
console.error('渲染PDF页面失败:', error);
message.error('渲染PDF页面失败');
}
};
//
const prevPage = async () => {
if (currentPage.value > 1 && !loading.value) {
currentPage.value--;
await renderPage(currentPage.value);
}
};
//
const nextPage = async () => {
if (currentPage.value < totalPages.value && !loading.value) {
currentPage.value++;
await renderPage(currentPage.value);
}
};
//
const zoomIn = () => {
if (scale.value < 3 && !loading.value) {
scale.value += 0.1;
renderPage(currentPage.value);
}
};
//
const zoomOut = () => {
if (scale.value > 0.5 && !loading.value) {
scale.value -= 0.1;
renderPage(currentPage.value);
}
};
//
const locateByText = async (text: string) => {
if (!pdfDoc || !text || loading.value) return;
loading.value = true;
loadingTip.value = '正在搜索文本...';
try {
// Markdown**
const cleanText = text.replace(/\*\*/g, '');
const numPages = pdfDoc.numPages;
const foundPages: number[] = [];
//
for (let i = 1; i <= numPages; i++) {
loadingTip.value = `正在搜索第${i}/${numPages}页...`;
const page = await pdfDoc.getPage(i);
const content = await page.getTextContent();
const pageText = content.items.map((item: any) => item.str).join('');
//
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) {
loadingTip.value = `正在跳转到第${foundPages[0]}页...`;
currentPage.value = foundPages[0];
await renderPage(foundPages[0], cleanText);
message.success(`已定位到第${foundPages[0]}`);
return;
}
//
matchedPages.value = foundPages;
showPageSelectModal.value = true;
} catch (error) {
console.error('文本定位失败:', error);
message.error('文本定位失败');
} finally {
loading.value = false;
loadingTip.value = '';
}
};
//
const locateByTextAndPage = async (text: string, pageNumber: number) => {
if (!pdfDoc || !text || loading.value || !pageNumber) return;
loading.value = true;
loadingTip.value = `正在定位到第${pageNumber}页...`;
try {
// Markdown**
const cleanText = text.replace(/\*\*/g, '');
//
if (pageNumber < 1 || pageNumber > totalPages.value) {
message.warning(`无效的页码: ${pageNumber}`);
return;
}
//
currentPage.value = pageNumber;
await renderPage(pageNumber, cleanText);
message.success(`已定位到第${pageNumber}`);
} catch (error) {
console.error('页面定位失败:', error);
message.error('页面定位失败');
} finally {
loading.value = false;
loadingTip.value = '';
}
};
//
const handlePageSelect = async (page: number) => {
loading.value = true;
loadingTip.value = `正在跳转到第${page}页...`;
try {
currentPage.value = page;
// highlightText.valuenull
await renderPage(page, highlightText.value || undefined);
showPageSelectModal.value = false;
message.success(`已定位到第${page}`);
} catch (error) {
console.error('页面跳转失败:', error);
message.error('页面跳转失败');
} finally {
loading.value = false;
loadingTip.value = '';
}
};
const handlePageModalClose = () => {
showPageSelectModal.value = false;
};
//
async function highlightTextOnPage(page: any, ctx: any, viewport: any, targetText: string) {
if (!targetText) return;
// **
const cleanTargetText = targetText.replace(/\*\*/g, '');
const textContent = await page.getTextContent();
const items = textContent.items as any[];
//
const pageTextWithSpaces = items.map((i: any) => 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();
}
// props
watch(() => props.pdfData, (newData) => {
if (newData) {
loadPdf(newData);
}
}, { immediate: true });
watch(() => props.taskId, (newTaskId) => {
if (newTaskId && props.getPdfStream) {
loadPdf(newTaskId);
}
}, { immediate: true });
//
onBeforeUnmount(() => {
if (pdfDoc) {
pdfDoc.destroy();
}
});
//
defineExpose<PdfPreviewInstance>({
locateByText,
locateByTextAndPage,
loadPdf,
renderPage,
prevPage,
nextPage,
zoomIn,
zoomOut
});
</script>
<style lang="less" scoped>
.pdf-preview {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: #f5f5f5;
position: relative;
}
.pdf-container {
flex: 1;
overflow: auto;
display: block;
padding: 24px 0;
background: #f5f5f5;
position: relative;
canvas {
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
border-radius: 6px;
display: block;
transition: opacity 0.3s;
}
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.8);
z-index: 10;
:deep(.ant-spin) {
.ant-spin-text {
color: #666;
margin-top: 8px;
}
}
}
.pdf-controls {
padding: 8px;
display: flex;
justify-content: center;
align-items: center;
background: white;
border-top: 1px solid #e8e8e8;
.page-info {
margin: 0 16px;
color: rgba(0, 0, 0, 0.65);
}
}
.ml-2 {
margin-left: 8px;
}
</style>

247
src/components/Preview/src/PdfPreview/ReviewPdfContainer.vue

@ -0,0 +1,247 @@
<template>
<div class="review-pdf-container">
<!-- 单PDF布局 -->
<div v-if="pdfLayout === 'single'" class="single-pdf-layout">
<PdfPreviewComponent
ref="singlePdfRef"
:taskId="pdfSources[0]?.taskId"
:getPdfStream="getPdfStreamBySource(pdfSources[0]?.id)"
@pdfLoaded="handlePdfLoaded"
@pdfError="handlePdfError"
/>
</div>
<!-- 双PDF布局 -->
<div v-else-if="pdfLayout === 'split'" class="split-pdf-layout">
<!-- 调试信息 -->
<div v-if="pdfSources.length === 0" class="debug-info">
<p> 没有PDF源配置</p>
<p>配置: {{ config }}</p>
</div>
<!-- 强制显示两个PDF面板用于测试 -->
<div v-if="pdfSources.length < 2" class="debug-info">
<p> PDF源数量不足: {{ pdfSources.length }}/2</p>
<p>当前源: {{ pdfSources.map(s => s.id).join(', ') }}</p>
</div>
<div class="pdf-panel" v-for="(source, index) in pdfSources" :key="source.id">
<div class="pdf-panel-header">
<span class="pdf-title">{{ source.title }} ({{ source.id }})</span>
<!-- 临时调试信息 -->
<span class="debug-text">TaskId: {{ source.taskId }}</span>
</div>
<div class="pdf-panel-content">
<PdfPreviewComponent
:ref="el => setPdfRef(source.id, el)"
:taskId="source.taskId"
:getPdfStream="getPdfStreamBySource(source.id)"
@pdfLoaded="(data) => handlePdfLoaded(data, source.id)"
@pdfError="(error) => handlePdfError(error, source.id)"
/>
</div>
</div>
</div>
<!-- 调试信息 -->
<div v-else class="debug-info">
<p> 未知的PDF布局类型: {{ pdfLayout }}</p>
<p>配置: {{ config }}</p>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import { PdfPreviewComponent } from '@/components/Preview';
import { getBidPdfStream } from '@/api/contractReview/ContractualTaskResults';
interface Props {
config: any;
taskInfo: any;
getPdfStream: (taskId: string) => Promise<Blob>;
}
const props = defineProps<Props>();
const emit = defineEmits(['pdfLoaded', 'pdfError']);
// PDF
const pdfLayout = computed(() => props.config.pdfLayout);
// PDF
const pdfSources = computed(() => {
const sources = props.config.pdfSources.map(source => ({
...source,
taskId: props.taskInfo[source.apiField] || props.taskInfo.id
}));
console.log('PDF Layout:', pdfLayout.value);
console.log('PDF Sources:', sources);
console.log('Task Info:', props.taskInfo);
return sources;
});
// PDF
const singlePdfRef = ref<InstanceType<typeof PdfPreviewComponent> | null>(null);
const pdfRefs = ref<Record<string, InstanceType<typeof PdfPreviewComponent> | null>>({});
// PDF
const setPdfRef = (sourceId: string, el: any) => {
if (el) {
pdfRefs.value[sourceId] = el;
}
};
// PDFgetPdfStream
const getPdfStreamBySource = (sourceId: string) => {
console.log('Getting PDF stream for source:', sourceId);
if (sourceId === 'bid') {
// 使API
return (taskId: string) => {
console.log('Using getBidPdfStream for taskId:', taskId);
return getBidPdfStream(taskId);
};
} else {
// 使API
return (taskId: string) => {
console.log('Using default getPdfStream for taskId:', taskId);
return props.getPdfStream(taskId);
};
}
};
// PDF
const handlePdfLoaded = (data: any, sourceId?: string) => {
console.log(`PDF loaded for ${sourceId || 'single'}:`, data);
emit('pdfLoaded', { data, sourceId });
};
// PDF
const handlePdfError = (error: any, sourceId?: string) => {
console.error(`PDF load error for ${sourceId || 'single'}:`, error);
emit('pdfError', { error, sourceId });
};
//
const locateByText = async (text: string, pdfSource?: string) => {
if (pdfLayout.value === 'single') {
// PDF
if (singlePdfRef.value) {
await singlePdfRef.value.locateByText(text);
}
} else {
// PDF
if (pdfSource && pdfRefs.value[pdfSource]) {
await pdfRefs.value[pdfSource]?.locateByText(text);
} else {
// PDFPDF
const firstPdfRef = Object.values(pdfRefs.value)[0];
if (firstPdfRef) {
await firstPdfRef.locateByText(text);
}
}
}
};
//
const locateByTextAndPage = async (text: string, page: number, pdfSource?: string) => {
if (pdfLayout.value === 'single') {
// PDF
if (singlePdfRef.value) {
await singlePdfRef.value.locateByTextAndPage(text, page);
}
} else {
// PDF
if (pdfSource && pdfRefs.value[pdfSource]) {
await pdfRefs.value[pdfSource]?.locateByTextAndPage(text, page);
} else {
// PDFPDF
const firstPdfRef = Object.values(pdfRefs.value)[0];
if (firstPdfRef) {
await firstPdfRef.locateByTextAndPage(text, page);
}
}
}
};
//
onMounted(() => {
console.log('=== ReviewPdfContainer Debug Info ===');
console.log('Config:', props.config);
console.log('PDF Layout:', pdfLayout.value);
console.log('PDF Sources Count:', props.config.pdfSources.length);
console.log('PDF Sources:', props.config.pdfSources);
console.log('Task Info:', props.taskInfo);
console.log('Computed PDF Sources:', pdfSources.value);
console.log('=====================================');
});
//
defineExpose({
locateByText,
locateByTextAndPage,
pdfRefs,
singlePdfRef
});
</script>
<style lang="less" scoped>
.review-pdf-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.single-pdf-layout {
width: 100%;
height: 100%;
}
.split-pdf-layout {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
}
.pdf-panel {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #e8e8e8;
&:last-child {
border-right: none;
}
}
.pdf-panel-header {
padding: 8px 16px;
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
flex-shrink: 0;
}
.pdf-title {
font-weight: 500;
color: #333;
font-size: 14px;
}
.pdf-panel-content {
flex: 1;
overflow: hidden;
}
.debug-info {
padding: 16px;
background: #f0f0f0;
border-top: 1px solid #e8e8e8;
text-align: center;
}
.debug-text {
font-size: 12px;
color: #999;
}
</style>

2
src/components/Preview/src/PdfPreview/index.ts

@ -0,0 +1,2 @@
export { default as PdfPreviewComponent } from './PdfPreviewComponent.vue';
export { default as PageSelectModal } from './PageSelectModal.vue';

1344
src/components/UniversalResultDrawer.vue

File diff suppressed because it is too large

166
src/configs/contractTaskConfigs.ts

@ -0,0 +1,166 @@
// 合同审核任务配置文件
import type { TaskConfig } from './taskConfigTypes';
// 合同审核任务配置
export const CONTRACT_TASK_CONFIGS: Record<string, TaskConfig> = {
// 合同审核(多标签模式)
"contractReview": {
taskType: "contractReview",
name: "合同审核",
mode: "tabs",
pdfConfig: {
layout: "split",
sources: [
{ id: "contract", title: "合同文件", apiField: "id" },
{ id: "bid", title: "招标文件", apiField: "id" }
]
},
tabs: [
{
key: "substantive",
label: "实质性审查",
pdfConfig: {
layout: "single",
sources: [
{ id: "contract", title: "合同文件", apiField: "id" }
]
},
fields: [
{
field: 'originalText',
title: '原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'contract',
required: true
},
{
field: 'modifiedContent',
title: '修改建议',
dataType: 'string',
displayType: 'markdown',
showComparison: true,
comparisonConfig: {
comparisonField: 'modificationDisplay',
comparisonTitle: '修改情况展示',
showButton: true
}
},
{
field: 'reviewBasis',
title: '审查依据',
dataType: 'json',
displayType: 'markdown',
jsonConfig: {
extractFields: ['review_points','review_content'],
separator: ':'
}
}
]
},
{
key: "compliance",
label: "合规性审查",
pdfConfig: {
layout: "single",
sources: [
{ id: "contract", title: "合同文件", apiField: "id" }
]
},
fields: [
{
field: 'originalText',
title: '原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'contract',
required: true
},
// {
// field: 'existingIssues',
// title: '合规性问题',
// dataType: 'string',
// displayType: 'markdown'
// },
// {
// field: 'modifiedContent',
// title: '修改建议',
// dataType: 'string',
// displayType: 'markdown',
// showComparison: true,
// comparisonConfig: {
// comparisonField: 'modificationDisplay',
// comparisonTitle: '修改情况展示',
// showButton: true
// }
// },
{
field: 'reviewBasis',
title: '法规依据',
dataType: 'json',
displayType: 'markdown',
jsonConfig: {
extractFields: ['review_points'],
separator: ':',
fieldProcessors: {
'review_points': (value) => Array.isArray(value) ? value.join('\n') : value
}
}
}
]
},
{
key: "consistency",
label: "一致性审查",
pdfConfig: {
layout: "split",
sources: [
{ id: "contract", title: "合同文件", apiField: "id" },
{ id: "bid", title: "招标文件", apiField: "id" }
]
},
fields: [
{
field: 'originalText',
title: '合同原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'contract',
required: true
},
{
field: 'comparedText',
title: '招标文件对应内容',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'bid',
required: true
},
// {
// field: 'modifiedContent',
// title: '修改建议',
// dataType: 'string',
// displayType: 'markdown',
// showComparison: true,
// comparisonConfig: {
// comparisonField: 'modificationDisplay',
// comparisonTitle: '修改情况展示',
// showButton: true
// }
// }
]
}
]
}
};
// 获取合同任务配置
export function getContractTaskConfig(taskType: string): TaskConfig | null {
return CONTRACT_TASK_CONFIGS[taskType] || null;
}
// 获取所有合同任务配置
export function getAllContractTaskConfigs(): TaskConfig[] {
return Object.values(CONTRACT_TASK_CONFIGS);
}

229
src/configs/documentTaskConfigs.ts

@ -0,0 +1,229 @@
// 文档审核任务配置文件
import type { TaskConfig } from './taskConfigTypes';
// 文档审核任务配置
export const DOCUMENT_TASK_CONFIGS: Record<string, TaskConfig> = {
// 文档错误检查
"checkDocumentError": {
taskType: "checkDocumentError",
name: "文档错误检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'originalText',
title: '原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document',
required: true
},
{
field: 'modifiedContent',
title: '修改建议',
dataType: 'string',
displayType: 'markdown',
showComparison: true,
comparisonConfig: {
comparisonField: 'modificationDisplay',
comparisonTitle: '修改情况展示',
showButton: true
}
},
// {
// field: 'modificationDisplay',
// title: '修改情况',
// dataType: 'string',
// displayType: 'markdown'
// }
]
},
// 重复文本检查
"checkRepeatText": {
taskType: "checkRepeatText",
name: "重复文本检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'originalText',
title: '第一段原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
},
{
field: 'comparedText',
title: '第二段原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
},
{
field: 'modificationDisplay',
title: '相似情况',
dataType: 'string',
displayType: 'markdown'
}
]
},
// 全文重复检查
"allCheckRepeatText": {
taskType: "allCheckRepeatText",
name: "全文重复检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'originalText',
title: '第一段原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
},
{
field: 'comparedText',
title: '第二段原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
},
{
field: 'modificationDisplay',
title: '相似情况',
dataType: 'string',
displayType: 'markdown'
}
]
},
// 公司名称检查
"checkCompanyName": {
taskType: "checkCompanyName",
name: "公司名称检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'modificationDisplay',
title: '相关原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
}
]
},
// 标题名称检查
"checkTitleName": {
taskType: "checkTitleName",
name: "标题名称检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'modificationDisplay',
title: '相关原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
}
]
},
// 地名检查
"checkPlaceName": {
taskType: "checkPlaceName",
name: "地名检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'modificationDisplay',
title: '相关原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
}
]
},
// 政策依据检查
"policyBases": {
taskType: "policyBases",
name: "政策依据检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'originalText',
title: '系统名称',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document',
},
{
field: 'reviewBasis',
title: '系统描述',
dataType: 'json',
displayType: 'markdown',
jsonConfig: {
extractFields: ['review_points', 'review_content'],
separator: ':',
fieldProcessors: {
'review_points': (value) => Array.isArray(value) ? value[0] : value
}
}
}
]
}
};
// 获取文档任务配置
export function getDocumentTaskConfig(taskType: string): TaskConfig | null {
return DOCUMENT_TASK_CONFIGS[taskType] || null;
}
// 获取所有文档任务配置
export function getAllDocumentTaskConfigs(): TaskConfig[] {
return Object.values(DOCUMENT_TASK_CONFIGS);
}

65
src/configs/taskConfigTypes.ts

@ -0,0 +1,65 @@
// 任务配置类型定义
// 基础接口定义
export interface FieldConfig {
field: string; // 数据字段名
title: string; // 显示标题
dataType: 'string' | 'json' | 'number' | 'array'; // 数据类型
displayType: 'markdown' | 'text' | 'html'; // 前端展示类型
required?: boolean;
pdfSource?: 'contract' | 'bid' | 'document' | 'auto'; // PDF源
displayCondition?: (item: any) => boolean;
showComparison?: boolean; // 显示对比按钮
// 对比配置
comparisonConfig?: {
comparisonField: string; // 对比数据字段名,默认为 modificationDisplay
comparisonTitle?: string; // 对比内容的标题,默认为 '修改情况展示'
showButton?: boolean; // 是否显示对比按钮,默认为 true
};
// JSON数据类型的配置
jsonConfig?: {
extractFields: string[];
separator?: string;
fieldLabels?: Record<string, string>;
fieldProcessors?: Record<string, (value: any) => string>;
};
}
export interface PdfSourceConfig {
id: string;
title: string;
apiField: string;
}
export interface PdfConfig {
layout: 'single' | 'split';
sources: PdfSourceConfig[];
}
export interface TabConfig {
key: string;
label: string;
fields: FieldConfig[];
displayCondition?: (data: any) => boolean;
pdfConfig?: PdfConfig; // 每个tab可以有自己的PDF配置
}
export interface TaskConfig {
taskType: string; // 任务类型标识
name: string; // 任务显示名称
mode: 'single' | 'tabs'; // 展示模式:单一内容或多标签
// PDF配置
pdfConfig?: PdfConfig;
// 单一模式配置
fields?: FieldConfig[];
// 多标签模式配置
tabs?: TabConfig[];
// 全局显示条件
displayCondition?: (taskInfo: any, data: any) => boolean;
}

68
src/configs/taskConfigs.ts

@ -0,0 +1,68 @@
// 统一任务配置入口文件
import type { TaskConfig, FieldConfig } from './taskConfigTypes';
import { DOCUMENT_TASK_CONFIGS, getDocumentTaskConfig } from './documentTaskConfigs';
import { CONTRACT_TASK_CONFIGS, getContractTaskConfig } from './contractTaskConfigs';
// 合并所有任务配置
const ALL_TASK_CONFIGS: Record<string, TaskConfig> = {
...DOCUMENT_TASK_CONFIGS,
...CONTRACT_TASK_CONFIGS
};
// 统一的获取任务配置函数
export function getTaskConfig(taskType: string): TaskConfig | null {
// 首先尝试从文档任务配置中获取
let config = getDocumentTaskConfig(taskType);
if (config) return config;
// 然后尝试从合同任务配置中获取
config = getContractTaskConfig(taskType);
if (config) return config;
return null;
}
// 获取所有可用配置
export function getAvailableConfigs(): TaskConfig[] {
return Object.values(ALL_TASK_CONFIGS);
}
// 检查字段是否应该显示对比按钮
export function shouldShowComparison(fieldConfig: FieldConfig, item: any): boolean {
if (!fieldConfig.showComparison) return false;
// 获取对比字段名,默认为 modificationDisplay
const comparisonField = fieldConfig.comparisonConfig?.comparisonField || 'modificationDisplay';
// 检查对比字段是否有内容
return !!(item[comparisonField] && item[comparisonField].trim());
}
// 获取对比字段名
export function getComparisonField(fieldConfig: FieldConfig): string {
return fieldConfig.comparisonConfig?.comparisonField || 'modificationDisplay';
}
// 获取对比标题
export function getComparisonTitle(fieldConfig: FieldConfig): string {
return fieldConfig.comparisonConfig?.comparisonTitle || '修改情况展示';
}
// 检查是否应该显示对比按钮
export function shouldShowComparisonButton(fieldConfig: FieldConfig): boolean {
return fieldConfig.comparisonConfig?.showButton !== false; // 默认为true
}
// 获取字段的PDF源
export function getFieldPdfSource(fieldConfig: FieldConfig): string | null {
return fieldConfig.pdfSource || null;
}
// 检查字段是否支持PDF定位
export function supportsPdfLocation(fieldConfig: FieldConfig): boolean {
return !!(fieldConfig.pdfSource && fieldConfig.pdfSource !== 'auto');
}
// 重新导出类型
export type { TaskConfig, FieldConfig, TabConfig, PdfSourceConfig } from './taskConfigTypes';

405
src/configs/universalTaskConfigs.ts

@ -0,0 +1,405 @@
// 通用任务配置文件
// 基础接口定义
export interface FieldConfig {
field: string; // 数据字段名
title: string; // 显示标题
dataType: 'string' | 'json' | 'number' | 'array'; // 数据类型
displayType: 'markdown' | 'text' | 'html'; // 前端展示类型
required?: boolean;
pdfSource?: 'contract' | 'bid' | 'document' | 'auto'; // PDF源
displayCondition?: (item: any) => boolean;
showComparison?: boolean; // 显示对比按钮
// JSON数据类型的配置
jsonConfig?: {
extractFields: string[];
separator?: string;
fieldLabels?: Record<string, string>;
fieldProcessors?: Record<string, (value: any) => string>;
};
}
export interface PdfSourceConfig {
id: string;
title: string;
apiField: string;
}
export interface TabConfig {
key: string;
label: string;
fields: FieldConfig[];
displayCondition?: (data: any) => boolean;
}
export interface TaskConfig {
taskType: string; // 任务类型标识
name: string; // 任务显示名称
mode: 'single' | 'tabs'; // 展示模式:单一内容或多标签
// PDF配置
pdfConfig?: {
layout: 'single' | 'split';
sources: PdfSourceConfig[];
};
// 单一模式配置
fields?: FieldConfig[];
// 多标签模式配置
tabs?: TabConfig[];
// 全局显示条件
displayCondition?: (taskInfo: any, data: any) => boolean;
}
// 配置定义
export const UNIVERSAL_TASK_CONFIGS: Record<string, TaskConfig> = {
// ==================== 文档审核任务配置 ====================
// 文档错误检查
"checkDocumentError": {
taskType: "checkDocumentError",
name: "文档错误检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'originalText',
title: '原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document',
required: true
},
{
field: 'modifiedContent',
title: '修改建议',
dataType: 'string',
displayType: 'markdown',
showComparison: true
},
{
field: 'modificationDisplay',
title: '修改情况',
dataType: 'string',
displayType: 'markdown'
}
]
},
// 重复文本检查
"checkRepeatText": {
taskType: "checkRepeatText",
name: "重复文本检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'originalText',
title: '第一段原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
},
{
field: 'comparedText',
title: '第二段原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
},
{
field: 'modificationDisplay',
title: '相似情况',
dataType: 'string',
displayType: 'markdown'
}
]
},
// 全文重复检查
"allCheckRepeatText": {
taskType: "allCheckRepeatText",
name: "全文重复检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'originalText',
title: '第一段原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
},
{
field: 'comparedText',
title: '第二段原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
},
{
field: 'modificationDisplay',
title: '相似情况',
dataType: 'string',
displayType: 'markdown'
}
]
},
// 公司名称检查
"checkCompanyName": {
taskType: "checkCompanyName",
name: "公司名称检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'modificationDisplay',
title: '相关原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
}
]
},
// 标题名称检查
"checkTitleName": {
taskType: "checkTitleName",
name: "标题名称检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'modificationDisplay',
title: '相关原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
}
]
},
// 地名检查
"checkPlaceName": {
taskType: "checkPlaceName",
name: "地名检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'modificationDisplay',
title: '相关原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'document'
}
]
},
// 政策依据检查
"policyBases": {
taskType: "policyBases",
name: "政策依据检查",
mode: "single",
pdfConfig: {
layout: "single",
sources: [
{ id: "document", title: "文档", apiField: "id" }
]
},
fields: [
{
field: 'originalText',
title: '系统名称',
dataType: 'string',
displayType: 'markdown'
},
{
field: 'reviewBasis',
title: '系统描述',
dataType: 'json',
displayType: 'markdown',
jsonConfig: {
extractFields: ['reviewPoints', 'review_content'],
separator: ':',
fieldProcessors: {
'reviewPoints': (value) => Array.isArray(value) ? value[0] : value
}
}
}
]
},
// ==================== 合同审核任务配置 ====================
// 合同审核(多标签模式)
"contractReview": {
taskType: "contractReview",
name: "合同审核",
mode: "tabs",
pdfConfig: {
layout: "split",
sources: [
{ id: "contract", title: "合同文件", apiField: "id" },
{ id: "bid", title: "招标文件", apiField: "id" }
]
},
tabs: [
{
key: "substantive",
label: "实质性审查",
fields: [
{
field: 'originalText',
title: '原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'contract',
required: true
},
{
field: 'modifiedContent',
title: '修改建议',
dataType: 'string',
displayType: 'markdown',
showComparison: true
},
{
field: 'reviewBasis',
title: '审查依据',
dataType: 'json',
displayType: 'markdown',
jsonConfig: {
extractFields: ['reviewContent', 'reviewPoints'],
separator: ':'
}
}
]
},
{
key: "compliance",
label: "合规性审查",
fields: [
{
field: 'originalText',
title: '原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'contract',
required: true
},
{
field: 'existingIssues',
title: '合规性问题',
dataType: 'string',
displayType: 'markdown'
},
{
field: 'modifiedContent',
title: '修改建议',
dataType: 'string',
displayType: 'markdown',
showComparison: true
},
{
field: 'reviewBasis',
title: '法规依据',
dataType: 'json',
displayType: 'markdown',
jsonConfig: {
extractFields: ['reviewContent', 'reviewPoints'],
separator: ':'
}
}
]
},
{
key: "consistency",
label: "一致性审查",
fields: [
{
field: 'originalText',
title: '合同原文',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'contract',
required: true
},
{
field: 'comparedText',
title: '招标文件对应内容',
dataType: 'string',
displayType: 'markdown',
pdfSource: 'bid',
required: true
},
{
field: 'modifiedContent',
title: '修改建议',
dataType: 'string',
displayType: 'markdown',
showComparison: true
}
]
}
]
}
};
// 工具函数
export function getTaskConfig(taskType: string): TaskConfig | null {
return UNIVERSAL_TASK_CONFIGS[taskType] || null;
}
export function getAvailableConfigs(): TaskConfig[] {
return Object.values(UNIVERSAL_TASK_CONFIGS);
}
// 检查字段是否应该显示对比按钮
export function shouldShowComparison(fieldConfig: FieldConfig, item: any): boolean {
return !!(fieldConfig.showComparison && item.modificationDisplay && item.modificationDisplay.trim());
}
// 获取字段的PDF源
export function getFieldPdfSource(fieldConfig: FieldConfig): string | null {
return fieldConfig.pdfSource || null;
}
// 检查字段是否支持PDF定位
export function supportsPdfLocation(fieldConfig: FieldConfig): boolean {
return !!(fieldConfig.pdfSource && fieldConfig.pdfSource !== 'auto');
}

1065
src/views/contractReview/ContractualTasks/ContractualResultDetailDrawer.vue

File diff suppressed because it is too large

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

@ -38,6 +38,11 @@ export const formSchemas: FormSchema[] = [
field: 'documentName',
component: 'Input',
},
{
label: '审查类型',
field: 'reviewTypes',
component: 'Input',
},
{
label: '状态',
field: 'progressStatus',
@ -61,14 +66,25 @@ export const columns: BasicColumn[] = [
dataIndex: 'documentName',
},
{
title: '模型所属区域',
dataIndex: 'taskRegion',
customRender: ({ value }) => renderDict(value, 'model_region'),
title: '审查类型',
dataIndex: 'reviewTypes',
customRender: ({ value }) => {
if (!value) return '-';
// 将逗号分隔的字符串转换为更友好的显示格式
const types = value.split(',').map((type: string) => {
switch (type.trim()) {
case 'substantive':
return '实质性审查';
case 'compliance':
return '合规性审查';
case 'consistency':
return '一致性审查';
default:
return type.trim();
}
});
return types.join('、');
},
{
title: '模型所属行业',
dataIndex: 'taskIndustry',
customRender: ({ value }) => renderDict(value, 'model_industry'),
},
{
title: '审查立场',
@ -95,38 +111,6 @@ export const columns: BasicColumn[] = [
];
export const modalSchemas: FormSchema[] = [
{
label: '模型所属区域',
field: 'taskRegion',
required: true,
component: 'Select',
// componentProps: {
// options: taskRegionPermission(),
// defaultValue:"normal",
// },
componentProps: () => {
const isSuperAdmin = roleList.includes(RoleEnum.SUPER_ADMIN);
let options = getDictOptions('model_region');
if (!isSuperAdmin) {
// 如果不是超级管理员,移除 label 带有 '#' 的项
options = options.filter((option) => !option.label.includes('#'));
}
return {
options: options,
defaultValue: 'normal',
};
},
},
{
label: '模型所属行业',
field: 'taskIndustry',
required: true,
component: 'Select',
componentProps: {
options: getDictOptions('model_industry'),
defaultValue: 'normal',
},
},
{
label: '审查立场',
field: 'contractPartyRole',

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

@ -1,175 +1,191 @@
<template>
<div class="compliance-content">
<!-- 法规范围选择 -->
<div class="section regulation-section">
<h3 class="section-title">选择适用法规范围</h3>
<p class="section-description">选择需要进行合规性检查的法律法规类别</p>
<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>
</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>
</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>
<!-- 行业特殊要求 -->
<div class="section industry-section">
<h3 class="section-title">行业特殊要求</h3>
<p class="section-description">选择合同涉及的行业领域进行专项合规检查</p>
<!-- 选择法规方式 -->
<div class="section regulation-method-section">
<h3 class="section-title">选择法规检查方式</h3>
<p class="section-description">选择生成合规检查法规的方式系统将根据您的选择进行相应的合规性审查</p>
<div class="review-type-selector">
<div class="radio-group">
<Radio.Group v-model:value="selectedIndustry" size="large">
<Radio value="financial">
<Radio.Group v-model:value="selectedRegulationMethod" size="large">
<Radio value="ai">
<div class="radio-option">
<BankOutlined class="option-icon" />
<RobotOutlined class="option-icon" />
<div class="option-content">
<div class="option-title">金融服务</div>
<div class="option-desc">银行证券保险等金融行业合规要求</div>
<div class="option-title">AI自动选择</div>
<div class="option-desc">智能分析合同内容自动选择适用法规</div>
</div>
</div>
</Radio>
<Radio value="medical">
<Radio value="manual">
<div class="radio-option">
<MedicineBoxOutlined class="option-icon" />
<FileTextOutlined class="option-icon" />
<div class="option-content">
<div class="option-title">医疗健康</div>
<div class="option-desc">医疗器械药品健康服务等行业规范</div>
</div>
</div>
</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>
</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 class="option-title">人工选择法规</div>
<div class="option-desc">手动选择需要进行合规性检查的法规名称</div>
</div>
</div>
</Radio>
</Radio.Group>
</div>
</div>
<!-- 合规检查级别 -->
<div class="section level-section">
<h3 class="section-title">合规检查级别</h3>
<p class="section-description">选择合规检查的严格程度</p>
<!-- 法规名称选择器 -->
<div v-if="showRegulationSelector" class="regulation-selector">
<label class="selector-label">选择法规名称</label>
<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>
</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>
<Select
v-model:value="selectedRegulationIds"
mode="multiple"
style="width: 100%"
placeholder="请搜索并选择法规名称..."
:loading="regulationLoading"
size="large"
show-search
:filter-option="false"
@search="handleRegulationSearch"
@change="handleRegulationChange"
@focus="handleRegulationFocus"
:dropdownMatchSelectWidth="false"
dropdownClassName="regulation-dropdown"
:show-arrow="true"
>
<Select.Option
v-for="regulation in filteredRegulations"
:key="regulation.id"
:value="String(regulation.id)"
>
<div class="regulation-option">
<span class="regulation-name">{{ regulation.regulationName }}</span>
<span class="regulation-desc" v-if="regulation.regulationDescription">{{ regulation.regulationDescription }}</span>
</div>
</Select.Option>
</Select>
</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>
<!-- 特别关注点 -->
<div class="section focus-section">
<h3 class="section-title">特别关注点可选</h3>
<p class="section-description">指定需要特别关注的合规风险点</p>
<!-- 特别说明 -->
<!-- <div class="section special-note-section">
<h3 class="section-title">特别说明可选</h3>
<p class="section-description">您可以在此添加针对本次合规性审查的特别要求或关注点</p>
<div class="focus-input">
<div class="special-note-input">
<Input.TextArea
v-model:value="focusPoints"
placeholder="请输入特别关注的合规要求,如特定法规条款、行业标准、监管要求等..."
v-model:value="specialNote"
placeholder="请输入特别说明,如特定的合规要求、风险关注点、监管要求等..."
:rows="3"
:maxlength="500"
show-count
size="large"
/>
</div>
</div>
</div> -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Input, Checkbox, Radio } from 'ant-design-vue';
import { ref, computed, onMounted } from 'vue';
import { Input, Select, Radio } from 'ant-design-vue';
import {
BankOutlined,
MedicineBoxOutlined,
LaptopOutlined,
GlobalOutlined
RobotOutlined,
FileTextOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { ContractualRegulationNamesEffectiveList } from '@/api/contractReview/ContractualRegulationNames';
import type { SelectValue } from 'ant-design-vue/es/select';
import type { ContractualRegulationNamesVO } from '@/api/contractReview/ContractualRegulationNames/model';
//
const selectedRegulations = ref<string[]>(['contract-law']); //
const selectedIndustry = ref<string>('general'); //
const selectedLevel = ref<string>('standard'); //
const focusPoints = ref<string>(''); //
const selectedRegulationMethod = ref<string>('ai'); // AI
const selectedRegulationIds = ref<string[]>([]);
const specialNote = ref<string>(''); //
//
const regulationLoading = ref(false);
const regulations = ref<ContractualRegulationNamesVO[]>([]);
const filteredRegulations = ref<ContractualRegulationNamesVO[]>([]);
const regulationSearchValue = ref('');
//
const showRegulationSelector = computed(() => {
return selectedRegulationMethod.value === 'manual';
});
//
const loadRegulations = async () => {
regulationLoading.value = true;
try {
const res = await ContractualRegulationNamesEffectiveList();
const data = res as any;
if (data && data.length > 0) {
regulations.value = data;
filteredRegulations.value = data;
} else {
regulations.value = [];
filteredRegulations.value = [];
}
} catch (error) {
console.error('加载法规名称失败:', error);
message.error('加载法规名称失败');
} finally {
regulationLoading.value = false;
}
};
//
const handleRegulationSearch = (value: string) => {
regulationSearchValue.value = value;
if (!value) {
filteredRegulations.value = regulations.value;
} else {
filteredRegulations.value = regulations.value.filter(item =>
item.regulationName.toLowerCase().includes(value.toLowerCase()) ||
(item.regulationDescription && item.regulationDescription.toLowerCase().includes(value.toLowerCase()))
);
}
};
//
const handleRegulationChange = (values: SelectValue) => {
let valueArray: string[] = [];
if (Array.isArray(values)) {
valueArray = values.map(v => String(v));
} else if (values !== undefined && values !== null) {
valueArray = [String(values)];
}
//
regulationSearchValue.value = '';
filteredRegulations.value = regulations.value;
};
//
const handleRegulationFocus = () => {
//
regulationSearchValue.value = '';
filteredRegulations.value = regulations.value;
};
//
const getData = () => {
//
if (selectedRegulations.value.length === 0) {
message.warning('请至少选择一个法规范围');
//
if (!selectedRegulationMethod.value) {
message.warning('请选择法规检查方式');
return null;
}
//
if (showRegulationSelector.value && selectedRegulationIds.value.length === 0) {
message.warning('请选择法规名称');
return null;
}
return {
type: 'compliance',
regulations: selectedRegulations.value,
industry: selectedIndustry.value,
level: selectedLevel.value,
focusPoints: focusPoints.value || undefined,
regulationMethod: selectedRegulationMethod.value,
regulationIds: selectedRegulationIds.value || undefined,
specialNote: specialNote.value || undefined,
};
};
@ -177,6 +193,11 @@
defineExpose({
getData
});
//
onMounted(() => {
loadRegulations();
});
</script>
<style lang="less" scoped>
@ -209,51 +230,11 @@
line-height: 1.4;
}
//
.checkbox-group {
:deep(.ant-checkbox-group) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 8px;
width: 100%;
}
:deep(.ant-checkbox-wrapper) {
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
margin: 0;
transition: all 0.3s;
&:hover {
border-color: #722ed1;
background-color: #f9f0ff;
}
&.ant-checkbox-wrapper-checked {
border-color: #722ed1;
background-color: #f9f0ff;
box-shadow: 0 0 0 2px rgba(114, 46, 209, 0.2);
}
}
:deep(.ant-checkbox) {
.ant-checkbox-inner {
border-color: #722ed1;
}
&.ant-checkbox-checked .ant-checkbox-inner {
background-color: #722ed1;
border-color: #722ed1;
}
}
}
//
.radio-group {
:deep(.ant-radio-group) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 8px;
width: 100%;
}
@ -289,8 +270,7 @@
}
}
//
.checkbox-option,
//
.radio-option {
display: flex;
align-items: center;
@ -319,8 +299,46 @@
}
}
//
.focus-input {
//
.review-type-selector {
padding: 12px;
}
//
.regulation-selector {
margin-top: 12px;
padding: 12px;
background-color: #f9f9f9;
border-radius: 4px;
.selector-label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
color: #333;
}
}
.regulation-option {
display: flex;
flex-direction: column;
gap: 2px;
.regulation-name {
font-weight: 500;
color: #333;
font-size: 13px;
}
.regulation-desc {
font-size: 11px;
color: #666;
}
}
//
.special-note-input {
margin-top: 12px;
:deep(.ant-input) {

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

@ -52,7 +52,7 @@
</div>
<!-- 特别关注点 -->
<div class="section focus-section">
<!-- <div class="section focus-section">
<h3 class="section-title">特别关注点可选</h3>
<p class="section-description">指定需要特别关注的一致性检查要点</p>
@ -66,7 +66,7 @@
size="large"
/>
</div>
</div>
</div> -->
</div>
</template>

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

@ -261,7 +261,7 @@
//
const reviewRequest: StartContractReviewRequest = {
ossId: currentOssId.value!,
reviewTypes: data.reviewTypes || props.reviewTypes,
reviewTypes: props.reviewTypes,
reviewData: data.reviewData,
visitedTabs: data.visitedTabs
};
@ -282,6 +282,9 @@
key: 'startReview'
});
//
clearUploadData();
//
emit('success', {
taskId: response.taskId,
@ -309,6 +312,15 @@
console.log('Review configuration cancelled');
emit('cancel');
}
//
function clearUploadData() {
fileList.value = [];
uploadPercent.value = 0;
currentOssId.value = null;
uploading.value = false;
console.log('Upload data cleared for next upload');
}
</script>
<style scoped>

190
src/views/contractReview/ContractualTasks/configs/reviewTypeConfigs.ts

@ -0,0 +1,190 @@
// 审查字段配置接口
export interface ReviewFieldConfig {
field: string;
title: string;
type: 'markdown' | 'text' | 'reviewPointsList';
required?: boolean;
pdfSource?: 'contract' | 'bid' | 'auto'; // 文本定位时使用哪个PDF
displayCondition?: (item: any) => boolean;
showComparison?: boolean; // 是否显示对比按钮
nestedFields?: string[]; // 嵌套字段,用于reviewPointsList类型
}
// PDF数据源配置接口
export interface PdfSourceConfig {
id: string;
title: string;
apiField: string; // 对应后端字段名
}
// 审查类型配置接口
export interface ReviewTypeConfig {
type: string;
name: string;
pdfLayout: 'single' | 'split';
pdfSources: PdfSourceConfig[];
fields: ReviewFieldConfig[];
}
// 审查类型配置定义
export const REVIEW_TYPE_CONFIGS: Record<string, ReviewTypeConfig> = {
// 实质性审查配置
substantive: {
type: 'substantive',
name: '实质性审查',
pdfLayout: 'single',
pdfSources: [
{ id: 'contract', title: '合同文件', apiField: 'id' }
],
fields: [
{
field: 'originalText',
title: '原文',
type: 'markdown',
pdfSource: 'contract',
required: true
},
{
field: 'modifiedContent',
title: '修改建议',
type: 'markdown',
showComparison: true // 显示对比按钮
},
{
field: 'reviewBasis',
title: '审查依据',
type: 'reviewPointsList',
nestedFields: ['reviewContent', 'reviewPoints']
}
]
},
// 合规性审查配置
compliance: {
type: 'compliance',
name: '合规性审查',
pdfLayout: 'single',
pdfSources: [
{ id: 'contract', title: '合同文件', apiField: 'id' }
],
fields: [
{
field: 'originalText',
title: '原文',
type: 'markdown',
pdfSource: 'contract',
required: true
},
{
field: 'existingIssues',
title: '合规性问题',
type: 'markdown'
},
{
field: 'modifiedContent',
title: '修改建议',
type: 'markdown',
showComparison: true
},
{
field: 'reviewBasis',
title: '法规依据',
type: 'reviewPointsList',
nestedFields: ['reviewContent', 'reviewPoints']
}
]
},
// 一致性审查配置
consistency: {
type: 'consistency',
name: '一致性审查',
pdfLayout: 'split',
pdfSources: [
{ id: 'contract', title: '合同文件', apiField: 'id' },
{ id: 'bid', title: '招标文件', apiField: 'id' }
],
fields: [
{
field: 'originalText',
title: '合同原文',
type: 'markdown',
pdfSource: 'contract',
required: true
},
{
field: 'comparedText',
title: '招标文件对应内容',
type: 'markdown',
pdfSource: 'bid',
required: true
},
{
field: 'modifiedContent',
title: '修改建议',
type: 'markdown',
showComparison: true
},
// {
// field: 'existingIssues',
// title: '不一致问题',
// type: 'markdown'
// },
// {
// field: 'reviewBasis',
// title: '审查依据',
// type: 'reviewPointsList',
// nestedFields: ['reviewContent', 'reviewPoints']
// }
]
}
};
// 根据审查类型获取配置的工具函数
export function getReviewTypeConfig(reviewTypes: string[]): ReviewTypeConfig {
// 优先级:一致性 > 合规性 > 实质性
if (reviewTypes.includes('consistency')) {
return REVIEW_TYPE_CONFIGS.consistency;
} else if (reviewTypes.includes('compliance')) {
return REVIEW_TYPE_CONFIGS.compliance;
} else {
return REVIEW_TYPE_CONFIGS.substantive;
}
}
// 根据中文名称获取配置
export function getReviewTypeConfigByName(chineseName: string): ReviewTypeConfig {
switch (chineseName) {
case '一致性审查':
return REVIEW_TYPE_CONFIGS.consistency;
case '合规性审查':
return REVIEW_TYPE_CONFIGS.compliance;
case '实质性审查':
return REVIEW_TYPE_CONFIGS.substantive;
default:
return REVIEW_TYPE_CONFIGS.substantive; // 默认返回实质性审查
}
}
// 获取所有可用的审查类型配置(根据中文名称列表)
export function getAvailableReviewConfigs(chineseNames: string[]): ReviewTypeConfig[] {
return chineseNames
.filter(name => name !== '全部') // 过滤掉"全部"
.map(name => getReviewTypeConfigByName(name));
}
// 检查字段是否应该显示对比按钮
export function shouldShowComparison(fieldConfig: ReviewFieldConfig, item: any): boolean {
return !!(fieldConfig.showComparison && item.modificationDisplay && item.modificationDisplay.trim());
}
// 获取字段的PDF源
export function getFieldPdfSource(fieldConfig: ReviewFieldConfig): string | null {
// 只有配置了pdfSource的字段才支持定位
return fieldConfig.pdfSource || null;
}
// 检查字段是否支持PDF定位
export function supportsPdfLocation(fieldConfig: ReviewFieldConfig): boolean {
return !!(fieldConfig.pdfSource && fieldConfig.pdfSource !== 'auto');
}

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

@ -43,16 +43,20 @@
</div>
<!-- 审查界面 -->
<InferenceReview :reviewTypes="selectedOptions" />
<InferenceReview :reviewTypes="selectedOptions" @success="handleReviewSuccess" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import InferenceReview from './components/InferenceReview.vue';
import { useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
defineOptions({ name: 'ContractualTasks' });
const router = useRouter();
//
const selectedOptions = ref<string[]>(['substantive']);
@ -65,6 +69,22 @@
selectedOptions.value.push(option);
}
}
function handleReviewSuccess(data: any) {
//
console.log('审查任务创建成功:', data);
//
message.success('合同审查任务已创建成功,正在后台处理');
//
setTimeout(() => {
router.push({
path: '/contractReview/contractualList',
query: { refresh: Date.now().toString() }
});
}, 100);
}
</script>
<style scoped>

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

@ -119,7 +119,10 @@
import { ref } from 'vue';
import ContractualResultDetailDrawer from './ContractualResultDetailDrawer.vue';
import { ContractualTaskResultDetailVO } from '@/api/contractReview/ContractualTaskResults/model';
import { useRouter } from 'vue-router';
import { watch } from 'vue';
const router = useRouter();
const [registerDrawer, { openDrawer }] = useDrawer();
const resultDetailDrawerVisible = ref(false);
const taskResultDetail = ref<ContractualTaskResultDetailVO[]>([]);
@ -156,6 +159,21 @@
const [registerModal, { openModal }] = useModal();
// refresh
watch(
() => router.currentRoute.value.query.refresh,
(newVal) => {
if (newVal) {
reload();
//
router.replace({
path: router.currentRoute.value.path,
query: { ...router.currentRoute.value.query, refresh: undefined }
});
}
}
);
async function handleDetail(record: Recordable) {
try {
const detailRes = await getDetailResultsByTaskId(record.id);
@ -180,7 +198,7 @@
}
function handleAdd() {
openModal(true, { update: false });
router.push('/contractReview/ContractualTasks');
}

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

File diff suppressed because it is too large
Loading…
Cancel
Save