|
|
@@ -224,58 +224,32 @@
|
|
|
<div class="editor-scroll" ref="editorRef" v-loading="loading" element-loading-text="正在加载项目...">
|
|
|
<!-- 文档视图 -->
|
|
|
<div class="document-view" v-if="viewMode === 'document'">
|
|
|
- <!-- 附件选择栏 -->
|
|
|
- <div class="doc-attachment-bar" v-if="attachments.length > 0">
|
|
|
- <span class="doc-att-label">查看附件:</span>
|
|
|
- <el-select v-model="docAttachmentId" placeholder="选择附件" size="small" style="width: 280px;" @change="loadDocContent">
|
|
|
- <el-option v-for="att in attachments" :key="att.id" :label="att.displayName" :value="att.id" />
|
|
|
- </el-select>
|
|
|
+ <!-- 工具栏 -->
|
|
|
+ <div class="doc-toolbar">
|
|
|
+ <div class="doc-toolbar-left" v-if="attachments.length > 0">
|
|
|
+ <span class="doc-att-label">附件:</span>
|
|
|
+ <el-select v-model="docAttachmentId" placeholder="选择附件" size="small" style="width: 240px;" @change="loadDocContent">
|
|
|
+ <el-option v-for="att in attachments" :key="att.id" :label="att.displayName" :value="att.id" />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+ <div class="doc-toolbar-right">
|
|
|
+ <el-switch v-model="highlightEnabled" active-text="要素高亮" inactive-text="" size="small" style="margin-right: 12px;" @change="renderDocHtml" />
|
|
|
+ <el-tag v-if="highlightEnabled" size="small" type="info">{{ elementHighlightCount }} 处高亮</el-tag>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 文档渲染区域 -->
|
|
|
- <div class="doc-paper" v-if="docContent && docContent.blocks" :style="docPaperStyle">
|
|
|
- <template v-for="block in docContent.blocks" :key="block.id">
|
|
|
- <!-- 段落/标题 -->
|
|
|
- <template v-if="block.type !== 'table'">
|
|
|
- <component
|
|
|
- :is="getBlockTag(block.type)"
|
|
|
- :class="['doc-block', 'doc-' + block.type]"
|
|
|
- :style="getBlockStyle(block.style)"
|
|
|
- :data-block-id="block.id"
|
|
|
- >
|
|
|
- <template v-if="block.images && block.images.length > 0">
|
|
|
- <img
|
|
|
- v-for="(img, idx) in block.images"
|
|
|
- :key="idx"
|
|
|
- :src="img.src"
|
|
|
- :style="getImageStyle(img)"
|
|
|
- class="doc-inline-image"
|
|
|
- />
|
|
|
- </template>
|
|
|
- <template v-if="block.runs">
|
|
|
- <template v-for="(run, ri) in block.runs" :key="ri">
|
|
|
- <span v-if="hasRunFormat(run)" :style="getRunStyle(run)">{{ run.text }}</span>
|
|
|
- <template v-else>{{ run.text }}</template>
|
|
|
- </template>
|
|
|
- </template>
|
|
|
- </component>
|
|
|
- </template>
|
|
|
-
|
|
|
- <!-- 表格 -->
|
|
|
- <table v-else class="doc-table" :data-block-id="block.id">
|
|
|
- <tr v-for="(row, ri) in block.table.data" :key="ri">
|
|
|
- <td
|
|
|
- v-for="(cell, ci) in row"
|
|
|
- :key="ci"
|
|
|
- :colspan="cell.colspan > 1 ? cell.colspan : undefined"
|
|
|
- class="doc-table-cell"
|
|
|
- >
|
|
|
- {{ cell.text }}
|
|
|
- </td>
|
|
|
- </tr>
|
|
|
- </table>
|
|
|
- </template>
|
|
|
- </div>
|
|
|
+ <!-- 文档渲染区域(可编辑) -->
|
|
|
+ <div
|
|
|
+ class="doc-paper"
|
|
|
+ v-if="docHtml"
|
|
|
+ :style="docPaperStyle"
|
|
|
+ contenteditable="true"
|
|
|
+ spellcheck="false"
|
|
|
+ v-html="docHtml"
|
|
|
+ @input="onDocInput"
|
|
|
+ @click="onDocClick"
|
|
|
+ ref="docPaperRef"
|
|
|
+ ></div>
|
|
|
|
|
|
<!-- 无内容提示 -->
|
|
|
<div class="doc-empty" v-else-if="!docLoading && docAttachmentId">
|
|
|
@@ -293,6 +267,37 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 要素高亮弹出框 -->
|
|
|
+ <div
|
|
|
+ v-if="highlightPopover.visible"
|
|
|
+ class="element-popover"
|
|
|
+ :style="{ top: highlightPopover.y + 'px', left: highlightPopover.x + 'px' }"
|
|
|
+ >
|
|
|
+ <div class="popover-header">
|
|
|
+ <span class="popover-label">{{ highlightPopover.elementName }}</span>
|
|
|
+ <el-tag size="small">{{ highlightPopover.elementKey }}</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="popover-body">
|
|
|
+ <div class="popover-field">
|
|
|
+ <span class="popover-field-label">当前值:</span>
|
|
|
+ <el-input
|
|
|
+ v-model="highlightPopover.currentValue"
|
|
|
+ size="small"
|
|
|
+ placeholder="输入要素值"
|
|
|
+ @keyup.enter="savePopoverValue"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="popover-field" v-if="highlightPopover.originalValue">
|
|
|
+ <span class="popover-field-label">原始值:</span>
|
|
|
+ <span class="popover-original">{{ highlightPopover.originalValue }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="popover-footer">
|
|
|
+ <el-button size="small" @click="highlightPopover.visible = false">关闭</el-button>
|
|
|
+ <el-button size="small" type="primary" @click="savePopoverValue">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- 要素视图 -->
|
|
|
<div class="elements-view" v-if="viewMode === 'elements'">
|
|
|
<div class="elements-grid">
|
|
|
@@ -302,11 +307,11 @@
|
|
|
<el-tag size="small" :type="elem.dataType === 'text' ? '' : 'warning'">{{ elem.dataType || '文本' }}</el-tag>
|
|
|
</div>
|
|
|
<div class="element-card-body">
|
|
|
- <div class="element-value-row" v-for="val in getElementValues(elem.elementKey)" :key="val.id">
|
|
|
- <el-input v-model="val.currentValue" :placeholder="val.originalValue || '暂无值'" size="small" @change="onValueChange(val)" />
|
|
|
- <div class="value-meta" v-if="val.originalValue">
|
|
|
- <span class="original-label">原值:</span>
|
|
|
- <span class="original-value">{{ val.originalValue }}</span>
|
|
|
+ <div class="element-value-row" v-for="val in getElementValues(elem.elementKey)" :key="val.valueId">
|
|
|
+ <el-input v-model="val.valueText" placeholder="暂无值" size="small" @change="onValueChange(val)" />
|
|
|
+ <div class="value-meta" v-if="val.fillSource">
|
|
|
+ <span class="original-label">来源:</span>
|
|
|
+ <span class="original-value">{{ val.fillSource }}</span>
|
|
|
</div>
|
|
|
<div class="value-status">
|
|
|
<el-tag v-if="val.isFilled" type="success" size="small">已填充</el-tag>
|
|
|
@@ -539,6 +544,14 @@ const executingRules = ref(false)
|
|
|
const docAttachmentId = ref(null)
|
|
|
const docContent = ref(null)
|
|
|
const docLoading = ref(false)
|
|
|
+const docHtml = ref('')
|
|
|
+const docPaperRef = ref(null)
|
|
|
+const highlightEnabled = ref(true)
|
|
|
+const elementHighlightCount = ref(0)
|
|
|
+const highlightPopover = reactive({
|
|
|
+ visible: false, x: 0, y: 0,
|
|
|
+ elementKey: '', fullElementKey: '', elementName: '', currentValue: '', originalValue: '', valueId: null
|
|
|
+})
|
|
|
|
|
|
const showNewProjectDialog = ref(false)
|
|
|
const showAddElementDialog = ref(false)
|
|
|
@@ -687,18 +700,21 @@ async function handleCopyProject() {
|
|
|
|
|
|
function handleSave() { saved.value = true; ElMessage.success('保存成功') }
|
|
|
|
|
|
-// ==================== 文档预览 ====================
|
|
|
+// ==================== 文档预览 + 可编辑 + 要素高亮 ====================
|
|
|
|
|
|
async function loadDocContent(attId) {
|
|
|
if (!attId) return
|
|
|
docLoading.value = true
|
|
|
docContent.value = null
|
|
|
+ docHtml.value = ''
|
|
|
try {
|
|
|
const data = await attachmentApi.getDocContent(attId)
|
|
|
docContent.value = data
|
|
|
+ renderDocHtml()
|
|
|
} catch (e) {
|
|
|
console.warn('加载文档内容失败:', e)
|
|
|
docContent.value = null
|
|
|
+ docHtml.value = ''
|
|
|
} finally {
|
|
|
docLoading.value = false
|
|
|
}
|
|
|
@@ -716,6 +732,125 @@ const docPaperStyle = computed(() => {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+// 从值的 elementKey 中提取不含项目前缀的 key
|
|
|
+// 例如 "PRJ-2024-001:basicInfo.projectCode" -> "basicInfo.projectCode"
|
|
|
+function stripValueKeyPrefix(valueElementKey) {
|
|
|
+ const idx = valueElementKey?.indexOf(':')
|
|
|
+ return idx >= 0 ? valueElementKey.substring(idx + 1) : valueElementKey
|
|
|
+}
|
|
|
+
|
|
|
+// 构建要素值映射表,用于在文档中高亮要素值文本
|
|
|
+function buildElementValueMap() {
|
|
|
+ const map = []
|
|
|
+ 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)
|
|
|
+ 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]
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ colorIdx++
|
|
|
+ }
|
|
|
+ // 按文本长度降序排列,优先匹配长文本
|
|
|
+ map.sort((a, b) => b.text.length - a.text.length)
|
|
|
+ return map
|
|
|
+}
|
|
|
+
|
|
|
+// 将文档 blocks 渲染为 HTML 字符串(含要素高亮)
|
|
|
+function renderDocHtml() {
|
|
|
+ if (!docContent.value?.blocks) { docHtml.value = ''; return }
|
|
|
+ const blocks = docContent.value.blocks
|
|
|
+ const elemMap = highlightEnabled.value ? buildElementValueMap() : []
|
|
|
+ let highlightCount = 0
|
|
|
+ const parts = []
|
|
|
+
|
|
|
+ for (const block of blocks) {
|
|
|
+ if (block.type === 'table') {
|
|
|
+ parts.push(renderTableHtml(block))
|
|
|
+ } else {
|
|
|
+ parts.push(renderBlockHtml(block, elemMap, (n) => { highlightCount += n }))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ elementHighlightCount.value = highlightCount
|
|
|
+ docHtml.value = parts.join('')
|
|
|
+}
|
|
|
+
|
|
|
+function renderBlockHtml(block, elemMap, countFn) {
|
|
|
+ const tag = getBlockTag(block.type)
|
|
|
+ const cls = `doc-block doc-${block.type}`
|
|
|
+ const styleStr = buildStyleStr(block.style)
|
|
|
+ const styleAttr = styleStr ? ` style="${styleStr}"` : ''
|
|
|
+
|
|
|
+ let inner = ''
|
|
|
+ // 图片
|
|
|
+ if (block.images?.length > 0) {
|
|
|
+ for (const img of block.images) {
|
|
|
+ const imgStyle = []
|
|
|
+ if (img.widthInch) imgStyle.push(`width:${img.widthInch}in`)
|
|
|
+ if (img.heightInch) imgStyle.push(`height:${img.heightInch}in`)
|
|
|
+ imgStyle.push('max-width:100%', 'display:block', 'margin:8px auto')
|
|
|
+ inner += `<img src="${img.src}" style="${imgStyle.join(';')}" class="doc-inline-image" contenteditable="false" />`
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Runs
|
|
|
+ if (block.runs) {
|
|
|
+ let runsHtml = ''
|
|
|
+ for (const run of block.runs) {
|
|
|
+ const text = escapeHtml(run.text)
|
|
|
+ const rs = buildRunStyleStr(run)
|
|
|
+ runsHtml += rs ? `<span style="${rs}">${text}</span>` : text
|
|
|
+ }
|
|
|
+ // 注入要素高亮
|
|
|
+ if (elemMap.length > 0 && runsHtml.length > 0) {
|
|
|
+ let count = 0
|
|
|
+ for (const em of elemMap) {
|
|
|
+ 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>`
|
|
|
+ runsHtml = runsHtml.split(escaped).join(hl)
|
|
|
+ count++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (count > 0) countFn(count)
|
|
|
+ }
|
|
|
+ inner += runsHtml
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!inner) inner = ' '
|
|
|
+ return `<${tag} class="${cls}"${styleAttr} data-block-id="${block.id}">${inner}</${tag}>`
|
|
|
+}
|
|
|
+
|
|
|
+function renderTableHtml(block) {
|
|
|
+ const t = block.table
|
|
|
+ if (!t?.data) return ''
|
|
|
+ let html = `<table class="doc-table" data-block-id="${block.id}">`
|
|
|
+ for (let ri = 0; ri < t.data.length; ri++) {
|
|
|
+ html += '<tr>'
|
|
|
+ for (const cell of t.data[ri]) {
|
|
|
+ const cs = cell.colspan > 1 ? ` colspan="${cell.colspan}"` : ''
|
|
|
+ html += `<td class="doc-table-cell"${cs}>${escapeHtml(cell.text)}</td>`
|
|
|
+ }
|
|
|
+ html += '</tr>'
|
|
|
+ }
|
|
|
+ html += '</table>'
|
|
|
+ return html
|
|
|
+}
|
|
|
+
|
|
|
function getBlockTag(type) {
|
|
|
if (type === 'heading1') return 'h1'
|
|
|
if (type === 'heading2') return 'h2'
|
|
|
@@ -724,49 +859,118 @@ function getBlockTag(type) {
|
|
|
return 'p'
|
|
|
}
|
|
|
|
|
|
-function getBlockStyle(style) {
|
|
|
- if (!style) return {}
|
|
|
- const s = {}
|
|
|
+function buildStyleStr(style) {
|
|
|
+ if (!style) return ''
|
|
|
+ const parts = []
|
|
|
if (style.alignment) {
|
|
|
const map = { left: 'left', center: 'center', right: 'right', justify: 'justify', both: 'justify' }
|
|
|
- s.textAlign = map[style.alignment] || style.alignment
|
|
|
+ parts.push(`text-align:${map[style.alignment] || style.alignment}`)
|
|
|
+ }
|
|
|
+ if (style.indentLeft) parts.push(`padding-left:${style.indentLeft / 914400}in`)
|
|
|
+ if (style.indentRight) parts.push(`padding-right:${style.indentRight / 914400}in`)
|
|
|
+ if (style.indentFirstLine) parts.push(`text-indent:${style.indentFirstLine / 914400}in`)
|
|
|
+ if (style.indentHanging) parts.push(`text-indent:-${style.indentHanging / 914400}in`)
|
|
|
+ if (style.spacingBefore) parts.push(`margin-top:${style.spacingBefore / 914400}in`)
|
|
|
+ if (style.spacingAfter) parts.push(`margin-bottom:${style.spacingAfter / 914400}in`)
|
|
|
+ if (style.lineSpacing && style.lineSpacing >= 1 && style.lineSpacing <= 5) parts.push(`line-height:${style.lineSpacing}`)
|
|
|
+ return parts.join(';')
|
|
|
+}
|
|
|
+
|
|
|
+function buildRunStyleStr(run) {
|
|
|
+ const parts = []
|
|
|
+ if (run.fontFamily) parts.push(`font-family:${run.fontFamily}`)
|
|
|
+ if (run.fontSize) parts.push(`font-size:${run.fontSize}pt`)
|
|
|
+ if (run.bold) parts.push('font-weight:bold')
|
|
|
+ if (run.italic) parts.push('font-style:italic')
|
|
|
+ if (run.color) parts.push(`color:${run.color.startsWith('#') ? run.color : '#' + run.color}`)
|
|
|
+ if (run.underline) parts.push('text-decoration:underline')
|
|
|
+ if (run.strikeThrough) parts.push('text-decoration:line-through')
|
|
|
+ return parts.join(';')
|
|
|
+}
|
|
|
+
|
|
|
+function escapeHtml(text) {
|
|
|
+ if (!text) return ''
|
|
|
+ return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
|
+}
|
|
|
+
|
|
|
+function escapeAttr(text) {
|
|
|
+ if (!text) return ''
|
|
|
+ return text.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''')
|
|
|
+}
|
|
|
+
|
|
|
+function darkenColor(hex) {
|
|
|
+ // 简单加深颜色用于下边框
|
|
|
+ const map = {
|
|
|
+ '#fff3cd': '#e0a800', '#cce5ff': '#3d8bfd', '#d4edda': '#28a745',
|
|
|
+ '#f8d7da': '#dc3545', '#e2d5f1': '#6f42c1', '#d1ecf1': '#17a2b8',
|
|
|
+ '#ffeeba': '#d39e00', '#c3e6cb': '#1e7e34', '#f5c6cb': '#c82333',
|
|
|
+ '#d6d8db': '#6c757d'
|
|
|
}
|
|
|
- if (style.indentLeft) s.paddingLeft = `${style.indentLeft / 914400}in`
|
|
|
- if (style.indentRight) s.paddingRight = `${style.indentRight / 914400}in`
|
|
|
- if (style.indentFirstLine) s.textIndent = `${style.indentFirstLine / 914400}in`
|
|
|
- if (style.indentHanging) s.textIndent = `-${style.indentHanging / 914400}in`
|
|
|
- if (style.spacingBefore) s.marginTop = `${style.spacingBefore / 914400}in`
|
|
|
- if (style.spacingAfter) s.marginBottom = `${style.spacingAfter / 914400}in`
|
|
|
- if (style.lineSpacing && style.lineSpacing >= 1 && style.lineSpacing <= 5) s.lineHeight = style.lineSpacing
|
|
|
- return s
|
|
|
+ return map[hex] || '#999'
|
|
|
}
|
|
|
|
|
|
-function hasRunFormat(run) {
|
|
|
- return run.bold || run.italic || run.fontSize || run.fontFamily || run.color || run.underline || run.strikeThrough || run.verticalAlign
|
|
|
+// 文档编辑事件
|
|
|
+function onDocInput() {
|
|
|
+ saved.value = false
|
|
|
}
|
|
|
|
|
|
-function getRunStyle(run) {
|
|
|
- const s = {}
|
|
|
- if (run.fontFamily) s.fontFamily = run.fontFamily
|
|
|
- if (run.fontSize) s.fontSize = `${run.fontSize}pt`
|
|
|
- if (run.bold) s.fontWeight = 'bold'
|
|
|
- if (run.italic) s.fontStyle = 'italic'
|
|
|
- if (run.color) s.color = run.color.startsWith('#') ? run.color : `#${run.color}`
|
|
|
- if (run.underline) s.textDecoration = 'underline'
|
|
|
- if (run.strikeThrough) s.textDecoration = (s.textDecoration ? s.textDecoration + ' ' : '') + 'line-through'
|
|
|
- return s
|
|
|
+// 点击文档中的高亮要素
|
|
|
+function onDocClick(e) {
|
|
|
+ const target = e.target.closest('.elem-highlight')
|
|
|
+ if (!target) {
|
|
|
+ highlightPopover.visible = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const elemKey = target.dataset.elemKey
|
|
|
+ const valueId = target.dataset.valueId
|
|
|
+ const elem = elements.value.find(el => el.elementKey === elemKey)
|
|
|
+ const val = values.value.find(v => String(v.valueId) === String(valueId)) ||
|
|
|
+ values.value.find(v => stripValueKeyPrefix(v.elementKey) === elemKey)
|
|
|
+
|
|
|
+ if (!elem) return
|
|
|
+
|
|
|
+ const rect = target.getBoundingClientRect()
|
|
|
+ const scrollEl = editorRef.value
|
|
|
+ const scrollRect = scrollEl?.getBoundingClientRect() || { top: 0, left: 0 }
|
|
|
+
|
|
|
+ highlightPopover.elementKey = elemKey
|
|
|
+ highlightPopover.fullElementKey = val?.elementKey || ''
|
|
|
+ highlightPopover.elementName = elem.elementName
|
|
|
+ highlightPopover.currentValue = val?.valueText || ''
|
|
|
+ highlightPopover.originalValue = ''
|
|
|
+ highlightPopover.valueId = val?.valueId || null
|
|
|
+ highlightPopover.x = rect.left - scrollRect.left + scrollEl.scrollLeft
|
|
|
+ highlightPopover.y = rect.bottom - scrollRect.top + scrollEl.scrollTop + 4
|
|
|
+ highlightPopover.visible = true
|
|
|
}
|
|
|
|
|
|
-function getImageStyle(img) {
|
|
|
- const s = { maxWidth: '100%', display: 'block', margin: '8px auto' }
|
|
|
- if (img.widthInch) s.width = `${img.widthInch}in`
|
|
|
- if (img.heightInch) s.height = `${img.heightInch}in`
|
|
|
- return s
|
|
|
+async function savePopoverValue() {
|
|
|
+ if (!highlightPopover.elementKey || !currentProjectId.value) {
|
|
|
+ ElMessage.warning('无法保存:未找到对应的值记录')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ // API: PUT /projects/{projectId}/values/{elementKey} with { valueText }
|
|
|
+ const apiKey = highlightPopover.fullElementKey || highlightPopover.elementKey
|
|
|
+ const result = await valueApi.update(currentProjectId.value, apiKey, { valueText: highlightPopover.currentValue })
|
|
|
+ // 更新本地数据
|
|
|
+ const val = values.value.find(v => stripValueKeyPrefix(v.elementKey) === highlightPopover.elementKey)
|
|
|
+ if (val) {
|
|
|
+ val.valueText = highlightPopover.currentValue
|
|
|
+ val.isFilled = !!highlightPopover.currentValue
|
|
|
+ }
|
|
|
+ highlightPopover.visible = false
|
|
|
+ renderDocHtml()
|
|
|
+ ElMessage.success('要素值已更新')
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error('保存失败: ' + e.message)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 要素/值
|
|
|
-function getElementValues(elementKey) { return values.value.filter(v => v.elementKey === elementKey) }
|
|
|
-function hasFilledValue(elementKey) { return values.value.some(v => v.elementKey === elementKey && v.isFilled) }
|
|
|
+function getElementValues(elementKey) { return values.value.filter(v => stripValueKeyPrefix(v.elementKey) === elementKey) }
|
|
|
+function hasFilledValue(elementKey) { return values.value.some(v => stripValueKeyPrefix(v.elementKey) === elementKey && v.isFilled) }
|
|
|
function onValueChange(val) { saved.value = false; val.isModified = true }
|
|
|
|
|
|
async function handleAddElement() {
|
|
|
@@ -3132,7 +3336,7 @@ onMounted(async () => {
|
|
|
}
|
|
|
|
|
|
// ==========================================
|
|
|
-// 文档视图 - Word 排版还原
|
|
|
+// 文档视图 - Word 排版还原 + 可编辑 + 要素高亮
|
|
|
// ==========================================
|
|
|
.document-view {
|
|
|
display: flex;
|
|
|
@@ -3141,19 +3345,27 @@ onMounted(async () => {
|
|
|
padding: 20px;
|
|
|
background: #e8e8e8;
|
|
|
min-height: 100%;
|
|
|
+ position: relative;
|
|
|
|
|
|
- .doc-attachment-bar {
|
|
|
+ .doc-toolbar {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- gap: 8px;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 12px;
|
|
|
margin-bottom: 16px;
|
|
|
- padding: 10px 16px;
|
|
|
+ padding: 8px 16px;
|
|
|
background: var(--white);
|
|
|
border-radius: var(--radius-md);
|
|
|
box-shadow: var(--shadow-sm);
|
|
|
width: 100%;
|
|
|
max-width: 820px;
|
|
|
|
|
|
+ .doc-toolbar-left, .doc-toolbar-right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
.doc-att-label {
|
|
|
font-size: 13px;
|
|
|
font-weight: 600;
|
|
|
@@ -3283,5 +3495,81 @@ onMounted(async () => {
|
|
|
color: var(--text-2);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+// ==========================================
|
|
|
+// 要素高亮弹出框
|
|
|
+// ==========================================
|
|
|
+.element-popover {
|
|
|
+ position: absolute;
|
|
|
+ z-index: 1000;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ border-radius: var(--radius-md);
|
|
|
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
|
|
|
+ width: 320px;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ .popover-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 10px 14px;
|
|
|
+ background: var(--bg);
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
+
|
|
|
+ .popover-label {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 14px;
|
|
|
+ color: var(--text-1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .popover-body {
|
|
|
+ padding: 12px 14px;
|
|
|
+
|
|
|
+ .popover-field {
|
|
|
+ margin-bottom: 10px;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .popover-field-label {
|
|
|
+ display: block;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--text-3);
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .popover-original {
|
|
|
+ font-size: 13px;
|
|
|
+ color: var(--text-2);
|
|
|
+ background: var(--bg);
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: var(--radius-sm);
|
|
|
+ display: inline-block;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .popover-footer {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 8px 14px;
|
|
|
+ border-top: 1px solid var(--border);
|
|
|
+ background: var(--bg);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 可编辑文档纸张的光标和选区样式
|
|
|
+.doc-paper[contenteditable="true"] {
|
|
|
+ outline: none;
|
|
|
+ cursor: text;
|
|
|
+
|
|
|
+ &:focus {
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15), 0 0 0 2px rgba(24, 144, 255, 0.2);
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|
|
|
|