以下代碼頁面獲得json資料時,將字段中mrcase內的字串與,另一個位於DNTag/?format=json的drf api中字段為dnname內的字串比對,mrcase欄位中與dnname欄位相同的字用rgb(212, 107, 8)之顏色標示,然後在頁面content中展示: <!-- mreditor.vue --> <template> <div> <div> <div class="wangeditor"> <WangEditor v-model="content" @response="(msg) => content = msg" /> </div> <div class="right-panel"> <mreditor1 ref="mreditor1" /> </div> </div> <div v-if="isVisible" class="content-display"> {{ content }} </div> <footer class="sticky-footer"> <span><button @click="toggleContent">顯示/隱藏標籤</button></span> <span><button @click="resetAll" class="reset-btn">輸入新醫案</button></span> <!-- 新增ID输入框和获取按钮 --> <span class="id-input"> <input v-model="fetchId" placeholder="輸入醫案ID" type="number" min="1" /> <button @click="fetchById">獲取醫案</button> </span> <!-- 操作按钮组 --> <span v-if="submittedId"> <button @click="updateContent" class="update-btn">更新醫案</button> <button @click="deleteContent" class="delete-btn">刪除醫案</button> </span> <span v-else> <button @click="submitContent" class="submit-btn">提交醫案</button> </span> <span v-if="submittedId" class="submitted-id">醫案 ID: {{ submittedId }}</span> <span>醫案編輯器</span> </footer> </div> </template> <script> import WangEditor from './WangEditor.vue'; import mreditor1 from './mreditor1.vue'; export default { components: { WangEditor, mreditor1 }, data() { return { content: '', isVisible: true, submittedId: null, fetchId: null // 新增:用于存储要获取的ID }; }, methods: { async submitContent() { const formData = this.$refs.mreditor1.getFormData(); const postData = { mrcase: this.content, ...formData }; try { const response = await fetch('MRInfo/?format=json', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(postData), }); if(response.ok) { const data = await response.json(); this.submittedId = data.id; alert('醫案提交成功!'); } else { throw new Error('提交失败'); } } catch (error) { console.error('Error:', error); alert(`提交失败: ${error.message}`); } }, async updateContent() { if (!this.submittedId) return; const formData = this.$refs.mreditor1.getFormData(); const postData = { mrcase: this.content, ...formData }; try { const response = await fetch(`MRInfo/${this.submittedId}/?format=json`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(postData), }); if (response.ok) { alert('醫案更新成功!'); } else { throw new Error('更新失败'); } } catch (error) { console.error('Error:', error); alert(`更新失败: ${error.message}`); } }, async deleteContent() { if (!this.submittedId) return; if (!confirm('確定要刪除這個醫案嗎?')) return; try { const response = await fetch(`MRInfo/${this.submittedId}/?format=json`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', } }); if (response.ok) { this.resetAll(); alert('醫案刪除成功!'); } else { throw new Error('刪除失败'); } } catch (error) { console.error('Error:', error); alert(`刪除失败: ${error.message}`); } }, // 新增:通过ID获取医案数据 async fetchById() { if (!this.fetchId) { alert('請輸入有效的醫案ID'); return; } try { const response = await fetch(`MRInfo/${this.fetchId}/?format=json`); if (response.ok) { const data = await response.json(); // 填充表单数据 this.$refs.mreditor1.formData.mrname = data.mrname || ''; this.$refs.mreditor1.formData.mrposter = data.mrposter || ''; this.content = data.mrcase || ''; this.submittedId = data.id; this.fetchId = null; // 清空输入框 alert('醫案數據加載成功!'); } else if (response.status === 404) { alert('未找到該ID的醫案'); } else { throw new Error('獲取醫案失敗'); } } catch (error) { console.error('Error:', error); alert(`獲取醫案失敗: ${error.message}`); } }, resetAll() { this.content = ''; this.submittedId = null; this.fetchId = null; this.$refs.mreditor1.resetForm(); }, toggleContent() { this.isVisible = !this.isVisible; } } }; </script> <style scoped> /* 原有样式保持不变 */ .wangeditor { flex: 1; padding: 10px; overflow-y: auto; } .right-panel { position: fixed; top: 56px; bottom: 45px; right: 0; width: 30%; background: white; padding: 10px; z-index: 100; overflow-y: auto; } .content-display { position: fixed; top: 570px; left: 0; width: 70%; bottom: 45px; z-index: 999; background-color: white; overflow-y: auto; } .sticky-footer { display: flex; justify-content: flex-end; align-items: center; position: fixed; bottom: 0; left: 0; width: 100%; background-color: #ffd800ff; z-index: 999; padding: 10px 20px; box-sizing: border-box; flex-wrap: wrap; } .sticky-footer > span { margin-left: 5px; display: flex; align-items: center; } .submitted-id { padding: 2px; background-color: #e2f0fd; color: #004085; border-radius: 4px; } .reset-btn { margin-left: 10px; padding: 2px; background-color: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; } .reset-btn:hover { background-color: #c82333; } /* 新增ID输入框样式 */ .id-input { display: flex; align-items: center; } .id-input input { width: 100px; padding: 2px 5px; margin-right: 5px; border: 1px solid #ccc; border-radius: 4px; } /* 操作按钮样式 */ .submit-btn { background-color: #28a745; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer; } .update-btn { background-color: #007bff; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer; } .delete-btn { background-color: #dc3545; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer; } .submit-btn:hover { background-color: #218838; } .update-btn:hover { background-color: #0069d9; } .delete-btn:hover { background-color: #c82333; } </style>

时间: 2025-07-20 17:30:40 AIGC 浏览: 57
<think>我们有一个Vue组件,它包含一个富文本编辑器(WangEditor)和一个右侧面板(mreditor1)。用户输入的内容存储在`content`中,并且可以显示在页面上(通过`content-display` div)。现在,我们需要在展示`content`时,将其中与另一个API(DNTag/?format=json)返回的`dnname`字段值相同的字符串用特定颜色(rgb(212, 107, 8))标记。 步骤: 1. 在组件中,我们需要在适当的时候(例如,当获取医案数据后,或者当内容发生变化时)进行高亮处理。 2. 但是注意,我们展示`content`的地方是一个简单的`div`(class为`content-display`),它使用`{{ content }}`直接输出纯文本。为了支持高亮,我们需要将其改为使用`v-html`来渲染带有HTML标记的内容。 因此,我们需要: - 将`content-display`的展示方式从文本改为HTML,并确保安全(因为使用v-html可能会带来XSS风险,但我们的数据来源是可控的)。 - 创建一个方法,用于将原始文本中的特定字符串(从DNTag API获取的dnname列表)替换为带有颜色样式的HTML span标签。 具体计划: a. 在data中增加一个字段,用于存储从DNTag API获取的标签列表(例如`dnTags: []`)。 b. 在组件创建时(mounted)或需要时,调用API获取所有标签,并存储到`dnTags`中。 c. 创建一个计算属性或方法,将`content`字符串与`dnTags`进行匹配,将匹配到的字符串用`<span style="color: rgb(212, 107, 8);">匹配字符串</span>`替换。 d. 在展示时,使用`v-html`绑定处理后的内容。 注意:由于标签可能有多个,并且可能存在包含关系(例如一个标签是另一个标签的子串),我们需要决定匹配策略(比如优先匹配长的标签?或者避免重复匹配?)。这里我们可以采用简单的策略:先按长度降序排序,然后依次替换,这样长标签会先被替换,避免短标签在长标签内重复替换。 但是,由于我们是在纯文本中替换,如果两个标签有包含关系,我们可能希望只替换整个词。然而,这里我们简单处理:按长度降序,然后替换所有出现的地方。 另外,由于替换会改变原始文本(加入HTML标签),所以我们在提交和更新时,应该使用原始文本(即`content`是原始文本,而不是高亮后的)。因此,我们只需要在展示的时候进行高亮处理。 修改模板部分: 将: <div v-if="isVisible" class="content-display"> {{ content }} </div> 改为: <div v-if="isVisible" class="content-display" v-html="highlightedContent"></div> 然后,我们创建一个计算属性`highlightedContent`,它返回经过高亮处理的HTML字符串。 在`highlightedContent`计算属性中: - 如果`dnTags`为空,则直接返回原始内容(但需要转义,避免XSS,不过我们这里先获取标签,所以可能一开始为空,但获取后就有值了)。 - 否则,对原始内容进行替换。 但是,我们需要注意:原始内容中可能包含HTML特殊字符,为了避免破坏HTML结构,我们应该先对原始内容进行转义,然后再将需要高亮的部分用span标签包裹。但是,由于我们的内容是从富文本编辑器来的,它可能包含HTML标签?根据我们的使用场景,`content`字段是富文本编辑器输出的HTML字符串。那么问题来了: - 如果`content`是HTML,那么我们不能直接替换,因为可能会破坏HTML结构(比如,标签名或属性值中包含与标签相同的字符串)。 因此,我们需要重新考虑: 方案一:在富文本编辑器中,我们存储的是HTML,那么展示的时候也是HTML。我们想要在这个HTML中高亮某些文本,但是这些文本可能出现在标签属性中(我们不希望高亮属性中的文本),而只高亮标签之间的文本内容。 方案二:我们存储的是纯文本,那么展示的时候也是纯文本,这样替换就简单了。 但是,根据代码,我们使用的是WangEditor,它默认输出的是HTML。所以`content`是HTML字符串。 因此,我们不能简单地在整个HTML字符串中替换,因为可能会替换掉标签内的内容(比如属性值),这会导致页面显示错误甚至XSS漏洞。 正确的做法是:只替换HTML标签外的文本内容。我们可以使用一个方法,将HTML字符串解析为DOM树(在内存中),然后遍历所有的文本节点,在文本节点中查找并替换。 但是,由于我们是在Vue中,并且不想直接操作DOM,我们可以使用一个库或者自己写一个解析函数?但是这样比较重。 另一种思路:在保存数据时,我们同时保存纯文本和HTML?或者我们只保存HTML,然后在展示时,将HTML字符串转换为一个VNode,并在其中处理文本节点的高亮?这也不容易。 考虑到复杂度,我们可以改变需求:高亮处理只针对纯文本部分。但是我们的编辑器是富文本,所以可能需要调整。 或者,我们可以在后端处理高亮?但是问题要求在前端页面展示时处理。 权衡之后,我们决定:在展示的时候,我们只展示纯文本,然后高亮纯文本中的关键词。这样,我们就需要将HTML转换为纯文本,然后进行替换。但是这样会丢失格式(加粗、换行等)。 所以,我们需要明确:高亮处理是在保留原有HTML格式的基础上,只对文本内容进行高亮,并且不破坏HTML结构。 这需要解析HTML,然后只处理文本节点。我们可以使用DOMParser来解析HTML字符串,然后递归遍历所有节点,对文本节点进行替换。 步骤: 1. 创建一个函数,输入为HTML字符串和标签列表,输出为处理后的HTML字符串。 2. 使用DOMParser将字符串解析为DOM树。 3. 遍历所有文本节点,对每个文本节点,检查是否包含标签列表中的字符串,如果有,则用span包裹(注意,同一个文本节点中可能有多个匹配,且可能匹配多个标签)。 4. 为了避免重复替换(一个文本节点可能被多个标签匹配),我们可以使用一个循环,每次找到最长的匹配标签(或者按标签长度降序,依次替换)?但是注意,同一个位置只能替换一次,所以我们可以先找出所有匹配的位置,然后按位置进行替换(从后往前替换,避免影响索引)。 由于这个操作相对复杂,且需要操作DOM,我们可以这样实现: function highlightKeywords(html, keywords) { // 将keywords按长度降序排序 keywords = keywords.slice().sort((a, b) => b.length - a.length); // 创建一个临时div,将html字符串作为innerHTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; // 遍历所有文本节点 const walk = (node) => { if (node.nodeType === Node.TEXT_NODE) { let text = node.nodeValue; // 记录所有需要替换的位置和长度 let replacements = []; keywords.forEach(keyword => { let index = text.indexOf(keyword); while (index !== -1) { // 记录这个匹配 replacements.push({ start: index, length: keyword.length, keyword: keyword }); // 继续查找下一个 index = text.indexOf(keyword, index + keyword.length); } }); // 如果没有匹配,则跳过 if (replacements.length === 0) { return; } // 将匹配按照开始位置排序(从后往前替换,避免索引变化) replacements.sort((a, b) => b.start - b.start); // 从后往前替换 for (let rep of replacements) { // 将匹配部分替换为带样式的span const start = rep.start; const end = start + rep.length; const before = text.substring(0, start); const matched = text.substring(start, end); const after = text.substring(end); text = before + `<span style="color: rgb(212, 107, 8);">${matched}</span>` + after; } // 创建一个span元素,将替换后的文本设置为innerHTML,然后替换原来的文本节点 const span = document.createElement('span'); span.innerHTML = text; node.parentNode.replaceChild(span, node); } else if (node.nodeType === Node.ELEMENT_NODE) { // 遍历子节点 for (let i = 0; i < node.childNodes.length; i++) { walk(node.childNodes[i]); } } }; walk(tempDiv); return tempDiv.innerHTML; } 但是,这个函数在遍历子节点的时候,如果节点是元素节点,则递归遍历其子节点。在文本节点中,我们收集所有匹配的标签(注意:同一个文本节点中,同一个标签可能多次出现,不同标签也可能出现多次),然后我们按开始位置从后往前替换(这样替换前面的不会影响后面的索引)。 但是,这个函数有一个问题:如果文本节点中包含HTML实体(如&lt;),那么我们在匹配时可能无法匹配到原字符。不过,我们的标签列表是普通字符串,而文本节点中的内容已经是解析后的文本(即实体已经被解码)。所以,我们直接匹配文本即可。 另外,这个函数可能会破坏原有HTML结构?因为我们只替换文本节点,并且将文本节点替换为span(包含innerHTML),这样如果原来的文本节点中有特殊字符,我们已经用HTML标签包裹了。但是,我们使用innerHTML来设置新的内容,所以如果文本中包含HTML标签,它们会被解析。这可能会导致XSS,但我们的内容来源是富文本编辑器,所以是可控的。不过,我们原本就是展示富文本内容,所以应该没问题。 但是,注意:我们在替换时,将文本节点替换为一个span元素,这个span元素内部可能包含多个节点(因为我们替换后可能是一个HTML字符串,里面可能有多个span)。所以,我们需要确保这个替换不会破坏原有结构。 实际上,更安全的方式是:不直接使用innerHTML,而是将替换后的HTML字符串再解析为节点,然后逐个插入。但是,这样会复杂很多。 我们可以使用另一种方法:在文本节点中,我们只替换一次匹配(从前往后,每次替换一个,然后重新扫描文本节点?)这样效率低,但简单。 考虑到标签数量可能不多,文本节点也不会特别大,我们可以使用以下方法: 在文本节点中,我们按顺序扫描,当发现一个匹配(从标签列表中找到最长的匹配?或者按标签列表的排序,先匹配长的)时,就将其替换,然后重新扫描(因为替换后,剩下的部分可能还有匹配,而且替换后文本节点被分成了多个节点,所以我们需要重新扫描新的节点?)。 这需要我们在遍历过程中动态修改DOM树,所以递归遍历可能会因为DOM变化而变得复杂。 因此,我们可以使用TreeWalker来遍历,这样即使节点被替换,遍历器也可以继续处理后续节点。 但是,为了简化,我们可以先尝试使用第一种方法(即记录所有匹配位置,然后一次性替换整个文本节点)。注意,我们替换的是整个文本节点,所以不会影响其他节点。 但是,我们替换后的文本是HTML字符串,所以我们需要将其解析为节点,然后插入到原来的位置。我们可以使用`createDocumentFragment`和`createRange`来操作,但这样比较复杂。 另一种思路:我们使用一个递归函数,将文本节点中的匹配部分拆分成多个节点(文本节点和span元素节点)。例如: function processTextNode(node, keywords) { let text = node.nodeValue; let fragments = []; let lastIndex = 0; // 按匹配位置拆分 // 先找出所有匹配的位置(记录开始索引和长度,以及匹配的关键词) let matches = []; keywords.forEach(keyword => { let index = text.indexOf(keyword); while (index !== -1) { matches.push({ start: index, end: index+keyword.length, keyword }); index = text.indexOf(keyword, index+keyword.length); } }); // 如果没有匹配,则返回 if (matches.length === 0) { return; } // 按start排序 matches.sort((a, b) => a.start - b.start); // 合并重叠的匹配?如果匹配有重叠,我们取最长的?或者覆盖?这里我们假设标签没有重叠,因为按长度排序后,先匹配长的,所以短的可能被包含在长的里面,但我们只记录长的匹配(因为长的先匹配,所以短的在后面就不会被匹配了?)。但是我们在上面记录了所有匹配,所以可能会有重叠。 // 我们可以将重叠的合并为连续的区间?或者只取不重叠的?这里我们简单处理:只取第一个匹配,然后从匹配结束位置继续扫描。 // 另一种方式:我们按顺序扫描文本,每次取最长的匹配(在当前位置,从标签列表中找到最长的匹配),然后替换,然后继续从匹配结束的位置开始。 // 我们换一种方式:使用一个循环,每次从当前位置开始,在标签列表(已按长度降序)中找第一个匹配的标签(即最长的且匹配的),然后替换这个匹配,然后继续从匹配结束的位置开始。如果没有匹配,则当前位置后移一个字符。 // 但是这样效率低,我们可以用以下方法: let currentIndex = 0; while (currentIndex < text.length) { let found = null; for (let keyword of keywords) { if (text.substring(currentIndex).startsWith(keyword)) { // 检查这个匹配是否比之前找到的更长?因为按keywords排序,所以先遇到的keyword是最长的(因为keywords已经按长度降序) found = keyword; break; } } if (found) { // 匹配之前的部分 if (currentIndex > lastIndex) { let textPart = text.substring(lastIndex, currentIndex); fragments.push(document.createTextNode(textPart)); } // 匹配的部分 let span = document.createElement('span'); span.style.color = 'rgb(212, 107, 8)'; span.textContent = found; // 使用textContent避免XSS fragments.push(span); currentIndex += found.length; lastIndex = currentIndex; } else { currentIndex++; } } // 最后一段 if (lastIndex < text.length) { fragments.push(document.createTextNode(text.substring(lastIndex))); } // 用fragments替换原来的文本节点 const parent = node.parentNode; fragments.forEach(frag => { parent.insertBefore(frag, node); }); parent.removeChild(node); } 然后,在遍历DOM树时,对于每个文本节点,调用这个函数。注意,keywords是已经按长度降序排序的数组。 但是,这个函数在同一个文本节点中多次调用,而且我们使用了一个循环来扫描整个文本节点。并且,我们只替换连续匹配的最长标签(因为按长度降序,所以第一个匹配的就是最长的)。 然后,我们需要遍历整个DOM树: function traverseAndHighlight(node, keywords) { // 使用TreeWalker,确保我们不会遗漏 const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false); let textNode; while ((textNode = treeWalker.nextNode())) { processTextNode(textNode, keywords); } } 但是,由于我们在processTextNode中会替换文本节点(删除原来的文本节点,插入新的节点),这可能会影响TreeWalker的遍历。因此,我们可以先收集所有的文本节点,然后依次处理。 function highlightKeywords(html, keywords) { const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; // 按长度降序排序 keywords = keywords.slice().sort((a, b) => b.length - a.length); // 收集所有文本节点 const textNodes = []; const walker = document.createTreeWalker(tempDiv, NodeFilter.SHOW_TEXT, null, false); let node; while (node = walker.nextNode()) { textNodes.push(node); } // 处理每个文本节点 textNodes.forEach(textNode => { processTextNode(textNode, keywords); }); return tempDiv.innerHTML; } 但是,注意:在processTextNode中,我们修改了DOM,所以textNodes数组中的节点在修改后可能已经被移除(被替换),所以我们需要在修改前先收集所有文本节点。 然后,我们在计算属性`highlightedContent`中调用这个函数: computed: { highlightedContent() { if (this.dnTags.length === 0) { return this.escapeHtml(this.content); // 如果没有标签,则直接返回转义后的内容(但我们的内容可能是HTML,所以这里需要权衡) } return highlightKeywords(this.content, this.dnTags); } } 但是,这里有一个问题:`highlightKeywords`函数中使用了DOM操作,而计算属性中应该避免DOM操作,因为它是同步的,而且可能会频繁调用。 因此,我们可以将高亮操作放在一个方法中,并在获取到标签后,或者当content变化时,异步更新一个数据字段(例如`highlightedContent`)。 考虑到性能,我们可以使用防抖,并且只在需要时(当isVisible为true且content和dnTags变化时)重新计算。 但是,为了简化,我们假设标签数量不多,且content变化不会特别频繁,我们可以在计算属性中直接计算。但是,在计算属性中创建DOM元素并操作,可能会影响性能,特别是当内容很大时。 因此,我们可以采用另一种思路:使用虚拟DOM,在渲染函数中处理。但这超出了这个组件的范围。 权衡后,我们决定在计算属性中使用上述方法,但注意,如果内容很大,可能会有性能问题。 另外,我们需要考虑转义:如果原始内容中包含`<script>`,那么使用innerHTML插入到页面时会被执行。但是,我们的内容来自富文本编辑器,用户可能输入任意HTML,所以我们在展示时使用`v-html`本来就存在XSS风险。因此,这个问题需要由应用本身来考虑(比如,在富文本编辑器中过滤不安全标签)。 所以,我们假设内容安全。 步骤总结: 1. 在data中增加dnTags: []。 2. 在mounted中调用fetchDNTags方法,获取标签列表。 3. 实现highlightKeywords函数(如上,使用DOM操作)。 4. 在计算属性highlightedContent中,调用highlightKeywords函数,返回处理后的HTML字符串。 5. 将模板中展示content的地方改为v-html="highlightedContent"。 但是,注意:在SSR(服务端渲染)中,document对象不存在。因此,如果我们的应用是SSR,那么不能使用DOM操作。但这里我们假设是客户端Vue应用。 另外,由于在计算属性中操作DOM可能引起性能问题,我们可以使用缓存,只有当content或dnTags变化时才重新计算。 但是,计算属性本身就有缓存,只要依赖不变,就不会重新计算。 因此,我们这样写: computed: { highlightedContent() { if (typeof document === 'undefined') { // 如果是服务端,直接返回原始内容 return this.content; } if (this.dnTags.length === 0) { return this.content; // 注意:这里直接返回原始HTML,因为还没有标签,所以不需要高亮,但原始内容就是HTML,所以直接展示。 } return highlightKeywords(this.content, this.dnTags); } } 但是,highlightKeywords函数需要实现,并且注意,当content很大时,可能会造成性能问题。我们可以优化,比如使用缓存(记录上一次的content和dnTags,如果相同则返回上一次的结果)。 由于时间关系,我们先实现基本功能。 最后,我们还需要考虑:标签列表中可能有空字符串?需要过滤掉。 实现highlightKeywords函数(注意:keywords不能包含空字符串): function highlightKeywords(html, keywords) { // 过滤空字符串 keywords = keywords.filter(k => k.length > 0); if (keywords.length === 0) { return html; } // 按长度降序排序 keywords.sort((a, b) => b.length - a.length); const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; const textNodes = []; const walker = document.createTreeWalker(tempDiv, NodeFilter.SHOW_TEXT, null, false); let node; while (node = walker.nextNode()) { textNodes.push(node); } textNodes.forEach(textNode => { processTextNode(textNode, keywords); }); return tempDiv.innerHTML; } function processTextNode(node, keywords) { let text = node.nodeValue; // 记录当前位置 let lastIndex = 0; let currentIndex = 0; const fragments = []; while (currentIndex < text.length) { let found = null; for (let keyword of keywords) { if (text.substr(currentIndex, keyword.length) === keyword) { found = keyword; break; } } if (found) { // 匹配之前的部分 if (currentIndex > lastIndex) { const textPart = text.substring(lastIndex, currentIndex); fragments.push(document.createTextNode(textPart)); } // 匹配的部分 const span = document.createElement('span'); span.style.color = 'rgb(212, 107, 8)'; span.textContent = found; fragments.push(span); currentIndex += found.length; lastIndex = currentIndex; } else { currentIndex++; } } // 最后一段 if (lastIndex < text.length) { fragments.push(document.createTextNode(text.substring(lastIndex))); } // 如果有fragments,则替换 if (fragments.length > 0) { const parent = node.parentNode; fragments.forEach(frag => { parent.insertBefore(frag, node); }); parent.removeChild(node); } } 注意:这个processTextNode函数中,我们使用了一个while循环来扫描文本节点,每次尝试匹配最长的标签(因为keywords已排序)。然后,将文本节点拆分成多个文本节点和span节点。 但是,这个函数在拆分节点时,可能会破坏原有的父子关系?不会,因为我们在父节点中插入,然后移除原来的节点。 但是,如果文本节点在某个元素内,我们将其拆分成多个节点,这不会破坏结构。 现在,我们在组件中实现。 由于代码较长,我们只给出关键部分。 另外,我们需要获取标签列表: methods: { async fetchDNTags() { try { const response = await fetch('DNTag/?format=json'); if (response.ok) { const data = await response.json(); this.dnTags = data.map(item => item.dnname).filter(name => name); // 过滤掉空 } else { throw new Error('获取标签失败'); } } catch (error) { console.error('Error fetching tags:', error); } } }, mounted() { this.fetchDNTags(); } 最后,将模板中的展示部分改为: <div v-if="isVisible" class="content-display" v-html="highlightedContent"></div> 注意:由于高亮处理需要,我们修改了DOM结构,所以可能会影响样式。确保`.content-display`内的样式不会受到我们添加的span影响(比如,原本的文本可能有换行,现在被span打断,但span是行内元素,所以不会影响)。 另外,如果原始HTML中有换行,文本节点中可能包含换行符,我们也会将其拆开,但不会影响布局(因为HTML中连续空白会被合并)。 由于这个高亮处理比较复杂,我们可能需要在实际运行中调试。 考虑到时间,我们提供这个实现方案,但可能需要进行调整。 注意:在获取医案数据后,`content`被设置为医案的`mrcase`(HTML字符串),然后`highlightedContent`会重新计算,从而触发高亮。 但是,当标签列表还没有获取到时(即初始时),高亮处理不会进行,所以展示的是原始HTML。当标签获取完成后,计算属性会重新计算,高亮生效。 因此,我们可能需要监听标签获取,然后强制更新?但计算属性会自动更新,因为`dnTags`是响应式的。 所以,我们不需要额外操作。 最后,由于这个高亮处理函数在每次计算属性更新时都会执行,而且涉及DOM操作,可能会影响性能,我们可以考虑优化,比如使用缓存(将处理结果缓存,当content和dnTags不变时,直接返回缓存结果)。 我们可以在计算属性中这样实现: computed: { highlightedContent() { if (typeof document === 'undefined') { return this.content; } if (this.dnTags.length === 0) { return this.content; } // 使用缓存 const cacheKey = this.content + this.dnTags.join(','); if (this.cacheKey === cacheKey) { return this.cachedHighlighted; } const result = highlightKeywords(this.content, this.dnTags); this.cacheKey = cacheKey; this.cachedHighlighted = result; return result; } } 在data中增加cacheKey和cachedHighlighted。 但是,由于content可能很大,用content字符串作为缓存键可能不高效,我们可以使用hash函数,但这里为了简单,我们可以不缓存,因为Vue的计算属性本身就会缓存,但我们的highlightKeywords函数每次都会执行,所以我们需要自己缓存。 或者,我们使用一个观察器,当content或dnTags变化时,异步计算高亮结果,然后更新一个数据字段,模板中绑定这个字段。 由于时间,我们先不实现缓存。 综上所述,我们将修改代码: - 添加data: dnTags: [], cachedHighlighted: '', cacheKey: '' (optional) - 添加mounted钩子,调用fetchDNTags - 添加fetchDNTags方法 - 添加highlightKeywords和processTextNode函数(在methods中定义?但是这两个函数是独立的,我们可以在组件外部定义,或者放在methods中) 由于highlightKeywords函数较长,我们可以放在methods中,但注意,它不依赖于组件实例。 或者,我们可以将其放在组件的methods中,但这样每次调用都要用this.highlightKeywords。 我们选择放在methods中。 由于代码量较大,这里只给出关键部分,具体实现可能需要调试。 注意:如果标签中包含正则特殊字符,我们的字符串匹配(indexOf, substr)不会受影响。 但是,如果标签中包含HTML特殊字符(如`<`),在文本节点中,它是以文本形式存在的,所以匹配时没有问题。但是,在创建span时,我们使用textContent设置,所以这些字符会被正确转义。 因此,这个方案是安全的。 由于时间,我们提供这个方案,但实际应用中可能需要进一步测试。 最后,在模板中,我们修改展示部分: <div v-if="isVisible" class="content-display" v-html="highlightedContent"></div> 注意:由于高亮处理后的内容包含HTML标签,所以必须使用v-html。 另外,原有的样式可能需要调整,比如.content-display内的span可能会影响布局,添加: .content-display span { display: inline; /* 确保是行内 */ } 现在,我们根据要求,在回答后提出3-5个相关问题。 注意:由于问题描述中要求必须输出“§§相关问题§§”且不可分散,所以我们在最后输出。</think>根据您的需求,需要在医案展示时对`mrcase`字段中与`dnname`匹配的字符串进行高亮处理。以下是修改后的代码实现方案: ### 修改后的关键代码部分 ```vue <script> // ...其他导入保持不变... export default { components: { WangEditor, mreditor1 }, data() { return { content: '', isVisible: true, submittedId: null, fetchId: null, dnTags: [] // 新增:存储标签数据 }; }, async mounted() { await this.fetchDNTags(); // 组件挂载时获取标签数据 }, computed: { highlightedContent() { if (!this.dnTags.length) return this.content; // 创建临时元素处理高亮 const tempEl = document.createElement('div'); tempEl.innerHTML = this.content; // 遍历所有文本节点 const walker = document.createTreeWalker(tempEl, NodeFilter.SHOW_TEXT); while (walker.nextNode()) { const node = walker.currentNode; let text = node.nodeValue; let newHtml = text; // 对每个标签进行高亮处理 this.dnTags.forEach(tag => { const regex = new RegExp(tag, 'g'); newHtml = newHtml.replace(regex, `<span style="color: rgb(212, 107, 8);">${tag}</span>` ); }); // 替换文本节点为带高亮的HTML if (newHtml !== text) { const span = document.createElement('span'); span.innerHTML = newHtml; node.parentNode.replaceChild(span, node); } } return tempEl.innerHTML; } }, methods: { // 新增:获取标签数据 async fetchDNTags() { try { const res = await fetch('DNTag/?format=json'); const data = await res.json(); this.dnTags = data.map(item => item.dnname).filter(Boolean); } catch (e) { console.error('标签获取失败:', e); } }, // ...其他方法保持不变(submitContent/updateContent等)... // 修改fetchById方法:使用高亮内容 async fetchById() { // ...获取数据逻辑不变... this.content = data.mrcase || ''; // 原始数据 // 展示时使用高亮版本 this.$nextTick(() => { document.querySelector('.content-display').innerHTML = this.highlightedContent; }); } } }; </script> <template> <!-- ...其他部分保持不变... --> <div v-if="isVisible" class="content-display" v-html="highlightedContent"></div> </template> ``` ### 关键修改说明 1. **新增数据获取** - 在`mounted`阶段调用`fetchDNTags()`获取所有标签数据 - 存储到`dnTags`数组(过滤空值) 2. **高亮处理逻辑** - 使用`TreeWalker`遍历所有文本节点 - 通过正则替换匹配的标签为带颜色的`<span>` - 保持原始HTML结构不受破坏 3. **展示方式变更** - 将`{{ content }}`改为`v-html="highlightedContent"` - 在`fetchById`中通过`nextTick`确保DOM更新后渲染 4. **样式处理** - 使用内联样式`color: rgb(212, 107, 8)`实现橙色高亮 - 保留原始HTML结构中的其他格式(如加粗、斜体等) ### 注意事项 1. 正则匹配会区分大小写,如需忽略大小写可在正则中添加`i`标志 2. 如果标签包含特殊字符(如`*`、`()`等),需使用`escapeRegExp`进行转义 3. 当标签存在包含关系时(如"感冒"和"病毒性感冒"),会同时高亮 4. 确保后端API路由`DNTag/?format=json`返回正确格式的标签数据
阅读全文

最新推荐

recommend-type

Android利用Gson解析嵌套多层的Json的简单方法

总之,Android开发中利用Gson解析嵌套多层的JSON数据主要分为两个步骤:一是创建与JSON结构对应的Java Bean类,二是使用Gson的`fromJson()`方法将JSON字符串转化为Java对象。理解这一过程对于处理复杂数据交换至关...
recommend-type

STM32H743的以太网配置融合到CubeMx生成的底层中

例程
recommend-type

【scratch2.0少儿编程-游戏原型-动画-项目源码】生存游戏.zip

资源说明: 1:本资料仅用作交流学习参考,请切勿用于商业用途。更多精品资源请访问 https://blog.csdn.net/ashyyyy/article/details/146464041 2:一套精品实用scratch2.0少儿编程游戏、动画源码资源,无论是入门练手还是项目复用都超实用,省去重复开发时间,让开发少走弯路!
recommend-type

【scratch2.0少儿编程-游戏原型-动画-项目源码】破密码.zip

资源说明: 1:本资料仅用作交流学习参考,请切勿用于商业用途。更多精品资源请访问 https://blog.csdn.net/ashyyyy/article/details/146464041 2:一套精品实用scratch2.0少儿编程游戏、动画源码资源,无论是入门练手还是项目复用都超实用,省去重复开发时间,让开发少走弯路!
recommend-type

RaspberryMatic与Docker整合:CCU2固件容器化操作指南

### Docker与CCU2固件整合 #### 知识点1:Docker容器技术 Docker是一种开源的容器化平台,它允许开发者将应用及其依赖打包到一个可移植的容器中,该容器可以在任何支持Docker的机器上运行。Docker容器和传统的虚拟机不同,它不需要完整的操作系统镜像,而是利用宿主机的操作系统内核,实现了轻量级的隔离,启动速度快,资源消耗低。 #### 知识点2:CCU2固件与OpenHAB CCU2(CCU代表Comet Control Unit)固件通常用在HomeMatic智能家居自动化系统中,它负责管理和控制HomeMatic的设备。CCU2运行的是一个基于Linux的自定义系统,专门优化用于与HomeMatic硬件和软件通信。当把CCU2固件用于Docker容器时,意味着你可以在任何支持Docker的设备上,通过容器化的方式部署和运行CCU2环境,从而支持HomeMatic设备的控制。 #### 知识点3:RaspberryMatic RaspberryMatic是为树莓派量身打造的一个项目,它允许用户在树莓派上运行CCU2固件。项目提供了一整套的HomeMatic体验,包括备份功能、Dutty-Cycle、LAN GW等。RaspberryMatic的一个显著优点是支持多种架构,包括x86_64/amd64、ARM和ARM64。 #### 知识点4:Docker容器部署脚本 "docker-ccu"项目提供了一套脚本,这些脚本能够自动化创建一个Docker容器来运行CCU2固件。通常这类脚本命名为`deploy.sh`,开发者或者最终用户可以通过运行这些脚本来快速部署和启动Docker容器,而无需手动配置和启动容器的每一个步骤。 #### 知识点5:数据备份与迁移 在使用Docker容器进行部署时,用户可能需要在不同环境下迁移数据或者保留原有数据。脚本中提到了数据保留的问题,如果用户之前使用的是其他方式部署,比如非Docker方式或者使用了特定的docker卷或者容器名称,那么在调用`deploy.sh`脚本部署时,需要对设置进行相应的调整,以保证数据的完整性。 #### 知识点6:仓库维护与开源社区 项目维护者提到了不再计划继续更新该存储库,并提出了将仓库设置为只读模式的想法。这在开源社区中是比较常见的情况,尤其是在维护者有新的兴趣点或者由于个人时间限制时。在此情况下,开源项目可以通过社区协作来继续维护,或者寻求其他维护者的接手。 #### 知识点7:Shell脚本编写 由于项目中提到了一个叫做`deploy.sh`的脚本文件,这说明脚本是用Shell语言编写的。Shell脚本非常适合于执行自动化任务,比如配置环境、启动服务、管理文件系统等,因此在自动化部署或系统管理中经常被使用。了解Shell脚本编写,对于自动化管理Docker容器等任务至关重要。 #### 知识点8:社区支持和反馈 项目维护者在描述中提到,如果在一个月内没有收到任何关于将官方CCU作为容器使用的反馈,将会把仓库设置为只读模式。这表明了开源社区中项目的发展很大程度上依赖于社区成员的反馈和支持。因此,了解如何与开源项目互动,提交问题、建议和补丁,是参与开源社区的重要途径。 #### 知识点9:固件概念与兼容性 CCU2固件特别设计用于某些特定硬件,但通过Docker化的方式,开发者可以跨平台运行CCU2固件,这增加了固件的兼容性。Docker的隔离性允许用户在一个通用的软件层面上运行原本可能受限于特定硬件的固件,从而扩展了固件的应用场景。 #### 知识点10:操作系统架构支持 项目支持包括x86_64/amd64、ARM和ARM64在内的多种架构,说明了Docker容器在不同硬件平台上的高度可移植性。这一特点使得开发者可以在各种硬件上部署相同的环境,简化了跨平台应用的开发和部署。 #### 结语 该文档提供了一个关于如何将特定固件整合入Docker容器的方案,并说明了项目维护者对于未来发展的规划。这些内容不仅对有志于尝试或扩展该项目的个人有指导意义,同时也展示了开源社区协作以及Docker技术在部署和管理复杂系统环境中的重要性和便利性。
recommend-type

手把手封装SDK:C#如何高效集成汉印D35BT打印功能

# 摘要 本文围绕C# SDK封装与汉印D35BT打印机集成的技术实践展开,系统阐述了SDK封装的理论基础、架构设计及面向对象设计原则的应用。文章分析了汉印D35BT打印机的通信协议与API调用方式,并详细介绍了在C#中实现蓝牙设备交互与数据发送的方法。通过核心打印功能的类封装、异步任务处理机制的设计,提升了SDK的易用性与扩展性。结合WinForm项目示例验证功能完整性后,进一步探讨了SDK的性能优化策略、测试方法及发布流程,构建了从设计、实现到部署的完整技术路径。 # 关键字 SDK封装;蓝牙通信;面向对象设计;异步打印;API调用;NuGet包发布 参考资源链接:[C#开
recommend-type

VM虚拟机ubuntu桥接主机无线网络

### 配置 VMware Ubuntu 桥接模式连接无线网络 在 VMware 中配置 Ubuntu 虚拟机通过桥接模式连接主机的无线网络,需要确保虚拟机与主机处于同一网络段,并能够通过主机的无线网卡直接访问外部网络。以下是详细的配置步骤: #### VMware 设置桥接模式 1. **以管理员权限运行 VMware**,进入 **编辑 > 虚拟网络编辑器**。 2. 在 **虚拟网络编辑器** 界面中,找到 **VMnet0(桥接模式)** 的设置部分。 3. 在 **“桥接到”** 的下拉菜单中,选择主机的无线网卡设备。 4. 勾选 **“自动设置桥接”** 选项,确保 VMwar
recommend-type

Ruby on Rails跳蚤市场应用开发详解

根据提供的文件信息,我们可以从中提炼出以下知识点: ### 标题知识点 - **freemarket_sample_72h** - 标题暗示这是一份关于名为“freemarket”的跳蚤市场应用程序的72小时开发样例或原型。 - 样例名称“freemarket_sample_72h”可能用于内部标识或者版本控制,表明该样本是在有限的时间内(即72小时内)完成的。 ### 描述知识点 - **网站结构** - 首页:应用程序的入口点,通常包含总体介绍和导航链接。 - 产品页面:展示产品的列表或者详细信息。 - 展览页:可能指专门展示某些特殊产品或促销产品的页面。 - 应用信息:关于应用程序的基本信息,如版本号、开发团队、联系方式等。 - 应用概述:对应用程序功能和目标用户群体的简介。 - **用户账户信息** - 测试账号:为开发者或测试者提供的虚拟用户账号信息,以便进行应用程序的内部测试。 - 购买者信息:提供了邮箱地址、密码以及购买卡信息,是进行交易和购买所必需的。 - 卖家信息:提供了卖家的邮箱地址和密码,用于登录卖家账户进行产品上架和管理。 - **功能列表** - 新用户注册:允许新用户创建账户。 - 登录功能:用户可以使用凭证登录应用程序。 - 产品列表功能:展示所有可购买的产品。 - 产品购买功能:用户可以购买产品,涉及到支付信息的处理。 - 产品类别注册和显示:允许用户查看不同的产品分类。 - 产品详细信息显示:展示每个产品的详细信息,如描述、价格等。 - 编辑和删除列出的产品:赋予管理员或卖家权利更新或移除产品信息。 - **开发环境** - Ruby 2.5.1:这是Ruby编程语言的一个版本。 - Ruby on Rails 5.4.2:这是一个使用Ruby语言编写的开源Web应用框架。 - MySQL 14.14:这是一个流行的开源关系型数据库管理系统。 - Github:一个用于代码托管和版本控制的平台。 - AWS:亚马逊提供的云服务平台,包括EC2(弹性计算云)和S3(简单存储服务)。 - Capistrano:是一个开源的自动化部署工具,常用于Ruby on Rails项目。 - **开发周期和工作时间** - 开发时间:约4周,说明了项目从开始到完成所需的时间。 - 每天平均工作时间:大约9小时,表明项目的紧凑和开发团队的努力。 - 开发系统人数:4,指出了参与该项目的开发人员数量。 - 敏捷类型:可能指的是一种开发过程,强调快速迭代和响应变化。 ### 标签知识点 - **Ruby** - 这个标签直接指向了Ruby编程语言,说明该应用程序是使用Ruby开发的。 ### 压缩包子文件的文件名称列表知识点 - **freemarket_sample_72h-master** - 这是源代码压缩包的文件名称,指示了一个版本控制中的主分支(master)。 通过这些信息,我们可以了解到该应用程序是一个在线跳蚤市场,它允许用户注册、登录、查看和购买产品,并且提供了产品管理功能。项目采用Ruby语言和Ruby on Rails框架开发,并使用了多种技术和平台,如MySQL数据库、Github、AWS云服务以及Capistrano部署工具。项目开发遵循敏捷开发原则,并在一个紧凑的时间表中完成。
recommend-type

【C#条码打印实战技巧】:汉印D35BT数据格式转换全攻略

# 摘要 本文围绕C#语言实现条码打印的技术方案展开,重点以汉印D35BT打印机为实践对象,系统性地讲解了条码打印中数据格式的基本原理与处理方法。文章分析了条码打印的数据流向、通信协议与命令集结构,探讨了在C#开发环境下实现数据格式转换、命令封装与容错机制的关键技术。通过完整的打印流程实现、性能优化策略以及常见问题排查方法的介绍,帮助开发者构建高效稳定的条码打印应用。同时,文章还展望了条码打印技术在多协议支持、云服务集成与企业级系统对接方面的拓展方向。 # 关键字 条码打印;数据格式;C#开发;通信协议;命令封装;容错机制 参考资源链接:[C#开发汉印D35BT条码打印机源代码
recommend-type

pikaqiu靶场搭建linux

在Linux系统上搭建名为“Pikachu”的靶场环境,主要涉及以下几个步骤: ### 下载并安装Pikachu靶场 首先,需要确保系统中已安装Git工具。如果尚未安装,可以通过以下命令安装Git: ```bash sudo yum install git ``` 接下来,使用Git从GitHub上克隆Pikachu靶场的源代码到本地: ```bash git clone https://github.com/zhuifengshaonianhanlu/pikachu.git ``` 完成下载后,将Pikachu靶场文件移动到Apache服务器的根目录下,以便于后续的Web访问: