|
|
@@ -257,7 +257,7 @@
|
|
|
v-for="inp in rule.inputs"
|
|
|
:key="inp.inputId"
|
|
|
class="rule-trace-att clickable"
|
|
|
- @click="openSourceInViewer(inp)"
|
|
|
+ @click="openSourceInViewer(inp, rule)"
|
|
|
title="点击查看来源"
|
|
|
>📎 {{ formatInputSource(inp) }}</span>
|
|
|
</div>
|
|
|
@@ -1144,16 +1144,90 @@ function getInputSourceText(rule) {
|
|
|
return ''
|
|
|
}
|
|
|
|
|
|
-async function openSourceInViewer(inp) {
|
|
|
+async function openSourceInViewer(inp, rule = null) {
|
|
|
+ console.log('[openSourceInViewer] 开始溯源定位')
|
|
|
+ console.log('[openSourceInViewer] inp:', inp)
|
|
|
+ console.log('[openSourceInViewer] rule:', rule)
|
|
|
+
|
|
|
// 关闭弹窗
|
|
|
highlightPopover.visible = false
|
|
|
|
|
|
const attId = inp.sourceNodeId
|
|
|
const entryPath = inp.entryPath
|
|
|
- const sourceText = inp.sourceText
|
|
|
+ let sourceText = inp.sourceText || ''
|
|
|
+
|
|
|
+ console.log('[openSourceInViewer] attId:', attId, 'entryPath:', entryPath, 'sourceText:', sourceText)
|
|
|
+
|
|
|
+ // 如果没有 sourceText,尝试从规则的各个字段中提取评审代码(如 5.1.5)
|
|
|
+ if (!sourceText && rule) {
|
|
|
+ // 尝试从 description、actionConfig、ruleName 等字段提取
|
|
|
+ const searchFields = [
|
|
|
+ rule.description,
|
|
|
+ rule.actionConfig,
|
|
|
+ rule.ruleName
|
|
|
+ ].filter(Boolean).join(' ')
|
|
|
+ console.log('[openSourceInViewer] 尝试从规则字段提取评审代码:', searchFields)
|
|
|
+
|
|
|
+ // 匹配 "评审代码5.1.5" 或 "代码5.1.5" 或直接匹配 "5.1.5" 格式
|
|
|
+ let codeMatch = searchFields.match(/评审代码\s*(\d+\.\d+(?:\.\d+)*)/i)
|
|
|
+ if (!codeMatch) {
|
|
|
+ codeMatch = searchFields.match(/代码\s*(\d+\.\d+(?:\.\d+)*)/i)
|
|
|
+ }
|
|
|
+ if (!codeMatch) {
|
|
|
+ // 尝试从 actionConfig JSON 中提取
|
|
|
+ try {
|
|
|
+ const cfg = typeof rule.actionConfig === 'string' ? JSON.parse(rule.actionConfig) : rule.actionConfig
|
|
|
+ if (cfg?.reviewCode) {
|
|
|
+ sourceText = cfg.reviewCode
|
|
|
+ console.log('[openSourceInViewer] 从 actionConfig.reviewCode 提取:', sourceText)
|
|
|
+ } else if (cfg?.sourceText) {
|
|
|
+ sourceText = cfg.sourceText
|
|
|
+ console.log('[openSourceInViewer] 从 actionConfig.sourceText 提取:', sourceText)
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
+ }
|
|
|
+ if (codeMatch) {
|
|
|
+ sourceText = codeMatch[1]
|
|
|
+ console.log('[openSourceInViewer] 提取到评审代码:', sourceText)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果还是没有,尝试根据 ruleName 匹配评审代码(核心要素评审表的项目名称)
|
|
|
+ if (!sourceText && rule.ruleName) {
|
|
|
+ const ruleNameToCode = {
|
|
|
+ '安全文化': '5.1.5',
|
|
|
+ '安全文化建设': '5.1.5',
|
|
|
+ '安全投入': '5.1.4',
|
|
|
+ '安全生产投入': '5.1.4',
|
|
|
+ '目标职责': '5.1',
|
|
|
+ '目标': '5.1.1',
|
|
|
+ '目标制定': '5.1.1.1',
|
|
|
+ '目标落实': '5.1.1.2',
|
|
|
+ '目标考核': '5.1.1.3',
|
|
|
+ '机构和职责': '5.1.2',
|
|
|
+ '机构设置': '5.1.2.1',
|
|
|
+ '全员参与': '5.1.3',
|
|
|
+ '信息化建设': '5.1.6',
|
|
|
+ '安全生产信息化建设': '5.1.6',
|
|
|
+ '制度化管理': '5.2',
|
|
|
+ '法规标准识别': '5.2.1',
|
|
|
+ '规章制度': '5.2.2',
|
|
|
+ '操作规程': '5.2.3',
|
|
|
+ '评估和修订': '5.2.4',
|
|
|
+ '文档管理': '5.2.5',
|
|
|
+ '设备设施管理': '5.4.1',
|
|
|
+ '设备设施': '5.4.1',
|
|
|
+ }
|
|
|
+ const code = ruleNameToCode[rule.ruleName]
|
|
|
+ if (code) {
|
|
|
+ sourceText = code
|
|
|
+ console.log('[openSourceInViewer] 根据 ruleName 映射到评审代码:', rule.ruleName, '->', code)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
// 查找附件
|
|
|
const att = attachments.value.find(a => a.id === attId)
|
|
|
+ console.log('[openSourceInViewer] 找到附件:', att)
|
|
|
if (!att) {
|
|
|
ElMessage.warning('未找到来源附件')
|
|
|
return
|
|
|
@@ -1161,6 +1235,7 @@ async function openSourceInViewer(inp) {
|
|
|
|
|
|
// 设置高亮文本(用于在查看器中高亮)
|
|
|
highlightSourceText.value = sourceText || ''
|
|
|
+ console.log('[openSourceInViewer] 设置 highlightSourceText:', highlightSourceText.value)
|
|
|
|
|
|
// 判断是否是 ZIP 文件
|
|
|
const ext = (att.fileType || '').toLowerCase()
|
|
|
@@ -1181,11 +1256,14 @@ async function openSourceInViewer(inp) {
|
|
|
} else {
|
|
|
// 直接打开附件查看器
|
|
|
const state = parseStates[att.id]
|
|
|
+ console.log('[openSourceInViewer] 附件解析状态:', state?.status)
|
|
|
if (state?.status === 'completed') {
|
|
|
+ console.log('[openSourceInViewer] 调用 viewParseResult')
|
|
|
viewParseResult(att)
|
|
|
} else {
|
|
|
// 先解析再查看
|
|
|
- await triggerParse(att)
|
|
|
+ console.log('[openSourceInViewer] 附件未解析,调用 handleParseAttachment')
|
|
|
+ await handleParseAttachment(att)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -1262,14 +1340,75 @@ const parseResultHtml = computed(() => {
|
|
|
html = `<pre>${parseResultContent.value}</pre>`
|
|
|
}
|
|
|
}
|
|
|
- // 如果有高亮文本,在 HTML 中高亮显示
|
|
|
- if (highlightSourceText.value && highlightSourceText.value.length > 5) {
|
|
|
- const escaped = highlightSourceText.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
- const regex = new RegExp(`(${escaped})`, 'gi')
|
|
|
- html = html.replace(regex, '<mark class="source-highlight">$1</mark>')
|
|
|
+ // 如果有高亮文本,在 HTML 中高亮显示(评审代码如 5.1.5 长度为5,所以用 >= 3)
|
|
|
+ if (highlightSourceText.value && highlightSourceText.value.length >= 3) {
|
|
|
+ html = highlightSourceInHtml(html, highlightSourceText.value)
|
|
|
}
|
|
|
return html
|
|
|
})
|
|
|
+
|
|
|
+// 智能高亮来源文本
|
|
|
+function highlightSourceInHtml(html, sourceText) {
|
|
|
+ console.log('[highlightSourceInHtml] 开始高亮, sourceText:', sourceText, 'html长度:', html?.length)
|
|
|
+ if (!sourceText || sourceText.length < 3) {
|
|
|
+ console.log('[highlightSourceInHtml] sourceText 太短,跳过')
|
|
|
+ return html
|
|
|
+ }
|
|
|
+
|
|
|
+ // 0. 检测评审代码模式(如 5.1.5, 5.1.4.1 等),高亮整个表格行
|
|
|
+ const codeMatch = sourceText.match(/\b(\d+\.\d+(?:\.\d+)*)\b/)
|
|
|
+ console.log('[highlightSourceInHtml] 评审代码匹配结果:', codeMatch)
|
|
|
+ if (codeMatch) {
|
|
|
+ const code = codeMatch[1]
|
|
|
+ console.log('[highlightSourceInHtml] 检测到评审代码:', code)
|
|
|
+
|
|
|
+ // 直接高亮评审代码本身(更简单可靠的方式)
|
|
|
+ const codeEscaped = code.replace(/\./g, '\\.')
|
|
|
+ // 匹配 >5.1.5< 格式(在标签之间的评审代码)
|
|
|
+ const codeRegex = new RegExp(`(>[^<]*?)(${codeEscaped})([^<]*?<)`, 'gi')
|
|
|
+ if (codeRegex.test(html)) {
|
|
|
+ console.log('[highlightSourceInHtml] 高亮评审代码')
|
|
|
+ return html.replace(new RegExp(`(>[^<]*?)(${codeEscaped})([^<]*?<)`, 'gi'),
|
|
|
+ '$1<mark class="source-highlight">$2</mark>$3')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 先尝试精确匹配
|
|
|
+ const escaped = sourceText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
+ const exactRegex = new RegExp(`(${escaped})`, 'gi')
|
|
|
+ if (exactRegex.test(html)) {
|
|
|
+ return html.replace(exactRegex, '<mark class="source-highlight">$1</mark>')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 尝试匹配前50个字符(处理截断的情况)
|
|
|
+ const prefix = sourceText.slice(0, 50).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
+ const prefixRegex = new RegExp(`(${prefix}[^<]*)`, 'gi')
|
|
|
+ if (prefixRegex.test(html)) {
|
|
|
+ return html.replace(prefixRegex, '<mark class="source-highlight">$1</mark>')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 对于表格内容,尝试高亮包含关键词的表格行
|
|
|
+ // 提取关键词(去除常见词,取前几个有意义的词)
|
|
|
+ const keywords = sourceText
|
|
|
+ .replace(/[,。、:;""''()\[\]【】\n\r]/g, ' ')
|
|
|
+ .split(/\s+/)
|
|
|
+ .filter(w => w.length >= 2)
|
|
|
+ .slice(0, 5)
|
|
|
+
|
|
|
+ if (keywords.length > 0) {
|
|
|
+ // 高亮包含关键词的 <tr> 或 <td>
|
|
|
+ let result = html
|
|
|
+ for (const kw of keywords) {
|
|
|
+ const kwEscaped = kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
+ // 高亮关键词本身
|
|
|
+ const kwRegex = new RegExp(`(${kwEscaped})`, 'gi')
|
|
|
+ result = result.replace(kwRegex, '<mark class="source-highlight">$1</mark>')
|
|
|
+ }
|
|
|
+ return result
|
|
|
+ }
|
|
|
+
|
|
|
+ return html
|
|
|
+}
|
|
|
const parseResultSource = computed(() => {
|
|
|
if (!parseResultContent.value) return ''
|
|
|
// 源码视图:将 base64 数据替换为简短占位符
|
|
|
@@ -2281,7 +2420,15 @@ async function confirmCitation(elem) {
|
|
|
})
|
|
|
// 查找附件节点ID(如果有的话)
|
|
|
const attId = referenceModeAttId.value || null
|
|
|
- const inputs = attId ? [{ sourceNodeId: attId, inputKey: 'attachment', inputType: 'ATTACHMENT', inputName: referenceModeAttName.value || parseResultAttName.value }] : []
|
|
|
+ const attName = referenceModeAttName.value || parseResultAttName.value || ''
|
|
|
+ const inputs = attId ? [{
|
|
|
+ sourceNodeId: attId,
|
|
|
+ inputKey: 'attachment',
|
|
|
+ inputType: 'ATTACHMENT',
|
|
|
+ inputName: attName,
|
|
|
+ sourceName: attName,
|
|
|
+ sourceText: selectedText // 传入来源段落文本,用于溯源定位
|
|
|
+ }] : []
|
|
|
|
|
|
const ruleData = {
|
|
|
elementKey: elem.elementKey,
|
|
|
@@ -2571,10 +2718,23 @@ function viewParseResult(att) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function scrollToHighlight() {
|
|
|
+function scrollToHighlight(retries = 3) {
|
|
|
+ console.log('[scrollToHighlight] 尝试滚动定位, retries:', retries)
|
|
|
+ // 查找高亮元素
|
|
|
const highlight = document.querySelector('.parse-result-rendered mark.source-highlight')
|
|
|
+ console.log('[scrollToHighlight] 找到高亮元素:', highlight)
|
|
|
if (highlight) {
|
|
|
highlight.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
|
+ // 添加闪烁效果
|
|
|
+ highlight.classList.add('highlight-flash')
|
|
|
+ setTimeout(() => highlight.classList.remove('highlight-flash'), 2000)
|
|
|
+ console.log('[scrollToHighlight] 滚动完成')
|
|
|
+ } else if (retries > 0) {
|
|
|
+ // 如果没找到,可能是 DOM 还没渲染完,重试
|
|
|
+ console.log('[scrollToHighlight] 未找到元素,重试...')
|
|
|
+ setTimeout(() => scrollToHighlight(retries - 1), 200)
|
|
|
+ } else {
|
|
|
+ console.log('[scrollToHighlight] 重试次数用尽,未找到高亮元素')
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -6603,6 +6763,31 @@ onMounted(async () => {
|
|
|
0%, 100% { background: linear-gradient(180deg, #fff3cd, #ffeeba); }
|
|
|
50% { background: linear-gradient(180deg, #ffe066, #ffc107); }
|
|
|
}
|
|
|
+
|
|
|
+ /* 溯源定位时的闪烁效果 */
|
|
|
+ mark.source-highlight.highlight-flash {
|
|
|
+ animation: highlight-flash 0.5s ease-in-out 3;
|
|
|
+ outline: 2px solid #f59e0b;
|
|
|
+ outline-offset: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes highlight-flash {
|
|
|
+ 0%, 100% { background: linear-gradient(180deg, #ffc107, #ff9800); }
|
|
|
+ 50% { background: linear-gradient(180deg, #fff3cd, #ffeeba); }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 表格行高亮样式(用于评审代码定位) */
|
|
|
+ mark.source-highlight-row {
|
|
|
+ display: contents;
|
|
|
+ }
|
|
|
+ mark.source-highlight-row td {
|
|
|
+ background: linear-gradient(180deg, #fff3cd, #ffeeba) !important;
|
|
|
+ border-left: 3px solid #f59e0b !important;
|
|
|
+ }
|
|
|
+ tr:has(mark.source-highlight-row) {
|
|
|
+ outline: 2px solid #f59e0b;
|
|
|
+ outline-offset: -1px;
|
|
|
+ }
|
|
|
|
|
|
code {
|
|
|
background: #f3f4f6;
|