<think>我们重新理解需求:需要显示与当前医案的dntag不相同,但是与mrcase相同的病态标签。
具体步骤:
1. 在mrrelate组件中,我们接收两个prop:当前医案的dntag数组(currentCaseTags)和当前医案的mrcase字符串(currentCaseContent)。
2. 同时,我们也接收所有病态标签数据(allTags)。
3. 我们需要做两重过滤:
a. 首先,排除当前医案已经包含的病态标签(即与dntag不相同)。
b. 其次,检查这些标签的名称是否出现在当前医案的mrcase文本中(即与mrcase相同,这里“相同”是指标签名称在mrcase文本中出现)。
注意:这里“与mrcase相同”是指标签名称在mrcase文本中出现,而不是完全相等。
因此,在mrrelate.vue中,我们将:
- 从props中获取currentCaseTags(当前医案的病态标签数组)、currentCaseContent(当前医案的mrcase文本)和allTags(所有标签)。
- 计算属性filteredTags:
1. 先过滤掉当前医案已经有的标签(通过标签ID)。
2. 再过滤出那些标签名称出现在currentCaseContent中的标签。
由于mrcase可能很长,而标签名称是短字符串,我们使用字符串的includes方法(不区分大小写)来判断。
修改mrviewer.vue:
- 在传递给mrrelate组件时,除了传递currentCaseTags(当前医案的病态标签数组)外,还要传递当前医案的mrcase文本(即currentCase.mrcase)。
注意:当前医案的数据在paginatedData计算属性中已经获取,我们将当前医案的mrcase也存储起来并传递给mrrelate。
具体代码调整如下:
在mrviewer.vue中:
- 在data里增加一个currentCaseContent用于存储当前医案的mrcase。
- 在paginatedData计算属性中,当有数据时,除了设置currentCaseTags,还要设置currentCaseContent为当前医案的mrcase。
- 在传递给mrrelate组件时,添加:currentCaseContent="currentCaseContent"
在mrrelate.vue中:
- 添加props: currentCaseContent,类型为String,默认值为空字符串。
- 在filteredTags计算属性中,先排除当前医案已有的标签,然后检查剩余标签的dnname是否出现在currentCaseContent中(不区分大小写)。
注意:因为currentCaseContent可能很大,而标签名称可能包含特殊字符,所以我们需要转义正则特殊字符,但这里我们使用简单的字符串包含(并转换为小写)来检查。
实现步骤:
1. 将currentCaseContent转换为小写(一次转换,避免重复转换)。
2. 对于每个候选标签,将其dnname转换为小写,然后检查是否包含在currentCaseContent的小写字符串中。
代码示例(在mrrelate.vue的filteredTags计算属性中):
```javascript
filteredTags() {
// 如果没有当前医案的标签,则先取所有标签,然后再过滤出现在mrcase中的
// 获取当前医案的标签ID
const currentTagIds = this.currentCaseTags.map(tag => tag.id);
// 第一步:过滤掉当前医案已有的标签
let candidateTags = this.allTags.filter(tag => !currentTagIds.includes(tag.id));
// 如果没有mrcase内容,则返回空数组(因为没有匹配依据)
if (!this.currentCaseContent) {
return candidateTags; // 或者根据需求,可以返回空数组,但这里我们认为如果没有mrcase,则无法判断,所以不显示任何标签?但需求是“与mrcase相同”,如果mrcase为空,那么任何标签都不满足条件,所以应该返回空数组。
}
// 将mrcase内容转换为小写(一次转换,避免重复转换)
const contentLower = this.currentCaseContent.toLowerCase();
// 第二步:过滤出标签名称出现在mrcase中的标签
return candidateTags.filter(tag => {
// 如果标签没有名称,则跳过
if (!tag.dnname) return false;
// 将标签名称转换为小写
const tagNameLower = tag.dnname.toLowerCase();
// 检查标签名称是否出现在mrcase中
return contentLower.includes(tagNameLower);
});
}
```
注意:这里我们假设标签名称是连续的字符串,并且mrcase中出现的也是连续的相同字符串。
下面给出完整代码:</think>我理解了您的需求,需要实现以下功能:将比对好的病态标签与当前医案的`dntag`和`mrcase`字段进行比对,只显示**与dntag不相同且与mrcase相同**的病态标签。以下是完整的实现代码:
### mrviewer.vue (完整代码)
```vue
<template>
<div class="container">
<!-- 控制面板 -->
<div class="control-panel">
<button @click="fetchData" class="refresh-btn">刷新數據</button>
<button @click="toggleRelative" class="relative-btn">
{{ showRelative ? '关闭相关' : '相关病态' }}
</button>
<input v-model="searchQuery"
placeholder="搜索..."
class="search-input" />
<div class="pagination-controls">
<span>每頁顯示:</span>
<select v-model.number="pageSize" class="page-size-select">
<option value="1">1筆</option>
<option value="4">4筆</option>
<option value="10">10筆</option>
</select>
<button @click="prevPage" :disabled="currentPage === 1">上一页</button>
<span>第</span>
<input type="number"
v-model.number="inputPage"
min="1"
:max="totalPages"
class="page-input"
@input="handlePageInput">
<span>頁 / 共 {{ totalPages }} 頁</span>
<button @click="nextPage" :disabled="currentPage === totalPages">下一頁</button>
<span>醫案閱讀器</span>
</div>
</div>
<!-- 主内容区域 -->
<div class="content-area">
<div class="horizontal-records" v-if="filteredData.length > 0">
<div v-for="(item, index) in paginatedData" :key="item.id" class="record-card">
<div class="record-header">
<h3>醫案 #{{ (currentPage - 1) * pageSize + index + 1 }}</h3>
</div>
<div class="record-body">
<div v-for="(value, key) in processFieldNames(item)" :key="key" class="record-field">
<div class="field-name">{{ key }}:</div>
<div class="field-value">
<div v-if="key === '病態名稱' && Array.isArray(value)" class="dntag-value">
{{ formatDntagValue(value) }}
</div>
<div v-else-if="Array.isArray(value)" class="array-value">
<span v-for="(subItem, subIndex) in value" :key="subIndex">
<span v-html="formatValue(subItem, key)"></span><span v-if="subIndex < value.length - 1">;</span>
</span>
</div>
<div v-else v-html="formatValue(value, key)"></div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="no-data">
沒有找到匹配的數據
</div>
</div>
<!-- 相关病态区域 -->
<div class="relative-area">
<mrrelate v-if="showRelative"
:currentCase="currentCase"
:allTags="api2Data"></mrrelate>
<span v-else>相關病態</span>
</div>
</div>
</template>
<script>
import mrrelate from './mrrelate.vue';
export default {
name: 'mrviewer',
components: { mrrelate },
data() {
return {
api1Data: [],
api2Data: [],
mergedData: [],
currentPage: 1,
pageSize: 1,
searchQuery: '',
sortKey: '',
sortOrders: {},
inputPage: 1,
fieldNames: {
'mrcase': '醫案全文',
'mrname': '醫案命名',
'mrposter': '醫案提交者',
'mrlasttime': '最後編輯時間',
'mreditnumber': '編輯次數',
'mrreadnumber': '閱讀次數',
'mrpriority': '重要性',
'dntag': '病態名稱'
},
inputTimeout: null,
dnNames: [],
stateVersion: '1.0',
showRelative: false,
currentCase: null // 存储当前医案完整数据
};
},
computed: {
filteredData() {
const query = this.searchQuery.trim();
if (query && /^\d+$/.test(query)) {
const idToSearch = parseInt(query, 10);
return this.mergedData.filter(item => item.id === idToSearch);
}
if (!query) return this.mergedData;
const lowerQuery = query.toLowerCase();
return this.mergedData.filter(item => {
return Object.values(item).some(value => {
if (value === null || value === undefined) return false;
if (Array.isArray(value)) {
return value.some(subValue => {
if (typeof subValue === 'object' && subValue !== null) {
return JSON.stringify(subValue).toLowerCase().includes(lowerQuery);
}
return String(subValue).toLowerCase().includes(lowerQuery);
});
}
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value).toLowerCase().includes(lowerQuery);
}
return String(value).toLowerCase().includes(lowerQuery);
});
});
},
sortedData() {
if (!this.sortKey) return this.filteredData;
const order = this.sortOrders[this.sortKey] || 1;
return [...this.filteredData].sort((a, b) => {
const getValue = (obj) => {
const val = obj[this.sortKey];
if (Array.isArray(val)) return JSON.stringify(val);
return val;
};
const aValue = getValue(a);
const bValue = getValue(b);
if (aValue === bValue) return 0;
return aValue > bValue ? order : -order;
});
},
paginatedData() {
const start = (this.currentPage - 1) * Number(this.pageSize);
const end = start + Number(this.pageSize);
const data = this.sortedData.slice(start, end);
// 更新当前医案完整数据
if (data.length > 0) {
this.currentCase = data[0];
} else {
this.currentCase = null;
}
return data;
},
totalPages() {
return Math.ceil(this.filteredData.length / this.pageSize) || 1;
}
},
watch: {
pageSize() {
this.currentPage = 1;
this.inputPage = 1;
this.saveState();
},
currentPage(newVal) {
this.inputPage = newVal;
this.saveState();
},
filteredData() {
if (this.currentPage > this.totalPages) {
this.currentPage = Math.max(1, this.totalPages);
}
this.inputPage = this.currentPage;
},
searchQuery() {
this.saveState();
}
},
methods: {
saveState() {
const state = {
version: this.stateVersion,
currentPage: this.currentPage,
pageSize: this.pageSize,
searchQuery: this.searchQuery,
timestamp: new Date().getTime()
};
sessionStorage.setItem('mrviewerState', JSON.stringify(state));
},
restoreState() {
const savedState = sessionStorage.getItem('mrviewerState');
if (!savedState) return;
try {
const state = JSON.parse(savedState);
if (state.version !== this.stateVersion) return;
this.currentPage = state.currentPage || 1;
this.pageSize = state.pageSize || 1;
this.searchQuery = state.searchQuery || '';
this.inputPage = this.currentPage;
} catch (e) {
sessionStorage.removeItem('mrviewerState');
}
},
clearState() {
sessionStorage.removeItem('mrviewerState');
},
async fetchData() {
try {
const api1Response = await fetch("MRInfo/?format=json");
this.api1Data = await api1Response.json();
const api2Response = await fetch("DNTag/?format=json");
this.api2Data = await api2Response.json();
this.dnNames = this.api2Data.map(item => item.dnname).filter(name => name && name.trim());
this.dnNames.sort((a, b) => b.length - a.length);
this.mergeData();
this.currentPage = 1;
this.inputPage = 1;
this.saveState();
} catch (error) {
console.error("獲取數據失敗:", error);
alert("數據加載失敗,請稍後重試");
}
},
mergeData() {
this.mergedData = this.api1Data.map((item) => {
const newItem = { ...item };
if (newItem.dntag && Array.isArray(newItem.dntag)) {
newItem.dntag = newItem.dntag.map((tagId) => {
const matchedItem = this.api2Data.find(api2Item => api2Item.id === tagId);
return matchedItem || { id: tagId, dnname: "未找到匹配的數據" };
});
}
return newItem;
});
this.sortOrders = {};
if (this.mergedData.length > 0) {
Object.keys(this.mergedData[0]).forEach(key => {
this.sortOrders[key] = 1;
});
}
},
processFieldNames(item) {
const result = {};
for (const key in item) {
const newKey = this.fieldNames[key] || key;
result[newKey] = item[key];
}
return result;
},
highlightMatches(text) {
if (!text || typeof text !== 'string' || this.dnNames.length === 0) {
return text;
}
const pattern = new RegExp(
this.dnNames
.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
.join('|'),
'gi'
);
return text.replace(pattern, match =>
`<span style="color: rgb(212, 107, 8); font-weight: bold;">${match}</span>`
);
},
formatValue(value, fieldName) {
if (value === null || value === undefined) return '';
if (fieldName === '醫案全文' && typeof value === 'string') {
return this.highlightMatches(value);
}
if (typeof value === 'string' && value.startsWith('http')) {
return `<a href="${value}" target="_blank">${value}</a>`;
}
return value;
},
formatDntagValue(dntagArray) {
return dntagArray.map(tagObj => {
return tagObj.dnname || tagObj.name || '未命名標籤';
}).join(';');
},
sortBy(key) {
const originalKey = Object.keys(this.fieldNames).find(
origKey => this.fieldNames[origKey] === key
) || key;
this.sortKey = originalKey;
this.sortOrders[originalKey] = this.sortOrders[originalKey] * -1;
this.saveState();
},
prevPage() {
if (this.currentPage > 1) {
this.currentPage--;
this.saveState();
}
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.saveState();
}
},
handlePageInput() {
clearTimeout(this.inputTimeout);
this.inputTimeout = setTimeout(() => {
this.goToPage();
this.saveState();
}, 300);
},
goToPage() {
if (this.inputPage === null || this.inputPage === undefined || this.inputPage === '') {
this.inputPage = this.currentPage;
return;
}
const page = parseInt(this.inputPage);
if (isNaN(page)) {
this.inputPage = this.currentPage;
return;
}
if (page < 1) {
this.currentPage = 1;
} else if (page > this.totalPages) {
this.currentPage = this.totalPages;
} else {
this.currentPage = page;
}
this.inputPage = this.currentPage;
},
toggleRelative() {
this.showRelative = !this.showRelative;
}
},
mounted() {
this.restoreState();
this.fetchData();
},
activated() {
this.restoreState();
},
deactivated() {
this.saveState();
}
};
</script>
<style scoped>
.container {
max-width: 1200px;
margin: 0px;
padding: 0px;
}
.control-panel {
margin-bottom: 0px;
display: flex;
flex-wrap: wrap;
gap: 10px;
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;
}
.content-area {
position: fixed;
top: 56px;
bottom: 45px;
left: 0;
width: 70%;
background: white;
padding: 1px;
z-index: 100;
overflow-y: auto;
}
.relative-area {
position: fixed;
top: 56px;
bottom: 45px;
right: 0;
width: 30%;
background: lightblue;
padding: 1px;
z-index: 100;
overflow-y: auto;
}
.refresh-btn, .relative-btn {
padding: 4px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.refresh-btn:hover, .relative-btn:hover {
background-color: #45a049;
}
.search-input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
flex-grow: 1;
max-width: 300px;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 5px;
}
.page-size-select {
padding: 4px;
border-radius: 4px;
width: 70px;
}
.page-input {
width: 50px;
padding: 4px;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
}
.horizontal-records {
display: flex;
flex-direction: column;
gap: 20px;
}
.record-card {
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.record-header {
padding: 12px 16px;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.record-header h3 {
margin: 0;
font-size: 1.1em;
}
.record-body {
padding: 16px;
}
.record-field {
display: flex;
margin-bottom: 12px;
line-height: 1.5;
}
.record-field:last-child {
margin-bottom: 0;
}
.field-name {
font-weight: bold;
min-width: 120px;
color: #555;
}
.field-value {
flex-grow: 1;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.dntag-value {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.array-value {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.no-data {
padding: 20px;
text-align: center;
color: #666;
font-style: italic;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
```
### mrrelate.vue (完整代码)
```vue
<template>
<div class="mrrelate-container">
<h3>相關病態列表</h3>
<div class="filter-info" v-if="filteredTags.length > 0">
顯示與當前醫案不同但相關的病態標籤({{ filteredTags.length }}個)
</div>
<div v-if="filteredTags.length > 0" class="dntag-list">
<div v-for="(tag, index) in filteredTags" :key="tag.id" class="dntag-item">
<div class="tag-content">
<span class="tag-index">{{ index + 1 }}.</span>
<span class="tag-name">{{ tag.dnname || '未命名標籤' }}</span>
</div>
<div class="tag-relation">
<span class="relation-icon">✓</span>
<span class="relation-text">出現在醫案中</span>
</div>
</div>
</div>
<div v-else class="no-tags">
沒有符合條件的相關病態標籤
</div>
</div>
</template>
<script>
export default {
name: 'mrrelate',
props: {
// 当前医案完整数据
currentCase: {
type: Object,
default: null
},
// 所有病态标签数据
allTags: {
type: Array,
required: true
}
},
computed: {
// 过滤与当前医案不同但出现在医案中的病态标签
filteredTags() {
// 如果没有当前医案数据,返回空数组
if (!this.currentCase) return [];
// 获取当前医案的病态标签ID
const currentTagIds = this.currentCase.dntag
? this.currentCase.dntag.map(tag => tag.id)
: [];
// 获取医案全文内容(用于比对)
const caseContent = this.currentCase.mrcase || '';
const contentLower = caseContent.toLowerCase();
// 双重过滤:
// 1. 排除当前医案已有的标签
// 2. 只保留标签名称出现在医案全文中的标签
return this.allTags.filter(tag => {
// 排除当前医案已有的标签
if (currentTagIds.includes(tag.id)) return false;
// 检查标签名称是否出现在医案中
if (!tag.dnname) return false;
const tagName = tag.dnname.toLowerCase();
// 如果标签名称出现在医案中,则保留
return contentLower.includes(tagName);
});
}
}
};
</script>
<style scoped>
.mrrelate-container {
padding: 15px;
background: lightblue;
height: 100%;
overflow-y: auto;
}
h3 {
margin-top: 0;
padding-bottom: 8px;
color: #2c3e50;
text-align: center;
border-bottom: 2px solid #3498db;
}
.filter-info {
text-align: center;
margin: 10px 0;
font-size: 0.9em;
color: #555;
background: rgba(255, 255, 255, 0.3);
padding: 5px;
border-radius: 4px;
}
.dntag-list {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
margin-top: 15px;
}
.dntag-item {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
transition: all 0.3s ease;
border-left: 4px solid #3498db;
}
.dntag-item:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.tag-content {
padding: 12px 15px;
display: flex;
align-items: center;
background: #f8f9fa;
}
.tag-index {
font-weight: bold;
margin-right: 10px;
min-width: 25px;
height: 25px;
display: flex;
align-items: center;
justify-content: center;
background: #3498db;
color: white;
border-radius: 50%;
}
.tag-name {
flex-grow: 1;
font-weight: 500;
color: #2c3e50;
}
.tag-relation {
padding: 8px 15px;
background: #e3f2fd;
display: flex;
align-items: center;
border-top: 1px dashed #bbdefb;
}
.relation-icon {
color: #4caf50;
font-weight: bold;
margin-right: 8px;
font-size: 1.2em;
}
.relation-text {
color: #388e3c;
font-size: 0.9em;
}
.no-tags {
padding: 25px;
text-align: center;
color: #666;
font-style: italic;
margin-top: 20px;
border: 1px dashed #3498db;
border-radius: 8px;
background: rgba(255, 255, 255, 0.5);
}
</style>
```
### 功能说明
1. **双重比对逻辑核心**:
```javascript
filteredTags() {
if (!this.currentCase) return [];
// 获取当前医案的病态标签ID
const currentTagIds = this.currentCase.dntag
? this.currentCase.dntag.map(tag => tag.id)
: [];
// 获取医案全文内容
const caseContent = this.currentCase.mrcase || '';
const contentLower = caseContent.toLowerCase();
// 双重过滤:
return this.allTags.filter(tag => {
// 1. 排除当前医案已有的标签
if (currentTagIds.includes(tag.id)) return false;
// 2. 检查标签名称是否出现在医案中
if (!tag.dnname) return false;
const tagName = tag.dnname.toLowerCase();
return contentLower.includes(tagName);
});
}
```
2. **UI/UX 增强**:
- 添加了关系说明区域,显示"出现在医案中"的提示
- 使用索引徽章增强视觉识别
- 添加了悬停动画效果
- 优化了标签卡片的分层结构
- 添加了过滤信息提示(显示符合条件的标签数量)
3. **比对逻辑细节**:
- **与dntag不相同**:通过比较标签ID,排除当前医案已有的病态标签
- **与mrcase相同**:检查标签名称是否出现在医案全文中(不区分大小写)
- 双重条件必须同时满足才会显示
4. **错误处理**:
- 处理当前医案为空的情况
- 处理dntag字段为空的情况
- 处理mrcase字段为空的情况
- 处理标签没有名称的情况
5. **性能优化**:
- 使用ID比对提高效率
- 将医案全文转为小写只执行一次
- 使用计算属性缓存结果
- 避免不必要的重新渲染