|
|
@@ -739,50 +739,90 @@ function stripValueKeyPrefix(valueElementKey) {
|
|
|
return idx >= 0 ? valueElementKey.substring(idx + 1) : valueElementKey
|
|
|
}
|
|
|
|
|
|
-// 构建要素值映射表,用于在文档中高亮要素值文本
|
|
|
+// 构建要素值映射表,分为长文本和短文本两类
|
|
|
function buildElementValueMap() {
|
|
|
- const map = []
|
|
|
+ const longTexts = [] // paragraph/table 类型的长文本要素
|
|
|
+ const shortTexts = [] // text 类型的短文本要素
|
|
|
const colors = [
|
|
|
'#fff3cd', '#cce5ff', '#d4edda', '#f8d7da', '#e2d5f1',
|
|
|
'#d1ecf1', '#ffeeba', '#c3e6cb', '#f5c6cb', '#d6d8db'
|
|
|
]
|
|
|
let colorIdx = 0
|
|
|
for (const elem of elements.value) {
|
|
|
- // 值的 elementKey 带项目前缀,需要匹配
|
|
|
const elemValues = values.value.filter(v => stripValueKeyPrefix(v.elementKey) === elem.elementKey)
|
|
|
+ const elemType = elem.elementType || 'text'
|
|
|
for (const val of elemValues) {
|
|
|
const text = val.valueText
|
|
|
- if (text && text.length >= 2) {
|
|
|
- map.push({
|
|
|
- text,
|
|
|
- elementKey: elem.elementKey,
|
|
|
- fullElementKey: val.elementKey, // 带前缀,用于 API 调用
|
|
|
- elementName: elem.elementName,
|
|
|
- valueId: val.valueId,
|
|
|
- color: colors[colorIdx % colors.length]
|
|
|
- })
|
|
|
+ if (!text || text.length < 2) continue
|
|
|
+ const entry = {
|
|
|
+ text,
|
|
|
+ elementKey: elem.elementKey,
|
|
|
+ fullElementKey: val.elementKey,
|
|
|
+ elementName: elem.elementName,
|
|
|
+ valueId: val.valueId,
|
|
|
+ elemType,
|
|
|
+ color: colors[colorIdx % colors.length]
|
|
|
+ }
|
|
|
+ // paragraph/table 类型或多行文本视为长文本
|
|
|
+ if (elemType === 'paragraph' || elemType === 'table' || text.includes('\n') || text.length > 100) {
|
|
|
+ longTexts.push(entry)
|
|
|
+ } else {
|
|
|
+ shortTexts.push(entry)
|
|
|
}
|
|
|
}
|
|
|
colorIdx++
|
|
|
}
|
|
|
- // 按文本长度降序排列,优先匹配长文本
|
|
|
- map.sort((a, b) => b.text.length - a.text.length)
|
|
|
- return map
|
|
|
+ // 长文本按长度降序
|
|
|
+ longTexts.sort((a, b) => b.text.length - a.text.length)
|
|
|
+ // 短文本按长度降序
|
|
|
+ shortTexts.sort((a, b) => b.text.length - a.text.length)
|
|
|
+ return { longTexts, shortTexts }
|
|
|
}
|
|
|
|
|
|
// 将文档 blocks 渲染为 HTML 字符串(含要素高亮)
|
|
|
function renderDocHtml() {
|
|
|
if (!docContent.value?.blocks) { docHtml.value = ''; return }
|
|
|
const blocks = docContent.value.blocks
|
|
|
- const elemMap = highlightEnabled.value ? buildElementValueMap() : []
|
|
|
+ const { longTexts, shortTexts } = highlightEnabled.value ? buildElementValueMap() : { longTexts: [], shortTexts: [] }
|
|
|
let highlightCount = 0
|
|
|
const parts = []
|
|
|
|
|
|
+ // 预处理:为每个长文本要素,将其 valueText 按行拆分为句子集合
|
|
|
+ // 用于判断某个 block 的文本是否属于某个长文本要素
|
|
|
+ const longTextLines = longTexts.map(lt => ({
|
|
|
+ ...lt,
|
|
|
+ lines: new Set(lt.text.split('\n').map(l => l.trim()).filter(l => l.length > 0))
|
|
|
+ }))
|
|
|
+
|
|
|
+ // 收集被长文本高亮覆盖的 block IDs,这些 block 内的短文本不再单独高亮
|
|
|
+ const longHighlightedBlockIds = new Set()
|
|
|
+
|
|
|
+ // 第一遍:确定哪些 block 属于长文本要素
|
|
|
+ for (const block of blocks) {
|
|
|
+ const blockText = getBlockPlainText(block)
|
|
|
+ if (!blockText) continue
|
|
|
+ for (const lt of longTextLines) {
|
|
|
+ if (lt.lines.has(blockText)) {
|
|
|
+ longHighlightedBlockIds.add(block.id)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 第二遍:渲染
|
|
|
for (const block of blocks) {
|
|
|
if (block.type === 'table') {
|
|
|
- parts.push(renderTableHtml(block))
|
|
|
+ // 检查表格是否属于某个长文本要素
|
|
|
+ const tableMatch = findTableLongTextMatch(block, longTextLines)
|
|
|
+ parts.push(renderTableHtml(block, tableMatch))
|
|
|
+ if (tableMatch) highlightCount++
|
|
|
} else {
|
|
|
- parts.push(renderBlockHtml(block, elemMap, (n) => { highlightCount += n }))
|
|
|
+ const blockText = getBlockPlainText(block)
|
|
|
+ // 检查是否属于某个长文本要素
|
|
|
+ const longMatch = findBlockLongTextMatch(blockText, longTextLines)
|
|
|
+ // 如果此 block 被长文本覆盖,只做长文本高亮,不做短文本高亮
|
|
|
+ const shortMap = longMatch ? [] : shortTexts
|
|
|
+ parts.push(renderBlockHtml(block, shortMap, longMatch, (n) => { highlightCount += n }))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -790,7 +830,32 @@ function renderDocHtml() {
|
|
|
docHtml.value = parts.join('')
|
|
|
}
|
|
|
|
|
|
-function renderBlockHtml(block, elemMap, countFn) {
|
|
|
+// 获取 block 的纯文本
|
|
|
+function getBlockPlainText(block) {
|
|
|
+ if (!block.runs) return ''
|
|
|
+ return block.runs.map(r => r.text).join('').trim()
|
|
|
+}
|
|
|
+
|
|
|
+// 查找 block 文本是否匹配某个长文本要素
|
|
|
+function findBlockLongTextMatch(blockText, longTextLines) {
|
|
|
+ if (!blockText) return null
|
|
|
+ for (const lt of longTextLines) {
|
|
|
+ if (lt.lines.has(blockText)) return lt
|
|
|
+ }
|
|
|
+ return null
|
|
|
+}
|
|
|
+
|
|
|
+// 查找表格是否匹配某个长文本要素(通过表格第一行文本匹配)
|
|
|
+function findTableLongTextMatch(block, longTextLines) {
|
|
|
+ if (!block.table?.data?.length) return null
|
|
|
+ const firstRowText = block.table.data[0].map(c => c.text).join(' | ')
|
|
|
+ for (const lt of longTextLines) {
|
|
|
+ if (lt.text.includes(firstRowText)) return lt
|
|
|
+ }
|
|
|
+ return null
|
|
|
+}
|
|
|
+
|
|
|
+function renderBlockHtml(block, shortMap, longMatch, countFn) {
|
|
|
const tag = getBlockTag(block.type)
|
|
|
const cls = `doc-block doc-${block.type}`
|
|
|
const styleStr = buildStyleStr(block.style)
|
|
|
@@ -815,10 +880,15 @@ function renderBlockHtml(block, elemMap, countFn) {
|
|
|
const rs = buildRunStyleStr(run)
|
|
|
runsHtml += rs ? `<span style="${rs}">${text}</span>` : text
|
|
|
}
|
|
|
- // 注入要素高亮
|
|
|
- if (elemMap.length > 0 && runsHtml.length > 0) {
|
|
|
+
|
|
|
+ // 长文本高亮:整个 block 内容包裹
|
|
|
+ if (longMatch) {
|
|
|
+ runsHtml = `<span class="elem-highlight elem-highlight-long" data-elem-key="${longMatch.elementKey}" data-value-id="${longMatch.valueId || ''}" style="background:${longMatch.color};border-left:3px solid ${darkenColor(longMatch.color)};padding-left:4px;cursor:pointer;display:inline;" contenteditable="true" title="${escapeAttr(longMatch.elementName)}">${runsHtml}</span>`
|
|
|
+ countFn(1)
|
|
|
+ } else if (shortMap.length > 0 && runsHtml.length > 0) {
|
|
|
+ // 短文本高亮:在 runsHtml 中查找并替换
|
|
|
let count = 0
|
|
|
- for (const em of elemMap) {
|
|
|
+ for (const em of shortMap) {
|
|
|
const escaped = escapeHtml(em.text)
|
|
|
if (runsHtml.includes(escaped)) {
|
|
|
const hl = `<span class="elem-highlight" data-elem-key="${em.elementKey}" data-value-id="${em.valueId || ''}" style="background:${em.color};border-bottom:2px solid ${darkenColor(em.color)};cursor:pointer;border-radius:2px;padding:0 1px;" contenteditable="false" title="${escapeAttr(em.elementName)}: ${escapeAttr(em.text)}">${escaped}</span>`
|
|
|
@@ -835,10 +905,13 @@ function renderBlockHtml(block, elemMap, countFn) {
|
|
|
return `<${tag} class="${cls}"${styleAttr} data-block-id="${block.id}">${inner}</${tag}>`
|
|
|
}
|
|
|
|
|
|
-function renderTableHtml(block) {
|
|
|
+function renderTableHtml(block, longMatch) {
|
|
|
const t = block.table
|
|
|
if (!t?.data) return ''
|
|
|
- let html = `<table class="doc-table" data-block-id="${block.id}">`
|
|
|
+ const hlClass = longMatch ? ' elem-highlight-table' : ''
|
|
|
+ const hlAttr = longMatch ? ` data-elem-key="${longMatch.elementKey}" data-value-id="${longMatch.valueId || ''}"` : ''
|
|
|
+ const hlStyle = longMatch ? ` style="outline:2px solid ${darkenColor(longMatch.color)};outline-offset:2px;background:${longMatch.color}22;"` : ''
|
|
|
+ let html = `<table class="doc-table${hlClass}" data-block-id="${block.id}"${hlAttr}${hlStyle}>`
|
|
|
for (let ri = 0; ri < t.data.length; ri++) {
|
|
|
html += '<tr>'
|
|
|
for (const cell of t.data[ri]) {
|
|
|
@@ -848,6 +921,9 @@ function renderTableHtml(block) {
|
|
|
html += '</tr>'
|
|
|
}
|
|
|
html += '</table>'
|
|
|
+ if (longMatch) {
|
|
|
+ html = `<div class="elem-highlight-wrap" data-elem-key="${longMatch.elementKey}" data-value-id="${longMatch.valueId || ''}" title="${escapeAttr(longMatch.elementName)}" style="cursor:pointer;">${html}</div>`
|
|
|
+ }
|
|
|
return html
|
|
|
}
|
|
|
|
|
|
@@ -916,7 +992,7 @@ function onDocInput() {
|
|
|
|
|
|
// 点击文档中的高亮要素
|
|
|
function onDocClick(e) {
|
|
|
- const target = e.target.closest('.elem-highlight')
|
|
|
+ const target = e.target.closest('.elem-highlight') || e.target.closest('.elem-highlight-wrap') || e.target.closest('.elem-highlight-table')
|
|
|
if (!target) {
|
|
|
highlightPopover.visible = false
|
|
|
return
|
|
|
@@ -3571,5 +3647,29 @@ onMounted(async () => {
|
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15), 0 0 0 2px rgba(24, 144, 255, 0.2);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+// 长文本高亮样式
|
|
|
+.elem-highlight-long {
|
|
|
+ display: inline !important;
|
|
|
+ border-radius: 2px;
|
|
|
+ transition: background 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.elem-highlight-wrap {
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ outline: 2px solid #1890ff !important;
|
|
|
+ outline-offset: 2px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.elem-highlight-table {
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ outline-color: #1890ff !important;
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|
|
|
|