23 changed files with 3601 additions and 2323 deletions
@ -1,2 +1,3 @@ |
|||
export { default as ImagePreview } from './src/Preview.vue'; |
|||
export { createImgPreview } from './src/functional'; |
|||
export { PdfPreviewComponent, PageSelectModal } from './src/PdfPreview'; |
|||
|
@ -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.value不为null时才传递 |
|||
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.js的y是基线,需调整 |
|||
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> |
@ -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; |
|||
} |
|||
}; |
|||
|
|||
// 根据PDF源获取对应的getPdfStream函数 |
|||
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 { |
|||
// 如果没有指定PDF源,默认在第一个PDF中定位 |
|||
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 { |
|||
// 如果没有指定PDF源,默认在第一个PDF中定位 |
|||
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> |
@ -0,0 +1,2 @@ |
|||
export { default as PdfPreviewComponent } from './PdfPreviewComponent.vue'; |
|||
export { default as PageSelectModal } from './PageSelectModal.vue'; |
File diff suppressed because it is too large
@ -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); |
|||
} |
@ -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); |
|||
} |
@ -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; |
|||
} |
@ -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'; |
@ -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'); |
|||
} |
File diff suppressed because it is too large
@ -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'); |
|||
} |
File diff suppressed because it is too large
Loading…
Reference in new issue