为什么style标签中加scoped属性

CSS样式隔离与页面独立性
在前端开发中,为了确保页面样式的隔离并避免样式冲突,可以使用`style`标签的`scoped`属性,限制CSS仅作用于当前页面。此外,通过在每个页面外包裹一个父级class也能实现相同的效果,这种方法同样能防止样式影响其他页面。这两种方法都是提升代码维护性和可读性的有效实践。

因为页面很多,在style 标签中加scoped 属性,会将此页面的css 样式只针对此页面,更加不会影响其他页面。

但是其实也可以每个页面最外面有一个父class, 这样样式也不会影响。

修正mrviewer.vue及mrdnrelate.vue功能,當進入mrviewer.vue頁面時,自動刷新,自動啟動病態關聯,自動啟動mrdnrelate.vue功能中刷新數據及手動新增關聯的功能: mrviewer.vue: <!-- mrviewer.vue --> <template> <div class="container"> <!-- 控制面板 --> <div class="control-panel"> <button @click="fetchData" class="refresh-btn">刷新數據</button> <button @click="toggleRelative" class="relative-btn"> {{ showRelative ? '病態關聯' : '病態關聯' }} </button> <!-- 本草关联器按钮 --> <button @click="toggleMNRelative" class="relative-btn"> {{ showMNRelative ? '本草關聯' : '本草關聯' }} </button> <!-- 方剂关联器按钮 --> <button @click="togglePNRelative" class="relative-btn"> {{ showPNRelative ? '方劑關聯' : '方劑關聯' }} </button> <!-- 出处关联器按钮 --> <button @click="toggleSNRelative" class="relative-btn"> {{ showSNRelative ? '典籍關聯' : '典籍關聯' }} </button> <!-- 人物关联器按钮 --> <button @click="toggleFNRelative" class="relative-btn"> {{ showFNRelative ? '人物關聯' : '人物關聯' }} </button> <!-- 新增辨證关联器按钮 --> <button @click="toggleDianNRelative" class="relative-btn"> {{ showDianNRelative ? '辨證關聯' : '辨證關聯' }} </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" v-html="formatDntagValueHTML(value)"></div> <!-- 本草名称 --> <div v-else-if="key === '相關本草' && Array.isArray(value)" class="mntag-value" v-html="formatMntagValueHTML(value)"></div> <!-- 方剂名称 --> <div v-else-if="key === '相關方劑' && Array.isArray(value)" class="pntag-value" v-html="formatPntagValueHTML(value)"></div> <!-- 出处名称 --> <div v-else-if="key === '相關典籍' && Array.isArray(value)" class="sntag-value" v-html="formatSntagValueHTML(value)"></div> <!-- 人物名称 --> <div v-else-if="key === '相關人物' && Array.isArray(value)" class="fntag-value" v-html="formatFntagValueHTML(value)"></div> <!-- 辨證名称 --> <div v-else-if="key === '相關辨證' && Array.isArray(value)" class="diantag-value" v-html="formatDiantagValueHTML(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"> <mrdnrelate v-if="showRelative" :currentCase="currentCase" :allTags="api2Data" @data-updated="handleDataUpdated"></mrdnrelate> <!-- 本草关联器 --> <mrmnrelate v-else-if="showMNRelative" :currentCase="currentCase" :allTags="api3Data" @data-updated="handleDataUpdated"></mrmnrelate> <!-- 方剂关联器 --> <mrpnrelate v-else-if="showPNRelative" :currentCase="currentCase" :allTags="api5Data" @data-updated="handleDataUpdated"></mrpnrelate> <!-- 出处关联器 --> <mrsnrelate v-else-if="showSNRelative" :currentCase="currentCase" :allTags="api4Data" @data-updated="handleDataUpdated"></mrsnrelate> <!-- 人物关联器 --> <mrfnrelate v-else-if="showFNRelative" :currentCase="currentCase" :allTags="api6Data" @data-updated="handleDataUpdated"></mrfnrelate> <!-- 新增辨證关联器 --> <mrdianrelate v-else-if="showDianNRelative" :currentCase="currentCase" :allTags="api7Data" @data-updated="handleDataUpdated"></mrdianrelate> <span v-else>點按下方關聯器,顯示醫案相關專有名詞</span> </div> </div> </template> <script> import mrdnrelate from './mrdnrelate.vue'; import mrmnrelate from './mrmnrelate.vue'; import mrpnrelate from './mrpnrelate.vue'; import mrsnrelate from './mrsnrelate.vue'; import mrfnrelate from './mrfnrelate.vue'; import mrdianrelate from './mrdianrelate.vue'; // 导入辨證关联器组件 export default { name: 'mrviewer', components: { mrdnrelate, mrmnrelate, mrpnrelate, mrsnrelate, mrfnrelate, mrdianrelate }, data() { return { api1Data: [], api2Data: [], api3Data: [], api4Data: [], api5Data: [], api6Data: [], api7Data: [], // 辨證标签数据 mergedData: [], currentPage: 1, pageSize: 1, searchQuery: '', sortKey: '', sortOrders: {}, inputPage: 1, fieldNames: { 'mrcase': '醫案全文', 'mrorigin': '醫案出處', 'mrdoctor': '醫案醫者', 'mrname': '醫案命名', 'mrposter': '醫案提交者', 'mrlasttime': '最後編輯時間', 'mreditnumber': '編輯次數', 'mrreadnumber': '閱讀次數', 'mrpriority': '重要性', 'dntag': '相關病態', 'mntag': '相關本草', 'pntag': '相關方劑', 'sntag': '相關典籍', 'fntag': '相關人物', 'diantag': '相關辨證' // 新增辨證字段 }, inputTimeout: null, dnNames: [], mnNames: [], pnNames: [], snNames: [], fnNames: [], dianNames: [], // 辨證名称列表 stateVersion: '1.0', showRelative: false, showMNRelative: false, showPNRelative: false, showSNRelative: false, showFNRelative: false, showDianNRelative: 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: { handleDataUpdated() { alert('數據已更新,正在刷新醫案數據...'); this.fetchData(); }, toggleMNRelative() { if (!this.showMNRelative) { this.showRelative = false; this.showPNRelative = false; this.showSNRelative = false; this.showFNRelative = false; this.showDianNRelative = false; } this.showMNRelative = !this.showMNRelative; }, togglePNRelative() { if (!this.showPNRelative) { this.showRelative = false; this.showMNRelative = false; this.showSNRelative = false; this.showFNRelative = false; this.showDianNRelative = false; } this.showPNRelative = !this.showPNRelative; }, toggleSNRelative() { if (!this.showSNRelative) { this.showRelative = false; this.showMNRelative = false; this.showPNRelative = false; this.showFNRelative = false; this.showDianNRelative = false; } this.showSNRelative = !this.showSNRelative; }, toggleRelative() { if (this.showRelative) { this.showRelative = false; } else { this.showMNRelative = false; this.showPNRelative = false; this.showSNRelative = false; this.showFNRelative = false; this.showDianNRelative = false; this.showRelative = true; } }, toggleFNRelative() { if (!this.showFNRelative) { this.showRelative = false; this.showMNRelative = false; this.showPNRelative = false; this.showSNRelative = false; this.showDianNRelative = false; } this.showFNRelative = !this.showFNRelative; }, // 辨證关联器切换方法 toggleDianNRelative() { if (!this.showDianNRelative) { this.showRelative = false; this.showMNRelative = false; this.showPNRelative = false; this.showSNRelative = false; this.showFNRelative = false; } this.showDianNRelative = !this.showDianNRelative; }, // 辨證名称HTML格式化方法 (使用红色) formatDiantagValueHTML(diantagArray) { return diantagArray.map(tagObj => { const name = tagObj.dianname || tagObj.name || '未命名標籤'; return `<span style="color: rgb(255, 0, 0); font-weight: bold;">${this.escapeHtml(name)}</span>`; }).join(';'); }, // 病态名称HTML格式化方法 (使用橙色) formatDntagValueHTML(dntagArray) { return dntagArray.map(tagObj => { const name = tagObj.dnname || tagObj.name || '未命名標籤'; return `<span style="color: rgb(212, 107, 8); font-weight: bold;">${this.escapeHtml(name)}</span>`; }).join(';'); }, // 本草名称HTML格式化方法 (使用绿色) formatMntagValueHTML(mntagArray) { return mntagArray.map(tagObj => { const name = tagObj.mnname || tagObj.name || '未命名標籤'; return `<span style="color: rgb(0, 128, 0); font-weight: bold;">${this.escapeHtml(name)}</span>`; }).join(';'); }, // 方剂名称HTML格式化方法 (使用紫色) formatPntagValueHTML(pntagArray) { return pntagArray.map(tagObj => { const name = tagObj.pnname || tagObj.name || '未命名標籤'; return `<span style="color: rgb(128, 0, 128); font-weight: bold;">${this.escapeHtml(name)}</span>`; }).join(';'); }, // 出处名称HTML格式化方法 (使用蓝色) formatSntagValueHTML(sntagArray) { return sntagArray.map(tagObj => { const name = tagObj.snname || tagObj.name || '未命名標籤'; return `<span style="color: rgb(51, 102, 255); font-weight: bold;">${this.escapeHtml(name)}</span>`; }).join(';'); }, // 人物名称HTML格式化方法 (使用棕色) formatFntagValueHTML(fntagArray) { return fntagArray.map(tagObj => { const name = tagObj.fnname || tagObj.name || '未命名標籤'; return `<span style="color: #8B4513; font-weight: bold;">${this.escapeHtml(name)}</span>`; }).join(';'); }, escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); }, 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'); }, // 修改后的fetchData方法:保留当前页码 async fetchData() { try { // 保存当前页码 const oldPage = this.currentPage; // 获取原有数据 const api1Response = await fetch("MRInfo/?format=json"); this.api1Data = await api1Response.json(); const api2Response = await fetch("DNTag/?format=json"); this.api2Data = await api2Response.json(); const api3Response = await fetch("MNTag/?format=json"); this.api3Data = await api3Response.json(); const api4Response = await fetch("SNTag/?format=json"); this.api4Data = await api4Response.json(); const api5Response = await fetch("PNTag/?format=json"); this.api5Data = await api5Response.json(); const api6Response = await fetch("FNTag/?format=json"); this.api6Data = await api6Response.json(); // 辨證数据获取 const api7Response = await fetch("DiaNTag/?format=json"); this.api7Data = await api7Response.json(); // 本草名称列表 this.mnNames = this.api3Data.map(item => item.mnname).filter(name => name && name.trim()); this.mnNames.sort((a, b) => b.length - a.length); // 方剂名称列表 this.pnNames = this.api5Data.map(item => item.pnname).filter(name => name && name.trim()); this.pnNames.sort((a, b) => b.length - a.length); // 病态名称列表 this.dnNames = this.api2Data.map(item => item.dnname).filter(name => name && name.trim()); this.dnNames.sort((a, b) => b.length - a.length); // 出处名称列表 this.snNames = this.api4Data.map(item => item.snname).filter(name => name && name.trim()); this.snNames.sort((a, b) => b.length - a.length); // 人物名称列表 this.fnNames = this.api6Data.map(item => item.fnname).filter(name => name && name.trim()); this.fnNames.sort((a, b) => b.length - a.length); // 辨證名称列表 this.dianNames = this.api7Data.map(item => item.dianname).filter(name => name && name.trim()); this.dianNames.sort((a, b) => b.length - a.length); this.mergeData(); // 计算新数据的总页数 const newTotalPages = Math.ceil(this.filteredData.length / this.pageSize) || 1; // 调整页码(不超过新总页数) if (oldPage > newTotalPages) { this.currentPage = newTotalPages; // 超过范围时设为最后一页 } else { this.currentPage = oldPage; // 保持原页码 } this.inputPage = this.currentPage; 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: "未找到匹配的數據" }; }); } // 处理本草标签 if (newItem.mntag && Array.isArray(newItem.mntag)) { newItem.mntag = newItem.mntag.map((tagId) => { const matchedItem = this.api3Data.find(api3Item => api3Item.id === tagId); return matchedItem || { id: tagId, mnname: "未找到匹配的數據" }; }); } // 处理方剂标签 if (newItem.pntag && Array.isArray(newItem.pntag)) { newItem.pntag = newItem.pntag.map((tagId) => { const matchedItem = this.api5Data.find(api5Item => api5Item.id === tagId); return matchedItem || { id: tagId, pnname: "未找到匹配的數據" }; }); } // 处理出处标签 if (newItem.sntag && Array.isArray(newItem.sntag)) { newItem.sntag = newItem.sntag.map((tagId) => { const matchedItem = this.api4Data.find(api4Item => api4Item.id === tagId); return matchedItem || { id: tagId, snname: "未找到匹配的數據" }; }); } // 处理人物标签 if (newItem.fntag && Array.isArray(newItem.fntag)) { newItem.fntag = newItem.fntag.map((tagId) => { const matchedItem = this.api6Data.find(api6Item => api6Item.id === tagId); return matchedItem || { id: tagId, fnname: "未找到匹配的數據" }; }); } // 处理辨證标签 if (newItem.diantag && Array.isArray(newItem.diantag)) { newItem.diantag = newItem.diantag.map((tagId) => { const matchedItem = this.api7Data.find(api7Item => api7Item.id === tagId); return matchedItem || { id: tagId, dianname: "未找到匹配的數據" }; }); } 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; }, formatValue(value, fieldName) { if (value === null || value === undefined) return ''; // 醫案全文的高亮 if (fieldName === '醫案全文' && typeof value === 'string') { let highlighted = this.highlightMatches(value, this.dnNames, 'rgb(212, 107, 8)'); highlighted = this.highlightMatches(highlighted, this.mnNames, 'rgb(0, 128, 0)'); highlighted = this.highlightMatches(highlighted, this.pnNames, 'rgb(128, 0, 128)'); highlighted = this.highlightMatches(highlighted, this.snNames, 'rgb(51, 102, 255)'); highlighted = this.highlightMatches(highlighted, this.fnNames, '#8B4513'); highlighted = this.highlightMatches(highlighted, this.dianNames, 'rgb(255, 0, 0)'); // 辨證高亮 return highlighted; } // 醫案出處字段 else if (fieldName === '醫案出處' && typeof value === 'string') { return this.highlightMatches(value, this.snNames, 'rgb(51, 102, 255)'); } // 醫案醫者字段 else if (fieldName === '醫案醫者' && typeof value === 'string') { return this.highlightMatches(value, this.fnNames, '#8B4513'); } if (typeof value === 'string' && value.startsWith('http')) { return `<a href="${value}" target="_blank">${value}</a>`; } return value; }, highlightMatches(text, words, color) { if (!text || typeof text !== 'string' || words.length === 0) { return text; } const pattern = new RegExp( words .map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) .join('|'), 'gi' ); return text.replace(pattern, match => `<span style="color: ${color}; font-weight: bold;">${this.escapeHtml(match)}</span>` ); }, 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; } }, 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; } .mntag-value { display: flex; flex-wrap: wrap; gap: 8px; color: #006400; font-weight: 500; } .pntag-value { display: flex; flex-wrap: wrap; gap: 8px; color: #800080; font-weight: 500; } .sntag-value { display: flex; flex-wrap: wrap; gap: 8px; color: #0094ff; font-weight: 500; } .fntag-value { display: flex; flex-wrap: wrap; gap: 8px; color: #8B4513; font-weight: 500; } /* 辨證名称样式 */ .diantag-value { display: flex; flex-wrap: wrap; gap: 8px; color: rgb(255, 0, 0); font-weight: 500; } .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> mrdnrelate.vue: <!-- mrdnrelate.vue --> <template> <div class="mrdnrelate-container"> <!-- 相关病态区域(上方) --> <div class="tags-section"> <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.id }}: {{ tag.dnname || '未命名標籤' }}</span> </div> </div> </div> <div v-else class="no-tags"> 沒有符合條件的相關病態標籤 </div> </div> <!-- 分隔线 --> <div class="divider"></div> <!-- MRDN数据区域(下方) --> <div class="mrdn-section"> <h3>醫案病態關聯管理</h3> <!-- 操作按钮 --> <div class="mrdn-controls"> <button @click="fetchMRDNData" class="refresh-btn">刷新數據</button> <button @click="manualCreateMRDN" class="create-btn">手動新增關聯</button> </div> <!-- 数据列表 --> <div v-if="mrdnData.length > 0" class="mrdn-list"> <table class="mrdn-table"> <thead> <tr> <th>ID</th> <th>醫案ID</th> <th>病態ID</th> <th>操作</th> </tr> </thead> <tbody> <tr v-for="item in mrdnData" :key="item.id"> <td>{{ item.id }}</td> <td> <span v-if="!item.editing">{{ item.mrinfo }}</span> <textarea v-else v-model="item.editData.mrinfo" class="edit-textarea"></textarea> </td> <td> <span v-if="!item.editing">{{ item.dntag }}</span> <input v-else v-model="item.editData.dntag" type="text" class="edit-input"> </td> <td class="actions"> <template v-if="!item.editing"> <button @click="startEdit(item)" class="edit-btn">編輯</button> <button @click="confirmDelete(item.id)" class="delete-btn">刪除</button> </template> <template v-else> <button @click="saveEdit(item)" class="save-btn">保存</button> <button @click="cancelEdit(item)" class="cancel-btn">取消</button> </template> </td> </tr> </tbody> </table> </div> <div v-else class="no-data"> 沒有醫案病態關聯數據 </div> </div> </div> </template> <script> export default { name: 'mrdnrelate', props: { currentCase: { type: Object, default: null }, allTags: { type: Array, required: true } }, data() { return { mrdnData: [], isCreating: false, hasAutoCreated: false }; }, computed: { filteredTags() { if (!this.currentCase) return []; 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 => { if (currentTagIds.includes(tag.id)) return false; if (!tag.dnname) return false; return contentLower.includes(tag.dnname.toLowerCase()); }); } }, watch: { currentCase: { immediate: true, handler(newVal) { if (newVal && newVal.id) { if (!this.hasAutoCreated) { this.autoCreateMRDN(); this.hasAutoCreated = true; } } } } }, methods: { async fetchMRDNData() { try { const response = await fetch("MRDN/?format=json"); const data = await response.json(); this.mrdnData = data.map(item => ({ id: item.id, mrinfo: item.mrinfo || '', dntag: item.dntag || '', editing: false, editData: { mrinfo: item.mrinfo || '', dntag: item.dntag || '' } })); } catch (error) { console.error("獲取MRDN數據失敗:", error); alert("MRDN數據載失敗"); } }, async autoCreateMRDN() { if (this.isCreating) return; if (!this.currentCase || !this.currentCase.id) return; if (this.filteredTags.length === 0) return; try { this.isCreating = true; const createRequests = this.filteredTags.map(tag => { return fetch("MRDN/", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRFToken": this.getCSRFToken() }, body: JSON.stringify({ mrinfo: `${this.currentCase.id}`, dntag: `${tag.id}` }) }); }); const responses = await Promise.all(createRequests); const allSuccess = responses.every(res => res.ok); if (allSuccess) { console.log(`成功新增 ${this.filteredTags.length} 筆MRDN數據!`); this.fetchMRDNData(); this.triggerDataUpdate(); } else { throw new Error("部分新增失敗"); } } catch (error) { console.error("批量新增MRDN失敗:", error); } finally { this.isCreating = false; } }, async manualCreateMRDN() { try { await this.autoCreateMRDN(); if (this.filteredTags.length > 0) { alert(`成功新增 ${this.filteredTags.length} 筆MRDN數據!`); } } catch (error) { alert("新增過程中發生錯誤"); } }, startEdit(item) { item.editing = true; item.editData = { mrinfo: item.mrinfo, dntag: item.dntag }; }, cancelEdit(item) { item.editing = false; }, async saveEdit(item) { try { const response = await fetch(`MRDN/${item.id}/`, { method: "PUT", headers: { "Content-Type": "application/json", "X-CSRFToken": this.getCSRFToken() }, body: JSON.stringify({ mrinfo: item.editData.mrinfo, dntag: item.editData.dntag }) }); if (response.ok) { item.mrinfo = item.editData.mrinfo; item.dntag = item.editData.dntag; item.editing = false; alert("更新成功!"); this.triggerDataUpdate(); } else { throw new Error("更新失敗"); } } catch (error) { console.error("更新MRDN失敗:", error); alert("更新失敗"); } }, confirmDelete(id) { if (confirm("確定要刪除此數據嗎?")) { this.deleteMRDN(id); } }, async deleteMRDN(id) { try { const response = await fetch(`MRDN/${id}/`, { method: "DELETE", headers: { "X-CSRFToken": this.getCSRFToken() } }); if (response.ok) { this.mrdnData = this.mrdnData.filter(item => item.id !== id); alert("刪除成功!"); this.triggerDataUpdate(); } else { throw new Error("刪除失敗"); } } catch (error) { console.error("刪除MRDN失敗:", error); alert("刪除失敗"); } }, getCSRFToken() { return document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''; }, triggerDataUpdate() { this.$emit('data-updated'); } }, mounted() { this.fetchMRDNData(); if (this.currentCase && this.currentCase.id && !this.hasAutoCreated) { this.autoCreateMRDN(); this.hasAutoCreated = true; } } }; </script> <style scoped> .mrdnrelate-container { padding: 15px; background: #FFE0B2; /* 浅蓝色 -> 浅橙色 */ height: 100%; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; } .tags-section { flex: 0 0 40%; overflow-y: auto; background: rgba(255, 255, 255, 0.3); border-radius: 8px; padding: 15px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .divider { height: 2px; background: #FF9800; /* 蓝色 -> 橙色 */ margin: 10px 0; } .mrdn-section { flex: 1; overflow-y: auto; background: rgba(255, 255, 255, 0.3); border-radius: 8px; padding: 15px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } h3 { margin-top: 0; padding-bottom: 8px; color: #2c3e50; text-align: center; border-bottom: 2px solid #FF9800; /* 蓝色 -> 橙色 */ } .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 #FF9800; /* 蓝色 -> 橙色 */ } .tag-content { padding: 12px 15px; display: flex; align-items: center; } .tag-index { font-weight: bold; margin-right: 10px; min-width: 25px; height: 25px; display: flex; align-items: center; justify-content: center; background: #FF9800; /* 蓝色 -> 橙色 */ color: white; border-radius: 50%; } .tag-name { flex-grow: 1; font-weight: 500; color: #2c3e50; } .no-tags, .no-data { padding: 25px; text-align: center; color: #666; font-style: italic; margin-top: 20px; border: 1px dashed #FF9800; /* 蓝色 -> 橙色 */ border-radius: 8px; background: rgba(255, 255, 255, 0.5); } /* MRDN数据管理样式 */ .mrdn-controls { display: flex; justify-content: space-between; margin-bottom: 15px; } .mrdn-controls button { padding: 6px 12px; border-radius: 4px; border: none; cursor: pointer; font-weight: bold; } .refresh-btn { background-color: #4CAF50; color: white; } .create-btn { background-color: #FF9800; /* 蓝色 -> 橙色 */ color: white; } .mrdn-table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .mrdn-table th, .mrdn-table td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eee; } .mrdn-table th { background-color: #FF9800; /* 蓝色 -> 橙色 */ color: white; font-weight: bold; } .mrdn-table tr:hover { background-color: #f5f5f5; } .actions { display: flex; gap: 5px; } .actions button { padding: 5px 10px; border: none; border-radius: 3px; cursor: pointer; font-size: 0.85em; } .edit-btn { background-color: #FFC107; color: #333; } .delete-btn { background-color: #F44336; color: white; } .save-btn { background-color: #4CAF50; color: white; } .cancel-btn { background-color: #9E9E9E; color: white; } .edit-input, .edit-textarea { width: 100%; padding: 5px; border: 1px solid #ddd; border-radius: 3px; } .edit-textarea { min-height: 60px; resize: vertical; } </style>
最新发布
08-04
<think>嗯,我现在在学习Vue3,遇到了一个关于scoped样式的问题。我想了解一下,在Vue3中,当我们在组件的style标签上使用scoped属性时,什么情况下才会给对应的样式新的data-v-id属性呢?首先,我需要回忆一下Vue的作用域CSS是怎么工作的。 我记得,当给style标签scoped后,Vue会通过PostCSS处理这些样式,使得它们只作用于当前组件。处理方式应该是给模板中的每个元素添一个唯一的data属性,比如data-v-f3f3eg9,然后在样式中将这个属性作为属性选择器来限定样式的作用范围。例如,.example会变成.example[data-v-f3f3eg9]。 那问题来了,什么时候才会生成这个data-v-id呢?是不是每个组件在使用了scopedstyle标签后,都会自动生成这个属性?或者有没有例外情况? 首先,scoped属性style标签上的,所以当组件中的任何一个style标签scoped时,该组件的所有元素都会被添这个data属性吗?还是说只有该style标签内的样式对应的元素才会被处理? 可能的情况是,当某个组件的style标签带有scoped时,Vue在处理该组件时,会为这个组件模板中的所有DOM元素添data-v-id属性,并将样式中的选择器上这个属性,从而确保样式只在这个组件内生效。 那如果同一个组件中有多个scopedstyle标签,会生成不同的data-v-id吗?应该不会,因为同一个组件的所有scoped样式应该共享同一个id,这样才能正确应用样式。每个Vue组件实例可能会有不同的data-v-id吗?或者每个组件文件生成一个唯一的id? 另外,可能有些情况下不会添这个属性。比如,在子组件的根元素上,父组件使用scoped样式时,可能会有穿透的需求,这时候使用/deep/或者::v-deep可以让父组件的样式影响到子组件,这时候子组件的元素可能不会被父组件的data-v-id标记,但可能会有自己的data-v-id? 或者,当在子组件内部,如果子组件自身也用了scoped样式,那么子组件的元素会被添子组件自己的data-v-id属性,而父组件的scoped样式中的穿透选择器会带上父组件的data-v-id,从而影响到子组件的元素? 可能需要注意,data-v-id是基于组件本身的,每个组件在编译时会生成唯一的哈希id。所以,当组件有scoped样式时,其模板内的所有元素都会带有该组件的data-v-id属性,而样式中的选择器也会被上这个属性。这样,无论子组件是否也有自己的scoped样式,父组件的scoped样式如果穿透到子组件,就会使用父组件的data-v-id作为属性选择器的一部分。 那问题回到原点:在什么情况下才会添这个data-v-id属性?可能的答案是,当组件的style标签scoped属性时,该组件模板中的所有元素都会被添该组件唯一的data-v-id属性,同时该组件的所有scoped样式中的选择器都会被添对应的属性选择器。而如果组件没有scopedstyle标签,就不会生成data-v-id。 那有没有例外情况?比如,动态生成的组件,或者函数式组件,是否也会应用这个逻辑?或者,当使用CSS Modules时,是否会影响data-v-id的生成?可能需要查阅Vue的官方文档来确认。 根据Vue3的文档,scoped样式是通过PostCSS转换实现的,每个单文件组件(SFC)的scoped样式会为该组件生成一个唯一的id,这个id会作为data-v-属性到组件模板的所有元素上,同时样式中的类名会被修改为包含这个属性选择器。这样,样式就被限定在组件内部。 因此,总结起来,当在Vue组件的style标签上使用scoped属性时,该组件的所有DOM元素会被添data-v-id属性,并且对应的样式选择器会被附这个属性。每个组件实例的data-v-id是相同的吗?应该每个组件的id是唯一的,基于文件路径或内容生成的哈希值,确保不同组件的id不同。 所以,回答用户的问题:在Vue3中,当组件的style标签带有scoped属性时,该组件模板中的所有元素都会被添data-v-id属性,同时对应的样式规则会被修改为包含该属性选择器,以确保样式仅作用于当前组件。这种情况发生在任何使用了scoped style的组件中,且每个组件的data-v-id是唯一的,生成于构建时。</think>在 Vue 3 中,当你在单文件组件(SFC)的 `<style>` 标签上使用 `scoped` 属性时,Vue 的编译工具会**自动为该组件生成一个唯一的 `data-v-id` 属性**(实际是 `data-v-<hash>` 格式的哈希值),并应用到以下场景: --- ### **触发 `data-v-id` 添的条件** 1. **组件启用了 `scoped` 样式** 只要 `<style>` 标签包含 `scoped` 属性Vue 就会处理该组件的模板和样式: ```vue <style scoped> .example { color: red; } </style> ``` 2. **组件模板内的所有 DOM 元素** 编译时,组件模板中的**每个 DOM 元素**都会被添唯一的 `data-v-<hash>` 属性(如 `data-v-f3f3eg9`)。 3. **样式规则的转换** 所有 CSS 选择器会被修改为包含 `[data-v-<hash>]` 属性选择器。例如: ```css .example → .example[data-v-f3f3eg9] ``` --- ### **例外和注意事项** - **子组件的根元素** 父组件的 `scoped` 样式会穿透到子组件的**根元素**(这是 Vue 的默认行为),因此子组件根元素会同时拥有**父组件的 `data-v-<hash>`** 和自身的 `data-v-<hash>`(如果子组件也用了 `scoped`)。 - **深度选择器(`::v-deep`)** 若父组件想穿透到子组件的深层元素,需用 `::v-deep`: ```css /* 父组件样式 */ ::v-deep(.child-element) { color: red; } /* 编译后 → [data-v-parent-hash] .child-element */ ``` 此时子组件的元素不会添父组件的 `data-v-id`,但父组件的样式会通过属性选择器作用于子组件。 - **动态内容(如 `v-html`)** 通过 `v-html` 插入的内容不会自动添 `data-v-id`,需手动处理作用域样式。 - **非组件内元素(如全局 HTML)** 未包裹在组件内的全局 HTML 元素不会添 `data-v-id`。 --- ### **技术原理** 1. **编译阶段** Vue 的编译器(如 `vue-loader`)会为每个 `scoped` 的组件生成唯一哈希值(基于文件路径或内容)。 2. **模板处理** 所有模板中的 DOM 元素会被添 `data-v-<hash>` 属性。 3. **样式处理** 通过 PostCSS 插件,将 CSS 选择器转换为 `选择器[data-v-<hash>]`,实现样式隔离。 --- ### **总结** - **何时生成 `data-v-id`**:当组件的 `<style>` 标签包含 `scoped` 属性时。 - **应用范围**:该组件模板内的所有 DOM 元素,以及转换后的 CSS 选择器。 - **唯一性**:每个组件的 `data-v-id` 是唯一的,基于文件/内容生成,确保样式隔离。 通过这一机制,Vue 实现了组件级样式作用域,避免全局污染。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值