以下mrassorciator.vue再增加一個功能,當點按(執行匹配流程)時,同時fetch (‘MRInfo/’)中mrreadnumber欄位的數字,比較所有各個醫案資料中此數字的大小,若是排在前1/5者,在該醫案mrpriority欄位選擇(高)並patch; 若是排在中間3/5者,在該醫案mrpriority欄位選擇(中)並patch;若是排在後1/5者,在該醫案mrpriority欄位選擇(低)並patch;執行完後顯示重要性高、中、低的醫案數目;其餘原來功能不要改變:
<div> <div class="controls"> <button @click="processAllMatches" :disabled="loading">執行匹配流程</button> <button v-if="hasData" @click="showData = !showData" class="toggle-button" :class="{ active: showData }"> {{ showData ? '隱藏匹配結果' : '顯示匹配結果' }} </button> </div> <div v-if="loading">處理中... 已完成 {{ completedTasks }}/{{ totalTasks }} 項 (批量創建中)</div> <div v-if="error" class="error">{{ error }}</div> <div v-if="success" class="success"> 所有匹配操作完成!{{ createdAssociations > 0 ? `共创建 ${createdAssociations} 个新关联` : '' }} </div> <div v-show="showData && hasData"> <div class="toggle-header"> <h3>字段匹配結果</h3> <span class="collapse-icon" @click="showData = false">▲</span> </div> <div class="summary"> <h4>匹配統計</h4> <table> <thead> <tr> <th>標籤類型</th> <th>總記錄數</th> <th>匹配成功</th> <th>匹配率</th> <th>創建關聯</th> </tr> </thead> <tbody> <tr v-for="(stats, tagType) in matchStatistics" :key="tagType"> <td>{{ tagType }}</td> <td>{{ stats.total }}</td> <td>{{ stats.matched }}</td> <td>{{ stats.rate }}%</td> <td>{{ stats.created }}</td> </tr> </tbody> </table> </div> <div v-for="(matches, tagType) in matchedResults" :key="tagType" class="match-section"> <div class="section-header" @click="toggleSection(tagType)"> <h4>{{ tagType }} 匹配詳情 ({{ matches.length }}條)</h4> <span>{{ isSectionOpen(tagType) ? '▼' : '▶' }}</span> </div> <div v-show="isSectionOpen(tagType)"> <table class="match-details"> <thead> <tr> <th>病例ID</th> <th>匹配標籤ID</th> <th>關聯狀態</th> </tr> </thead> <tbody> <tr v-for="(match, idx) in matches" :key="idx"> <td>{{ match.mrId }}</td> <td> <template v-if="match.tagIds.length"> <span v-for="(tagId, i) in match.tagIds" :key="tagId"> {{ tagId }}<span v-if="i < match.tagIds.length - 1">, </span> </span> </template> <span v-else>無匹配</span> </td> <td :class="{'match-success': match.associationCreated}"> {{ match.associationCreated ? '✓ 已創建' : match.matched ? '✓ 已存在' : '✗' }} </td> </tr> </tbody> </table> </div> </div> </div> </div> </template> <script setup> import { ref, computed } from 'vue'; // 状态管理 const loading = ref(false); const error = ref(null); const success = ref(false); const showData = ref(false); const completedTasks = ref(0); const totalTasks = ref(8); const expandedSections = ref([]); const createdAssociations = ref(0); // 数据存储 const matchedResults = ref({}); const matchStatistics = ref({}); // 计算是否有匹配数据 const hasData = computed(() => Object.keys(matchedResults.value).length > 0); // 分区展开状态方法 const isSectionOpen = (tagType) => expandedSections.value.includes(tagType); const toggleSection = (tagType) => { const index = expandedSections.value.indexOf(tagType); if (index > -1) { expandedSections.value.splice(index, 1); } else { expandedSections.value.push(tagType); } }; // 获取CSRF令牌函数 const getCSRFToken = () => { const cookieValue = document.cookie .split('; ') .find(row => row.startsWith('csrftoken=')) ?.split('=')[1]; return cookieValue || ''; }; // 安全API请求函数 const secureFetch = async (url, method, data = null) => { try { const config = { method, headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCSRFToken() }, ...(data && { body: JSON.stringify(data) }) }; const response = await fetch(url, config); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (err) { throw new Error(`请求失败: ${url} - ${err.message}`); } }; // 关联API端点映射 const getAssociationEndpoint = (tagType) => { const map = { 'DNTag': 'MRDN/', 'MNTag': 'MRMN/', 'SNTag': 'MRSN/', 'PNTag': 'MRPN/', 'FNTag': 'MRFN/', 'DiaNTag': 'MRDiaN/', 'TNTag': 'MRTN/', 'ENTag': 'MREN/', 'PRNTag': 'MRPRN/' // 新增PRNTag端点 }; return map[tagType] || ''; }; // 字段名映射(根据标签类型获取关联字段名) const getAssociationField = (tagType) => { const map = { 'DNTag': 'dntag', 'MNTag': 'mntag', 'SNTag': 'sntag', 'PNTag': 'pntag', 'FNTag': 'fntag', 'DiaNTag': 'diantag', 'TNTag': 'tntag', 'ENTag': 'entag', 'PRNTag': 'prntag' // 新增PRNTag字段 }; return map[tagType] || ''; }; // 批量创建关联(参考a.vue的批量创建逻辑) const batchCreateAssociations = async (tagType, assocEndpoint, assocField, matches) => { try { // 1. 获取现有关联记录 const existingAssociations = await secureFetch(assocEndpoint, 'GET'); // 2. 创建唯一标识的Set const existingSet = new Set(); existingAssociations.forEach(item => { existingSet.add(`${item.mrinfo}-${item[assocField]}`); }); // 3. 收集需要创建的关联 const createRequests = []; const matchUpdates = []; // 4. 遍历匹配结果 matches.forEach(match => { if (match.matched) { match.tagIds.forEach(tagId => { const associationKey = `${match.mrId}-${tagId}`; // 检查是否已存在 if (!existingSet.has(associationKey)) { createRequests.push( secureFetch(assocEndpoint, 'POST', { mrinfo: match.mrId, [assocField]: tagId }) ); matchUpdates.push({ matchId: match.mrId, tagId }); } }); } }); // 5. 批量创建新关联 if (createRequests.length > 0) { const responses = await Promise.all(createRequests); const allSuccess = responses.every(res => res && !res.error); if (!allSuccess) { throw new Error(`${tagType}批量创建失败`); } // 6. 更新匹配状态 matchedResults.value[tagType].forEach(match => { matchUpdates.forEach(update => { if (match.mrId === update.matchId && match.tagIds.includes(update.tagId)) { match.associationCreated = true; } }); }); return createRequests.length; } return 0; } catch (err) { throw new Error(`${tagType}关联创建错误: ${err.message}`); } }; // 匹配处理函数 const processMatch = async (config) => { const { tagType, nameField } = config; try { const assocEndpoint = getAssociationEndpoint(tagType); const assocField = getAssociationField(tagType); // 1. 获取数据 const [mrInfos, tags] = await Promise.all([ secureFetch('MRInfo/', 'GET'), secureFetch(`${tagType}/`, 'GET') ]); matchedResults.value[tagType] = []; // 2. 执行匹配 mrInfos.forEach(mr => { const caseContent = (mr.mrcase || '').toLowerCase(); const matchedTagIds = []; tags.forEach(tag => { const tagName = tag[`${nameField}name`] || ''; if (tagName && caseContent.includes(tagName.trim().toLowerCase())) { matchedTagIds.push(tag.id); } }); matchedResults.value[tagType].push({ mrId: mr.id, tagIds: matchedTagIds, matched: matchedTagIds.length > 0, associationCreated: false }); }); // 3. 批量创建关联 const createdCount = await batchCreateAssociations( tagType, assocEndpoint, assocField, matchedResults.value[tagType] ); // 4. 更新统计信息 matchStatistics.value[tagType] = { total: mrInfos.length, matched: matchedResults.value[tagType].filter(m => m.matched).length, rate: mrInfos.length ? Math.round((matchedResults.value[tagType].filter(m => m.matched).length / mrInfos.length) * 100) : 0, created: createdCount }; createdAssociations.value += createdCount; completedTasks.value++; } catch (err) { throw new Error(`${tagType}处理错误: ${err.message}`); } }; // 主处理函数 const processAllMatches = async () => { loading.value = true; error.value = null; success.value = false; showData.value = true; completedTasks.value = 0; createdAssociations.value = 0; expandedSections.value = []; matchedResults.value = {}; matchStatistics.value = {}; try { const tasks = [ { tagType: 'DNTag', nameField: 'dn' }, { tagType: 'MNTag', nameField: 'mn' }, { tagType: 'SNTag', nameField: 'sn' }, { tagType: 'PNTag', nameField: 'pn' }, { tagType: 'FNTag', nameField: 'fn' }, { tagType: 'DiaNTag', nameField: 'dian' }, { tagType: 'TNTag', nameField: 'tn' }, { tagType: 'ENTag', nameField: 'en' }, { tagType: 'PRNTag', nameField: 'prn' } // 新增PRNTag处理任务 ]; totalTasks.value = tasks.length; // 顺序执行所有任务 for (const task of tasks) { await processMatch(task); } success.value = true; expandedSections.value = tasks.map(t => t.tagType); } catch (err) { error.value = err.message; showData.value = false; } finally { loading.value = false; } }; </script> <style scoped> /* 样式保持不变 */ .controls { display: flex; gap: 15px; margin-bottom: 20px; align-items: center; } button { padding: 10px 20px; background: #42b983; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; transition: background 0.3s; } button:hover { background: #359f74; } button:disabled { background: #cccccc; cursor: not-allowed; } .toggle-button { background: #3498db; } .toggle-button:hover { background: #2980b9; } .toggle-button.active { background: #e74c3c; } .toggle-button.active:hover { background: #c0392b; } .error { color: #e74c3c; padding: 10px; background: #ffebee; border-radius: 4px; margin-top: 10px; } .success { color: #2ecc71; padding: 10px; background: #e8f5e9; border-radius: 4px; margin-top: 10px; } .toggle-header { display: flex; justify-content: space-between; align-items: center; margin-top: 30px; padding: 10px 0; border-bottom: 2px solid #42b983; cursor: pointer; } .toggle-header h3 { margin: 0; color: #2c3e50; } .collapse-icon { font-size: 18px; cursor: pointer; padding: 5px; transition: transform 0.3s; } .toggle-header:hover .collapse-icon { transform: scale(1.2); } .summary { margin: 20px 0; padding: 15px; background: #f9f9f9; border-radius: 6px; } .match-section { margin-bottom: 25px; border: 1px solid #e0e0e0; border-radius: 6px; overflow: hidden; } .section-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; background-color: #f5f7fa; cursor: pointer; transition: background 0.2s; } .section-header:hover { background-color: #ebf0f5; } .section-header h4 { margin: 0; color: #34495e; } .match-details { margin: 0; border-top: 1px solid #e0e0e0; width: 100%; } .match-details th, .match-details td { padding: 10px 12px; text-align: center; } .match-details th:nth-child(1), .match-details td:nth-child(1) { width: 30%; } .match-details th:nth-child(2), .match-details td:nth-child(2) { width: 40%; text-align: left; } .match-details th:nth-child(3), .match-details td:nth-child(3) { width: 30%; } th { background-color: #42b983; color: white; font-weight: 600; } th, td { border: 1px solid #e0e0e0; } .match-success { color: #27ae60; font-weight: bold; } tr:nth-child(even) { background-color: #f8f9fa; } </style>
最新发布