以下頁面代碼如下,當點擊父組件切換按鈕時,子組件的頁面仍能保持原來的頁面狀態,請以完整代碼展示:
父組件代碼(不更改):
<template>
<div class="app-container">
<!-- 顶部固定菜单 -->
<div class="fixed-menu">
<!-- “医仙”按钮(当前页跳转) -->
<button class="external-link"
@click="openExternalLink">
醫仙
</button>
<!-- 动态组件切换按钮 -->
<button @click="setCurrentView('mrviewer')"
:class="{ active: currentView === 'mrviewer' }">
醫案閱讀器
</button>
<button @click="setCurrentView('mreditor')"
:class="{ active: currentView === 'mreditor' }">
醫案編輯器
</button>
<button @click="setCurrentView('dncrud')"
:class="{ active: currentView === 'dncrud' }">
病態編輯器
</button>
</div>
<!-- 动态组件展示区域 -->
<div class="content-wrapper">
<!-- 使用 keep-alive 缓存组件状态 -->
<keep-alive :include="cachedComponents">
<component :is="currentViewComponent" :key="currentView" />
</keep-alive>
</div>
</div>
</template>
<script>
import mrviewer from "./components/mrviewer.vue";
import mreditor from "./components/mreditor.vue";
import dncrud from "./components/dncrud.vue"; // 导入病态编辑器组件
export default {
components: { mrviewer, mreditor, dncrud }, // 注册组件
data() {
return {
currentView: "mrviewer",
// 需要缓存的组件列表
cachedComponents: ["mrviewer", "mreditor", "dncrud"]
};
},
computed: {
currentViewComponent() {
return this.currentView;
}
},
methods: {
// 设置当前视图并保存状态
setCurrentView(viewName) {
// 保存当前组件状态(如果需要)
if (this.currentViewComponent && this.currentViewComponent.beforeLeave) {
this.currentViewComponent.beforeLeave();
}
// 切换到新视图
this.currentView = viewName;
// 保存当前视图到本地存储
localStorage.setItem("lastActiveView", viewName);
},
openExternalLink() {
// 保存当前状态(如果需要)
if (this.currentViewComponent && this.currentViewComponent.beforeLeave) {
this.currentViewComponent.beforeLeave();
}
// 跳转到外部链接
window.location.href = "http://localhost:65358/";
},
// 恢复上次活动视图
restoreLastView() {
const lastView = localStorage.getItem("lastActiveView");
if (lastView && this.cachedComponents.includes(lastView)) {
this.currentView = lastView;
}
}
},
mounted() {
// 组件挂载时恢复上次视图
this.restoreLastView();
}
};
</script>
<style scoped>
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.fixed-menu {
position: fixed;
top: 0;
left: 0;
width: 100%;
background: #2c3e50;
padding: 10px;
display: flex;
gap: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.fixed-menu button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: #3498db;
color: white;
cursor: pointer;
transition: background 0.3s;
}
.fixed-menu button:hover {
background: #2980b9;
}
.fixed-menu button.active {
background: #e74c3c;
font-weight: bold;
}
.fixed-menu button.external-link {
background: #2ecc71; /* 绿色背景区分 */
}
.fixed-menu button.external-link:hover {
background: #27ae60;
}
.content-wrapper {
flex: 1;
margin-top: 60px; /* 增加顶部间距避免内容被顶部菜单遮挡 */
padding: 20px;
background: #f5f5f5;
height: calc(100vh - 80px); /* 确保内容区域高度适配 */
overflow-y: auto; /* 添加滚动条 */
}
</style>
子組件代碼:
<template>
<div class="container">
<!-- 控制面板 -->
<div class="control-panel">
<button @click="fetchData" class="refresh-btn">刷新數據</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">
<!-- 使用v-for遍历处理后的字段对象 -->
<div v-for="(value, key) in processFieldNames(item)" :key="key" class="record-field">
<div class="field-name">{{ key }}:</div>
<div class="field-value">
<!-- 处理dntag字段的特殊显示 -->
<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>
</template>
<script>
export default {
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: [] // 存储所有病态名称
};
},
computed: {
filteredData() {
const query = this.searchQuery.trim();
// 判斷是否為數字(即搜尋 ID)
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);
return this.sortedData.slice(start, end);
},
totalPages() {
return Math.ceil(this.filteredData.length / this.pageSize) || 1;
}
},
watch: {
// 监控分页大小变化
pageSize() {
// 重置到第一页
this.currentPage = 1;
this.inputPage = 1; // 同步输入框的值
},
// 监控当前页码变化
currentPage(newVal) {
// 同步输入框的值
this.inputPage = newVal;
},
// 监控过滤数据变化
filteredData() {
// 确保当前页码有效
if (this.currentPage > this.totalPages) {
this.currentPage = Math.max(1, this.totalPages);
}
// 同步输入框的值
this.inputPage = this.currentPage;
}
},
methods: {
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();
// 提取所有dnname
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; // 重置输入框
} 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;
},
// 专门处理dntag字段的显示格式
formatDntagValue(dntagArray) {
return dntagArray.map(tagObj => {
// 只显示name属性,隐藏id等其他属性
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;
},
prevPage() {
if (this.currentPage > 1) {
this.currentPage--;
}
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
}
},
// 实时处理页码输入
handlePageInput() {
// 清除之前的定时器
clearTimeout(this.inputTimeout);
// 设置新的定时器(防抖处理)
this.inputTimeout = setTimeout(() => {
this.goToPage();
}, 300); // 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;
}
},
mounted() {
this.fetchData();
}
};
</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: 100%;
background: white;
padding: 1px;
z-index: 100;
overflow-y: auto;
}
.refresh-btn {
padding: 4px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.refresh-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>