完善grade(统计成绩页),增加删除数据按钮,增加标准答案字典,支持导出标准答案,完善计分方式

This commit is contained in:
2026-01-15 02:23:43 +00:00
parent a389879406
commit d0357ac556
15 changed files with 483 additions and 152 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules node_modules
.next/
.next .next

View File

@@ -1 +1 @@
{"encryption.key":"AxmjrhnEIlWTqaOj84DDbm8NSadx1bBdyyMc77kaHFY=","encryption.expire_at":1766739099583} {"encryption.key":"41RLTu25s6bKnxtTa0cAIxEID3ece30//eTx5e3P3aE=","encryption.expire_at":1767849224402}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -2,6 +2,6 @@
"/_app": "pages/_app.js", "/_app": "pages/_app.js",
"/_error": "pages/_error.js", "/_error": "pages/_error.js",
"/_document": "pages/_document.js", "/_document": "pages/_document.js",
"/": "pages/index.js", "/grade": "pages/grade.js",
"/grade": "pages/grade.js" "/": "pages/index.js"
} }

File diff suppressed because one or more lines are too long

View File

@@ -162,9 +162,9 @@ export default function Office({
<div <div
className={`w-16 h-16 mb-1 transition-colors flex items-center justify-center ${getPortStyle(`1A-${i + 1}`)}`} className={`w-16 h-16 mb-1 transition-colors flex items-center justify-center ${getPortStyle(`1A-${i + 1}`)}`}
jstype="testport-copper" jstype="testport-copper"
id={`1A-${i + 1}`} id={`Rack1-1A-${i + 1}`}
onClick={() => onPortClick(`1A-${i + 1}`)} onClick={() => onPortClick(`Rack1-1A-${i + 1}`)}
onMouseEnter={() => onPortHover(`1A-${i + 1}`)} onMouseEnter={() => onPortHover(`Rack1-1A-${i + 1}`)}
> >
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div> <div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div> </div>
@@ -190,9 +190,9 @@ export default function Office({
className={`w-16 h-16 mb-1 transition-colors flex items-center justify-center ${getPortStyle(`1B-${i + 1}`)}`} className={`w-16 h-16 mb-1 transition-colors flex items-center justify-center ${getPortStyle(`1B-${i + 1}`)}`}
jstype="testport-fiber" jstype="testport-fiber"
lcclean="false" lcclean="false"
id={`1B-${i + 1}`} id={`Rack1-1B-${i + 1}`}
onClick={() => onPortClick(`1B-${i + 1}`)} onClick={() => onPortClick(`Rack1-1B-${i + 1}`)}
onMouseEnter={() => onPortHover(`1B-${i + 1}`)} onMouseEnter={() => onPortHover(`Rack1-1B-${i + 1}`)}
> >
<div className={`w-8 h-8 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center`}></div> <div className={`w-8 h-8 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center`}></div>
</div> </div>

View File

@@ -72,7 +72,7 @@ export default function OfficeTask() {
<Image src="/Office.png" alt="office" fill className="object-contain"/> <Image src="/Office.png" alt="office" fill className="object-contain"/>
</div> </div>
<p className="text-gray-300 leading-relaxed"> <p className="text-gray-300 leading-relaxed">
在这些重要链路正式启用前集团工程部要求进行 <span className="font-semibold text-red-400">全面的物理层认证测试</span> <span className="font-medium text-yellow-300">GB/T</span> 在这些重要链路正式启用前集团工程部要求进行 <span className="font-semibold text-red-400">全面的物理层认证测试</span> <span className="font-medium text-yellow-300">GB/T</span>
</p> </p>
</div> </div>

View File

@@ -26,6 +26,7 @@ const API_URLS = {
// 教学相关 // 教学相关
TEACHING: { TEACHING: {
DATA: `${BASE_URL}/dsxapi/api/teaching/data`, DATA: `${BASE_URL}/dsxapi/api/teaching/data`,
CLEAR: `${BASE_URL}/dsxapi/api/teaching/clear`,
}, },
// 管理员鉴权相关 // 管理员鉴权相关
ADMIN: { ADMIN: {

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { API_URLS } from '@/config/api'; import { API_URLS } from '@/config/api';
import { faultReferenceDict, resolveFaultReference } from '@/store/faultReferenceDict';
export default function Grade({ isComponent = false }) { export default function Grade({ isComponent = false }) {
@@ -34,108 +35,209 @@ export default function Grade({ isComponent = false }) {
const exportToExcel = () => { const exportToExcel = () => {
import('xlsx').then((xlsx) => { import('xlsx').then(async (xlsx) => {
// 获取表格元素 const isValidResult = (result) => {
const table = document.querySelector('table'); const mt = result?.testconfig?.moduleType;
if (!table) return; if (mt === '8000') {
const lv = result?.testconfig?.params?.limitValue;
// 获取表头数据 const ps = (result?.CopperPerformanceStatus || '').toLowerCase();
const headers = []; const ct = result?.testconfig?.params?.cableType;
const headerCells = table.querySelectorAll('thead th'); const wo = result?.testconfig?.params?.wireOrder;
headerCells.forEach(cell => { const ref = !!result?.CopperRef;
headers.push(cell.textContent.trim()); let limitOk = false;
}); if (ps.includes('mptl')) {
limitOk = !!(lv?.includes('GBT') && lv?.includes('MPTL') && lv?.includes('PoE'));
// 获取表格数据 } else if (ps.includes('workshop')) {
const exportData = []; limitOk = !!lv?.includes('Profinet');
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const rowData = {};
const cells = row.querySelectorAll('td');
cells.forEach((cell, index) => {
// 获取单元格中的文本内容
let cellText = '';
// 查找所有span元素
const spans = cell.querySelectorAll('span');
if (spans.length > 0) {
// 如果有span元素合并它们的文本内容
cellText = Array.from(spans)
.map(span => span.textContent.trim())
.filter(text => text) // 过滤掉空文本
.join(' ');
} else { } else {
// 如果没有span元素直接使用单元格的文本内容 limitOk = !!lv?.includes('GBT');
cellText = cell.textContent.trim();
} }
rowData[headers[index]] = cellText; const cableOk = ct === 'Cat6 U/UTP';
let wireOk = false;
if (ps.includes('m12')) {
wireOk = !!wo?.includes('M12');
} else if (ps.includes('2p')) {
wireOk = !!wo?.includes('Ethernet');
} else {
wireOk = wo === 'T568B';
}
return limitOk && cableOk && wireOk && ref;
}
if (mt === 'cfp') {
const lv = result?.testconfig?.params?.limitValue;
const ct = result?.testconfig?.params?.cableType;
const connectorOk = Number(result?.testconfig?.params?.connectorCount) === 2;
const spliceOk = Number(result?.testconfig?.params?.spliceCount) === 2;
const refOk = result?.CFPRef === true;
const refConnOk = result?.CFPRefConnect === true;
const cleanOk = result?.PortCleanStatus === 2;
const limitOk = !!lv?.includes('GBT');
const cableOk = !!ct?.includes('OS2');
return limitOk && cableOk && connectorOk && spliceOk && refOk && refConnOk && cleanOk;
}
if (mt === 'ofp') {
const lv = result?.testconfig?.params?.limitValue;
const ct = result?.testconfig?.params?.cableType;
const refStatusOk = result?.ofpRefStatus === 'start';
const refConnOk = result?.OFPRefConnect === true;
const cleanOk = result?.PortCleanStatus === 2;
const limitOk = !!lv?.includes('GBT');
const cableOk = !!ct?.includes('OS2');
return limitOk && cableOk && refStatusOk && refConnOk && cleanOk;
}
return false;
};
const summary = dataSource.map((row) => {
const all = row.projects?.flatMap(p => p.testResults || []) || [];
const counted = all.slice(0, 6);
const validCount = counted.reduce((n, r) => n + (isValidResult(r) ? 1 : 0), 0);
const pass = row.testResultsCount?.passCount || 0;
const fail = row.testResultsCount?.failCount || 0;
return {
场景: scenario,
用户: row.userId,
项目: row.project || '',
测试总数: pass + fail,
计分测试条数: counted.length,
有效数据条数: validCount,
分数: row.score,
报告数: Array.isArray(row.reports) ? row.reports.length : 0,
最后更新时间: new Date(row.lastUpdate).toLocaleString('zh-CN')
};
}); });
exportData.push(rowData); const details = [];
dataSource.forEach((row) => {
const all = row.projects?.flatMap(p => p.testResults || []) || [];
const counted = all.slice(0, 6);
counted.forEach((r) => {
const mt = r?.testconfig?.moduleType || '';
const status =
mt === '8000' ? r.CopperResultStatus :
mt === 'cfp' ? r.CFPResultStatus :
mt === 'ofp' ? r.ofpResultStatus : '';
details.push({
场景: scenario,
用户: row.userId,
结果名称: r.name || '',
模块: mt,
有效性: isValidResult(r) ? '有效' : '无效',
检测结果: status || '',
极限值: r?.testconfig?.params?.limitValue || '',
线缆类型: r?.testconfig?.params?.cableType || '',
插座配置: r?.testconfig?.params?.wireOrder || '',
参考状态: mt === '8000' ? (r?.CopperRef ? '基准已设置' : '基准未设置')
: mt === 'cfp' ? (r?.CFPRef ? '参考已设置' : '参考未设置')
: mt === 'ofp' ? (r?.ofpRefStatus || '') : ''
}); });
});
// 创建工作簿并导出 });
let reference = [];
try {
const resp = await fetch(API_URLS.CONNECTION.MAP_WITH_SCENE(scenario));
if (resp.ok) {
const data = await resp.json();
reference = Object.entries(data || {})
.filter(([id]) => (
!id.includes('main-permanent') &&
!id.includes('main-channel') &&
!id.includes('remote-channel') &&
!id.includes('main-cfp-sm-out') &&
!id.includes('main-cfp-mm-out') &&
!id.includes('main-cfp-in') &&
!id.includes('remote-cfp-sm-out') &&
!id.includes('remote-cfp-mm-out') &&
!id.includes('remote-cfp-in')
))
.map(([room, conn]) => {
const ref = resolveFaultReference(conn);
return {
场景: scenario,
链路名称: `${room}-${conn.connectedTo}`,
类型: conn.type === 'fiber' ? '光纤' : '铜缆',
故障类型: ref?.type || '',
故障位置: ref?.location || '',
故障原因: ref?.reason || ''
};
});
}
} catch {}
const wb = xlsx.utils.book_new(); const wb = xlsx.utils.book_new();
const ws = xlsx.utils.json_to_sheet(exportData); const ws1 = xlsx.utils.json_to_sheet(summary);
xlsx.utils.book_append_sheet(wb, ws, '数据'); const ws2 = xlsx.utils.json_to_sheet(details);
const ws3 = xlsx.utils.json_to_sheet(reference);
// 生成文件名 xlsx.utils.book_append_sheet(wb, ws1, '概览');
xlsx.utils.book_append_sheet(wb, ws2, '用户明细');
xlsx.utils.book_append_sheet(wb, ws3, '参考答案');
const fileName = isHistoryMode const fileName = isHistoryMode
? `数据_历史_${historyInfo?.uuid || new Date().toISOString().split('T')[0]}.xlsx` ? `数据_历史_${historyInfo?.uuid || new Date().toISOString().split('T')[0]}_${scenario}.xlsx`
: `数据_${new Date().toISOString().split('T')[0]}.xlsx`; : `数据_${new Date().toISOString().split('T')[0]}_${scenario}.xlsx`;
xlsx.writeFile(wb, fileName); xlsx.writeFile(wb, fileName);
}); });
}; };
// 计算评分的函数
const calculateScore = (row) => { const calculateScore = (row) => {
let score = 0; let score = 0;
// 项目名称加2分
if (row.project === 'Joja-2025') { if (row.project === 'Joja-2025') {
score += 2; score += 10;
}else{ }else{
// console.log('项目错误'); console.log('项目错误');
} }
// 基准设置情况为已设置加2分
const allResults = row.projects?.flatMap(project => project.testResults || []) || []; const allResults = row.projects?.flatMap(project => project.testResults || []) || [];
const hasResults = allResults.length > 0; const countedResults = allResults.slice(0, 6);
const allSet = hasResults && allResults.every(result => result.CopperRef); score += countedResults.length * 7.5;
const validCount = countedResults.reduce((acc, result) => {
if (allSet) { const moduleType = result?.testconfig?.moduleType;
score += 2; if (moduleType === '8000') {
const limitValue = result?.testconfig?.params?.limitValue;
const performanceStatus = (result?.CopperPerformanceStatus || '').toLowerCase();
const cableType = result?.testconfig?.params?.cableType;
const wireOrder = result?.testconfig?.params?.wireOrder;
const copperRef = !!result?.CopperRef;
let limitValid = false;
if (performanceStatus.includes('mptl')) {
limitValid = !!(limitValue?.includes('GBT') && limitValue?.includes('MPTL') && limitValue?.includes('PoE'));
} else if (performanceStatus.includes('workshop')) {
limitValid = !!limitValue?.includes('Profinet');
} else { } else {
// console.log('未设置基准'); limitValid = !!limitValue?.includes('GBT');
} }
// 检查测试极限值是否正确 const cableTypeValid = cableType === 'Cat6 U/UTP';
const allHaveLimit = hasResults && allResults.every(result => let wireOrderValid = false;
result.testconfig?.params?.limitValue?.includes('GBT') if (performanceStatus.includes('m12')) {
); wireOrderValid = !!wireOrder?.includes('M12');
if (allHaveLimit) { } else if (performanceStatus.includes('2p')) {
score += 2; wireOrderValid = !!wireOrder?.includes('Ethernet');
} else { } else {
// console.log('未设置测试极限值'); wireOrderValid = wireOrder === 'T568B';
} }
return acc + (limitValid && cableTypeValid && wireOrderValid && copperRef ? 1 : 0);
// 检查命名方式是否符合xxx-xxx格式
const hasValidNaming = allResults.some(result =>
/^[^-]+(?:-[^-]+)+$/.test(result?.name || '')
);
if (hasValidNaming) {
score += 2;
} else {
// console.log('命名方式不符合格式');
} }
if (moduleType === 'cfp') {
// 检查测试结果总数是否大于6 const limitValue = result?.testconfig?.params?.limitValue;
const totalResults = row.testResultsCount.passCount + row.testResultsCount.failCount; const cableType = result?.testconfig?.params?.cableType;
if (totalResults >= 5) { const connectorValid = Number(result?.testconfig?.params?.connectorCount) === 2;
score += 2; const spliceValid = Number(result?.testconfig?.params?.spliceCount) === 2;
} else { const refValid = result?.CFPRef === true;
// console.log('测试结果总数小于5'); const refConnectValid = result?.CFPRefConnect === true;
const cleanValid = result?.PortCleanStatus === 2;
const limitValid = !!limitValue?.includes('GBT');
const cableValid = !!cableType?.includes('OS2');
return acc + (limitValid && cableValid && connectorValid && spliceValid && refValid && refConnectValid && cleanValid ? 1 : 0);
} }
if (moduleType === 'ofp') {
return score; const limitValue = result?.testconfig?.params?.limitValue;
const cableType = result?.testconfig?.params?.cableType;
const refStatusValid = result?.ofpRefStatus === 'start';
const refConnectValid = result?.OFPRefConnect === true;
const cleanValid = result?.PortCleanStatus === 2;
const limitValid = !!limitValue?.includes('GBT');
const cableValid = !!cableType?.includes('OS2');
return acc + (limitValid && cableValid && refStatusValid && refConnectValid && cleanValid ? 1 : 0);
}
return acc;
}, 0);
score += validCount * 7.5;
return Math.min(100, score);
}; };
const columns = [ const columns = [
@@ -220,7 +322,7 @@ export default function Grade({ isComponent = false }) {
// }, // },
{ {
title: '报告提交', title: '课后实验报告提交',
dataIndex: 'reports', dataIndex: 'reports',
key: 'reports', key: 'reports',
}, },
@@ -305,17 +407,87 @@ export default function Grade({ isComponent = false }) {
const [selectedRow, setSelectedRow] = useState(null); const [selectedRow, setSelectedRow] = useState(null);
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [showReference, setShowReference] = useState(false);
const [referenceConnections, setReferenceConnections] = useState([]);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [alertVisible, setAlertVisible] = useState(false);
const [alertMessage, setAlertMessage] = useState('');
const [alertType, setAlertType] = useState('info');
const handleClearTeachingData = async () => {
try {
const resp = await fetch(API_URLS.TEACHING.CLEAR, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
scenario,
org: orgFromUrl || ''
})
});
if (resp.ok) {
setDataSource([]);
setOnlineCount(0);
setIsHistoryMode(false);
setHistoryInfo(null);
setAlertMessage('已清除历史数据');
setAlertType('success');
setAlertVisible(true);
} else {
setAlertMessage('删除失败');
setAlertType('error');
setAlertVisible(true);
}
} catch (e) {
setAlertMessage('删除异常');
setAlertType('error');
setAlertVisible(true);
} finally {
setShowDeleteConfirm(false);
}
};
const handleRowClick = (record) => { const handleRowClick = (record) => {
setSelectedRow(record); setSelectedRow(record);
setIsModalVisible(true); setIsModalVisible(true);
}; };
useEffect(() => {
if (!isModalVisible || !showReference) return;
const fetchReference = async () => {
try {
const response = await fetch(API_URLS.CONNECTION.MAP_WITH_SCENE(scenario));
if (!response.ok) return;
const data = await response.json();
const formatted = Object.entries(data || {})
.filter(([portId]) => (
!portId.includes('main-permanent') &&
!portId.includes('main-channel') &&
!portId.includes('remote-channel') &&
!portId.includes('main-cfp-sm-out') &&
!portId.includes('main-cfp-mm-out') &&
!portId.includes('main-cfp-in') &&
!portId.includes('remote-cfp-sm-out') &&
!portId.includes('remote-cfp-mm-out') &&
!portId.includes('remote-cfp-in')
))
.map(([source, connection]) => ({
room: source,
rack: connection.connectedTo,
meta: connection
}));
setReferenceConnections(formatted);
} catch (e) {
console.error('参考答案数据获取失败:', e);
}
};
fetchReference();
}, [isModalVisible, showReference, scenario]);
// 处理文件上传 // 处理文件上传
const handleFileUpload = async (event) => { const handleFileUpload = async (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file || !file.name.endsWith('.est')) { if (!file || !file.name.endsWith('.est')) {
alert('请选择.est文件'); setAlertMessage('请选择.est文件');
setAlertType('warning');
setAlertVisible(true);
return; return;
} }
@@ -338,7 +510,9 @@ export default function Grade({ isComponent = false }) {
const data = jsonData; const data = jsonData;
if (!data.competitionStatus?.statisticsData) { if (!data.competitionStatus?.statisticsData) {
alert('文件格式不正确'); setAlertMessage('文件格式不正确');
setAlertType('error');
setAlertVisible(true);
return; return;
} }
@@ -373,7 +547,9 @@ export default function Grade({ isComponent = false }) {
setOnlineCount(sortedData.length); setOnlineCount(sortedData.length);
} catch (error) { } catch (error) {
console.error('读取文件失败:', error); console.error('读取文件失败:', error);
alert('读取文件失败'); setAlertMessage('读取文件失败');
setAlertType('error');
setAlertVisible(true);
} }
}; };
@@ -386,6 +562,16 @@ export default function Grade({ isComponent = false }) {
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold text-[#0ff]">详细信息 - {selectedRow?.userId || ''}</h2> <h2 className="text-xl font-bold text-[#0ff]">详细信息 - {selectedRow?.userId || ''}</h2>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 text-[#0ff]/80">
<span>显示参考答案</span>
<input
type="checkbox"
checked={showReference}
onChange={(e) => setShowReference(e.target.checked)}
className="accent-[#0ff]"
/>
</label>
<button <button
onClick={() => setIsModalVisible(false)} onClick={() => setIsModalVisible(false)}
className="text-[#0ff] hover:text-[#0ff]/70" className="text-[#0ff] hover:text-[#0ff]/70"
@@ -393,10 +579,11 @@ export default function Grade({ isComponent = false }) {
× ×
</button> </button>
</div> </div>
</div>
<div className="flex flex-1 gap-6 overflow-hidden"> <div className="flex flex-1 gap-6 overflow-hidden">
{/* 左侧项目列表 */} {/* 左侧项目列表 */}
<div className="w-1/2 overflow-y-auto custom-scrollbar bg-[#0F172A] rounded-lg p-4"> <div className={`${showReference ? 'w-1/3' : 'w-1/2'} overflow-y-auto custom-scrollbar bg-[#0F172A] rounded-lg p-4`}>
<div className="text-lg font-bold text-[#0ff] mb-4">测试数据</div> <div className="text-lg font-bold text-[#0ff] mb-4">测试数据</div>
{selectedRow?.projects?.map((project) => ( {selectedRow?.projects?.map((project) => (
<div key={project.id} className="mb-6"> <div key={project.id} className="mb-6">
@@ -430,28 +617,6 @@ export default function Grade({ isComponent = false }) {
<span className="text-[#0ff] text-lg font-semibold">{result.name}</span> <span className="text-[#0ff] text-lg font-semibold">{result.name}</span>
{(() => { {(() => {
// 命名判断
// const validNames = [
// 'Room1-TO-1-1F-RackA-1A-1', 'Room1-TO-2-1F-RackA-1A-2',
// 'Room3-TO-1-1F-RackA-1A-5', 'Room3-TO-2-1F-RackA-1A-6',
// 'Room4-CAM-1F-RackA-1A-12',
// '2F-RackA-1A-1-1F-RackA-1B-1', '2F-RackA-1A-2-1F-RackA-1B-2',
// '2F-RackA-1A-3-1F-RackA-1B-3', '2F-RackA-1A-4-1F-RackA-1B-4',
// '2F-RackA-1A-6-1F-RackA-1B-5', '2F-RackA-1A-5-1F-RackA-1B-6',
// 'R-M-A-1-PLC-Rack-1A-1', 'R-M-A-2-PLC-Rack-1A-2',
// 'PLC-Rack-1B-1-Data-Rack-1A-1', 'PLC-Rack-1B-2-Data-Rack-1A-2',
// '1F-RackA-1A-1-Room1-TO-1', '1F-RackA-1A-2-Room1-TO-2',
// '1F-RackA-1A-5-Room3-TO-1', '1F-RackA-1A-6-Room3-TO-2',
// '1F-RackA-1A-12-Room4-CAM',
// '1F-RackA-1B-1-2F-RackA-1A-1', '1F-RackA-1B-2-2F-RackA-1A-2',
// '1F-RackA-1B-3-2F-RackA-1A-3', '1F-RackA-1B-4-2F-RackA-1A-4',
// '1F-RackA-1B-5-2F-RackA-1A-6', '1F-RackA-1B-6-2F-RackA-1A-5',
// 'PLC-Rack-1A-1-R-M-A-1', 'PLC-Rack-1A-2-R-M-A-2',
// 'Data-Rack-1A-1-PLC-Rack-1B-1', 'Data-Rack-1A-2-PLC-Rack-1B-2',
// '1F-RackA-1C-1', '1F-RackA-1C-2', '1F-RackA-1C-3', '1F-RackA-1C-4'
// ];
// const isValidName = validNames.includes(result.name);
const moduleType = result.testconfig?.moduleType; const moduleType = result.testconfig?.moduleType;
let isValid = false; let isValid = false;
@@ -766,7 +931,7 @@ export default function Grade({ isComponent = false }) {
{/* 右侧报告列表 */} {/* 右侧报告列表 */}
<div className="w-1/2 overflow-y-auto custom-scrollbar bg-[#0F172A] rounded-lg p-4"> <div className={`${showReference ? 'w-1/3' : 'w-1/2'} overflow-y-auto custom-scrollbar bg-[#0F172A] rounded-lg p-4`}>
<div className="text-lg font-bold text-[#0ff] mb-4">报告记录</div> <div className="text-lg font-bold text-[#0ff] mb-4">报告记录</div>
{(!selectedRow?.reports || selectedRow.reports.length === 0) ? ( {(!selectedRow?.reports || selectedRow.reports.length === 0) ? (
<div className="text-[#ff6600] text-center mt-8">暂无提交报告</div> <div className="text-[#ff6600] text-center mt-8">暂无提交报告</div>
@@ -806,12 +971,97 @@ export default function Grade({ isComponent = false }) {
)} )}
</div> </div>
{/* 右侧参考答案列表 */}
{showReference && (
<div className="w-1/3 overflow-y-auto custom-scrollbar bg-[#0F172A] rounded-lg p-4">
<div className="text-lg font-bold text-[#0ff] mb-4">参考答案</div>
{referenceConnections.length === 0 ? (
<div className="text-[#ff6600] text-center mt-8">暂无参考数据</div>
) : (
referenceConnections.map((conn, idx) => {
const fault = (() => {
const m = conn.meta || {};
let statusKey = '';
if (m.wiremapstatus !== undefined && m.wiremapstatus !== null && String(m.wiremapstatus).length > 0) {
if (String(m.wiremapstatus).toLowerCase() === 'pass') {
statusKey = m.performancestatus ? `performancestatus:${m.performancestatus}` : '';
} else {
statusKey = `wiremapstatus:${m.wiremapstatus}`;
}
} else if (m.fiberstatus) {
statusKey = `fiberstatus:${m.fiberstatus}`;
}
return statusKey ? faultReferenceDict[statusKey] || null : null;
})();
return (
<div key={idx} className="mb-6 p-4 bg-[#1E293B] rounded-lg border border-[#0ff]/20">
<div className="flex justify-between items-center mb-2">
<span className="text-[#0ff] font-semibold">链路名称{`${conn.room}-${conn.rack}`}</span>
<span className="text-[#0ff]/70 text-sm">{conn.meta?.type === 'fiber' ? '光纤' : '铜缆'}</span>
</div>
<div className="text-[#0ff]/90">
{fault ? (
<div className="mb-2 p-2 rounded-md bg-[#334155] border border-[#0ff]/10">
<div className="text-sm">类型: {fault.type}</div>
<div className="text-sm">位置: {fault.location}</div>
<div className="text-sm">原因: {fault.reason}</div>
</div>
) : (
<div className="text-sm text-green-400 mt-2">未匹配到故障参考</div>
)}
</div>
</div>
);
})
)}
</div>
)}
</div> </div>
</div> </div>
</div> </div>
)} )}
<div className="p-4 md:p-6 bg-[#0F172A] "> <div className="p-4 md:p-6 bg-[#0F172A] ">
{alertVisible && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-[#1E293B] rounded-lg p-6 w-full max-w-md">
<div className="text-xl font-bold text-[#0ff] mb-4">提示</div>
<div className={`${alertType === 'success' ? 'text-green-400' : alertType === 'error' ? 'text-red-400' : 'text-yellow-300'} mb-6`}>
{alertMessage}
</div>
<div className="flex justify-end">
<button
onClick={() => setAlertVisible(false)}
className="bg-[#0ff]/20 hover:bg-[#0ff]/30 text-[#0ff] px-4 py-2 rounded transition-colors"
>
确定
</button>
</div>
</div>
</div>
)}
{showDeleteConfirm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-[#1E293B] rounded-lg p-6 w-full max-w-md">
<div className="text-xl font-bold text-[#0ff] mb-4">确认删除</div>
<div className="text-[#0ff]/80 mb-6">此操作将删除历史数据请确认</div>
<div className="flex justify-end gap-3">
<button
onClick={() => setShowDeleteConfirm(false)}
className="bg-[#0ff]/20 hover:bg-[#0ff]/30 text-[#0ff] px-4 py-2 rounded transition-colors"
>
取消
</button>
<button
onClick={handleClearTeachingData}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 px-4 py-2 rounded transition-colors"
>
确认删除
</button>
</div>
</div>
</div>
)}
<div className="mb-4 md:mb-6 p-4 md:p-6 bg-[#1E293B] rounded-lg shadow-lg border border-[#0ff]/20"> <div className="mb-4 md:mb-6 p-4 md:p-6 bg-[#1E293B] rounded-lg shadow-lg border border-[#0ff]/20">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
@@ -845,6 +1095,7 @@ export default function Grade({ isComponent = false }) {
</select> </select>
<button <button
onClick={() => { onClick={() => {
exportToExcel(); exportToExcel();
@@ -854,6 +1105,13 @@ export default function Grade({ isComponent = false }) {
导出至Excel 导出至Excel
</button> </button>
<button
onClick={() => setShowDeleteConfirm(true)}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 px-4 py-2 rounded transition-colors"
>
删除历史数据
</button>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,54 @@
export const faultReferenceDict = {
'wiremapstatus:open': { type: '开路', location: '线芯1距离主机端0米距离远端21.4米开路', reason: '可能是靠近主机端连接器压接不到位' },
'wiremapstatus:open2': { type: '开路', location: '线对45距离主机端0.2米距离远端22.4米开路', reason: '可能是靠近主机端连接器压接不到位' },
'wiremapstatus:open3': { type: '开路', location: '线芯2、6、4、5、7、8距离主机端11米远端13米开路', reason: '可能是被老鼠咬了' },
'wiremapstatus:short': { type: '短路', location: '线芯1、2、6距离主机端1.1米处短路', reason: '可能是靠近主机端的位置线缆外护套破损' },
'wiremapstatus:short2': { type: '短路', location: '线芯1、6、7距离远端1.1米处短路', reason: '可能是靠近远端的位置线缆外护套破损' },
'wiremapstatus:short3': { type: '短路', location: '线芯1、2、4、8距离主机端0.1米处短路', reason: '可能是端接时外护套开剥过深' },
'wiremapstatus:cross': { type: '跨接', location: '线对1、2与线对3、6跨接', reason: '端接错误' },
'wiremapstatus:cross2': { type: '跨接', location: '线对1、2与线对4、5跨接', reason: '端接错误' },
'wiremapstatus:cross3': { type: '跨接', location: '线对3、6与线对4、5跨接', reason: '端接错误' },
'wiremapstatus:reversed': { type: '交叉', location: '线对4、5交叉', reason: '端接错误' },
'wiremapstatus:reversed2': { type: '交叉', location:'线对1、2交叉', reason: '端接错误' },
'wiremapstatus:reversed3': { type: '交叉', location: '线对3、6线对7、8交叉', reason: '端接错误' },
'wiremapstatus:miswire': { type: '乱序', location: '无法定位', reason: '端接错误' },
'wiremapstatus:sopen': { type: '屏蔽层开路', location: '无法定位', reason: '未正确接地' },
'wiremapstatus:ShieldOpen': { type: '屏蔽层开路', location: '无法定位', reason: '未正确接地' },
'wiremapstatus:ShieldShort': { type: '屏蔽层短路', location: '线芯4距离主机端0.1米处短路', reason: '未正确可能是端接时外护套开剥过深' },
'performancestatus:return-loss-fail': { type: '回波损耗不合格', location: '整条链路', reason: '链路阻抗不连续' },
'performancestatus:next-fail': { type: '近端串扰不合格', location: '两端连接器', reason: '两端连接器的串扰过高' },
'performancestatus:lengthfail': { type: '长度超限', location: '整条链路', reason: '链路长度超过标准,导致插入损耗过高' },
'performancestatus:ohmfail': { type: '电阻类故障', location: '线对1、2', reason: '导体电阻偏高或不均衡' },
'performancestatus:mptl-nextfail': { type: '电阻类故障', location: '线对4、5', reason: '线对间电阻差过大' },
'performancestatus:pass': { type: '通过', location: '无', reason: '链路通过' },
'performancestatus:pass-4m': { type: '通过', location: '无', reason: '链路通过' },
'performancestatus:pass-30m': { type: '通过', location: '无', reason: '链路通过' },
'performancestatus:workshop-m12-pass-30m': { type: '通过', location: '无', reason: '链路通过' },
'fiberstatus:bend': { type: '光纤弯曲', location: '在48.95米处出现弯曲', reason: '可能是光纤出现扭曲/弯折' },
'fiberstatus:connector-fail-start': { type: '连接器故障', location: '距离主机的第一个连接器', reason: '可能是耦合器质量较差或者连接器损坏/污染' },
'fiberstatus:connector-fail-end': { type: '连接器故障', location: '最后一个连接器', reason: '可能是耦合器质量较差或者连接器损坏/污染' },
'fiberstatus:splice-fail': { type: '熔接点故障', location: '在12.3米处', reason: '熔接点损耗过大' },
'fiberstatus:sm-pass': { type: '单模通过', location: '无', reason: '链路通过' },
};
export const resolveFaultReference = (meta) => {
const m = meta || {};
const wm = m.wiremapstatus;
const pm = m.performancestatus;
const fs = m.fiberstatus;
let key = '';
if (wm !== undefined && wm !== null && String(wm).length > 0) {
const wmStr = String(wm).toLowerCase();
if (wmStr.startsWith('pass')) {
key = pm ? `performancestatus:${pm}` : '';
} else {
key = `wiremapstatus:${wm}`;
}
} else if (fs) {
key = `fiberstatus:${fs}`;
}
if (!key) return null;
return faultReferenceDict[key] || null;
};