This commit is contained in:
2025-09-16 16:39:48 +08:00
commit c5808e85e2
336 changed files with 695951 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
import { useEffect, useState } from 'react';
import useDisplayStore from '@/store/displayStore';
import useDeviceStore from '@/store/deviceStore';
import FingerprintJS from '@fingerprintjs/fingerprintjs';
import { API_URLS } from '@/config/api';
export default function CompetitionStatistics() {
const [fingerprint, setFingerprint] = useState('');
const { faultScenarios, seatNumber,seatUUID, reports,cleanStatus} = useDeviceStore();
const { getCurrentProject,getCurrentOperator, getCurrentTestConfig} = useDisplayStore();
const UUID = seatUUID;
const SeatNumber = seatNumber;
// 新增:获取历史数据
const fetchHistoryData = async (uuid, fp) => {
try {
const response = await fetch(`${API_URLS.COMPETITION.DATA}?UUID=${uuid}&fingerprint=${fp}`);
if (response.ok) {
const data = await response.json();
if (data[fp]?.projects) {
useDisplayStore.getState().updateProjects(data[fp].projects);
}
if (data[fp]?.selectedIndexes) {
useDisplayStore.getState().updateselectedIndexes(data[fp].selectedIndexes);
}
}
} catch (error) {
console.error('获取历史数据失败:', error);
}
};
// 初始化指纹
useEffect(() => {
if (!UUID || !SeatNumber) return;
const initFingerprint = async () => {
try {
const fp = await FingerprintJS.load();
const { visitorId } = await fp.get();
// 将SeatNumber编码后混合到指纹中
const encodedSeatNumber = encodeURIComponent(SeatNumber);
// const mixedFingerprint = `${visitorId}-${encodedSeatNumber}`;
const mixedFingerprint = `${encodedSeatNumber}`;
setFingerprint(mixedFingerprint);
// 获取历史数据
await fetchHistoryData(UUID, mixedFingerprint);
} catch (error) {
console.error('生成指纹失败:', error);
}
};
initFingerprint();
}, [UUID, SeatNumber]);
// 定时发送数据
useEffect(() => {
if (!UUID || !fingerprint) return;
let intervalId = null; // 声明intervalId变量
// 设置定时器每3秒发送一次数据
intervalId = setInterval(() => {
sendStatistics();
}, 3000);
// 获取基准设置状态
const getRefStatus = () => {
const currentConfig = getCurrentTestConfig();
const moduleType = currentConfig?.moduleType;
if (!moduleType) return false;
const { ref } = useDisplayStore.getState();
if (moduleType === '8000') return ref.copper?.status || false;
if (moduleType === 'cfp') return ref.cfp?.status || false;
if (moduleType === 'ofp') return ref.ofp?.status || false;
return false;
};
// 获取测试结果统计
const getTestResults = () => {
const currentProject = getCurrentProject();
if (!currentProject?.testResults) return { passCount: 0, failCount: 0 };
const passCount = currentProject?.testResults.filter(result => {
const copperStatus = result.CopperResultStatus;
const cfpStatus = result.CFPResultStatus;
const ofpStatus = result.ofpResultStatus;
if (copperStatus) {
return copperStatus === 'pass';
} else if (cfpStatus) {
return cfpStatus === 'pass';
} else if (ofpStatus) {
return ofpStatus === 'pass';
}
return false;
}).length;
const failCount = currentProject?.testResults.filter(result => {
const copperStatus = result.CopperResultStatus;
const cfpStatus = result.CFPResultStatus;
const ofpStatus = result.ofpResultStatus;
if (copperStatus) {
return copperStatus === 'fail';
} else if (cfpStatus) {
return cfpStatus === 'fail';
} else if (ofpStatus) {
return ofpStatus === 'fail';
}
return false;
}).length;
return { passCount, failCount };
};
// 发送统计数据
const sendStatistics = async () => {
const currentProject = getCurrentProject();
const currentOperator = getCurrentOperator();
const currentConfig = getCurrentTestConfig();
const refStatus = getRefStatus();
const { passCount, failCount } = getTestResults();
const currentReports = useDeviceStore.getState().reports;
const statisticsData = {
userId: seatNumber,
scenario: faultScenarios,
project: currentProject?.name || '',
operator: currentOperator?.name || '',
testLimit: currentConfig?.params?.limitValue || '',
refStatus,
testResultsCount: { passCount, failCount },
projects: useDisplayStore.getState().projects,
selectedIndexes:useDisplayStore.getState().selectedIndexes,
reports: currentReports,
};
try {
const response = await fetch(API_URLS.COMPETITION.DATA, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
UUID,
fingerprint,
timestamp: Date.now(),
data: statisticsData
}),
});
if (!response.ok) {
const error = await response.json();
console.error('发送比赛数据失败:', error);
}
} catch (error) {
console.error('发送比赛数据出错:', error);
}
};
// 返回清理函数
return () => {
if (intervalId) {
clearInterval(intervalId);
}
};
}, [seatUUID, fingerprint, faultScenarios, seatNumber, getCurrentProject, getCurrentTestConfig]);
return null;
}

View File

@@ -0,0 +1,442 @@
import React, { useEffect } from 'react';
import useDeviceStore from '@/store/deviceStore';
import useDisplayStore from '@/store/displayStore';
import { API_URLS } from '@/config/api';
// 构建连接图
function buildConnectionGraph(connections) {
const graph = {};
connections.forEach(conn => {
graph[conn.source] = graph[conn.source] || [];
graph[conn.target] = graph[conn.target] || [];
graph[conn.source].push(conn.target);
graph[conn.target].push(conn.source);
});
return graph;
}
// 查找跳线/连接器的另一端
function findCounterpart(nodeName) {
const jumperTypes = [
{ pattern: /(pachcode-copper|pachcode-m12-d|pachcode-m12-x|sm-lc-lc|mm-lc-lc|smc-lc-lc|mmc-lc-lc|mm-mpo-mpo|sm-mpo-mpo)/,
replace: (n) => n.includes('-left') ? n.replace('-left', '-right') : n.replace('-right', '-left') },
{ pattern: /lc-connector-\d+-(left|right)/,
replace: (n) => n.includes('-left') ? n.replace('-left', '-right') : n.replace('-right', '-left') },
// MPO-LC连接器穿透逻辑 - 从LC端口到MPO端口
{ pattern: /(mm-mpo-lc-box-\d+-lc-port)(\d+)/,
replace: (n) => {
const matches = n.match(/(mm-mpo-lc-box-\d+)-lc-port(\d+)/);
if (matches) {
return `${matches[1]}-mpo-port1`; // 从任意LC端口穿透到对应的MPO端口
}
return n;
}
},
];
for (const { pattern, replace } of jumperTypes) {
if (nodeName.match(pattern)) return replace(nodeName);
}
return null;
}
// 完整路径追踪
function traceFullPath(graph, startPoint) {
const visited = new Set();
const path = [];
let current = startPoint;
while (current && !visited.has(current)) {
visited.add(current);
path.push(current);
// 跳线穿透逻辑
const counterpart = findCounterpart(current);
if (counterpart && graph[counterpart] && !visited.has(counterpart)) {
current = counterpart;
continue;
}
// 常规连接
const neighbors = (graph[current] || []).filter(n => !visited.has(n));
current = neighbors[0];
}
// 检测路径中是否存在MPO-LC端口并修改end值
let endPoint = path[path.length - 1] || startPoint;
// 检查路径中是否包含MPO-LC端口
for (const item of path) {
const matches = item.match(/(mm-mpo-lc-box-\d+-lc-port)(\d+)/);
if (matches) {
// 在end值上添加端口号
endPoint = `${endPoint}-port${matches[2]}`;
break; // 只使用第一个匹配的端口号
}
}
return {
start: startPoint,
end: endPoint,
path: path,
length: path.length - 1
};
}
// 8000型分析
function analyze8000(graph) {
const result = { mainPaths: {}, remotePaths: {} };
// 动态识别端口
const analyzePorts = (prefix, target) => {
Object.keys(graph)
.filter(k => k.startsWith(prefix))
.forEach(port => {
const trace = traceFullPath(graph, port);
target[port] = {
...trace,
portType: port.includes('channel') ? 'channel' : 'permanent'
};
});
};
analyzePorts('main-', result.mainPaths);
analyzePorts('remote-', result.remotePaths);
return result;
}
// CFP型分析
function analyzeCFP(graph) {
const result = { mainPaths: {}, remotePaths: {} };
// 端口配置
const PORT_CONFIG = {
main: {
outputs: ['main-cfp-sm-out', 'main-cfp-mm-out'],
input: 'main-cfp-in',
vfl:'main-vfl'
},
remote: {
outputs: ['remote-cfp-sm-out', 'remote-cfp-mm-out'],
input: 'remote-cfp-in'
}
};
// 分析端口连接
const analyzePort = (port, type) => {
if (!graph[port]) return;
const trace = traceFullPath(graph, port);
result[`${type}Paths`][port] = {
...trace,
};
};
// 分析所有端口
Object.values(PORT_CONFIG.main).flat().forEach(p => analyzePort(p, 'main'));
Object.values(PORT_CONFIG.remote).flat().forEach(p => analyzePort(p, 'remote'));
return result;
}
// OFP型分析
function analyzeOFP(graph) {
const result = { mainPaths: {} };
['main-ofp-sm-out', 'main-ofp-mm-out','main-vfl'].forEach(port => {
if (graph[port]) {
const trace = traceFullPath(graph, port);
result.mainPaths[port] = {
...trace,
};
}
});
return result;
}
// 创建通用连接状态检查
const setConnectionStatus = async (result, moduleType, connections) => {
try {
// 从API获取连接配置
const deviceStore = useDeviceStore.getState();
const faultScenario = deviceStore.faultScenarios || '';
const apiUrl = API_URLS.CONNECTION.MAP_WITH_SCENE(faultScenario);
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error('获取连接配置失败');
}
const data = await response.json();
const connectionMap = data || {};
const mainPaths = result.mainPaths;
const mainEnd = mainPaths ? Object.values(mainPaths)[0]?.end || null : null;
const mainStart = mainPaths ? Object.values(mainPaths)[0]?.start || null : null;
// OFP模块特殊处理
if (moduleType === 'ofp' && mainEnd && mainPaths) {
// 直接查找mainEnd作为键的情况
if (connectionMap[mainEnd]) {
result.OFPStatus = connectionMap[mainEnd]?.fiberstatus || null;
result.OFPConnectedTo = connectionMap[mainEnd]?.connectedTo || null;
} else {
// 如果mainEnd不是键查找connectedTo等于mainEnd的项
for (const key in connectionMap) {
if (connectionMap[key]?.connectedTo === mainEnd && connectionMap[key]?.type === 'fiber') {
result.OFPStatus = connectionMap[key]?.fiberstatus || null;
result.OFPConnectedTo = key;
break;
}
}
}
// 查找OFPConnectedTo对应的source值
if (result.OFPConnectedTo && connections) {
const matchingConnection = connections.find(conn => conn.target === result.OFPConnectedTo);
result.OFPConnectedToRefStatus = matchingConnection ? matchingConnection.source : null;
}
// 查找mainEnd对应的source值
if (mainStart && connections) {
const mainStartConnection = connections.find(conn => conn.target === mainStart);
result.OFPoutRefStatus = mainStartConnection ? mainStartConnection.source : null;
}
return;
}
// 其他模块的处理逻辑
const remotePaths = result.remotePaths || {};
const remoteEnd = Object.values(remotePaths)[0]?.end;
// 端口互通检查
if (mainEnd && remoteEnd) {
// CFP模块特殊处理
if (moduleType === 'cfp') {
// 获取四个端点
const mainSmOutEnd = mainPaths['main-cfp-sm-out']?.end;
const mainMmOutEnd = mainPaths['main-cfp-mm-out']?.end;
const mainInEnd = mainPaths['main-cfp-in']?.end;
const remoteSmOutEnd = remotePaths['remote-cfp-sm-out']?.end;
const remoteMmOutEnd = remotePaths['remote-cfp-mm-out']?.end;
const remoteInEnd = remotePaths['remote-cfp-in']?.end;
// 检查两对连接
const checkConnection = (end1, end2) => {
if (!end1 || !end2) return false;
return Object.keys(connectionMap).some(key => {
const connection = connectionMap[key];
return (connection.connectedTo === end1 && key === end2) ||
(connection.connectedTo === end2 && key === end1);
});
};
// 分别检查单模和多模的连接
const isMainToRemoteSmConnected = checkConnection(mainSmOutEnd, remoteInEnd);
const isMainToRemoteMmConnected = checkConnection(mainMmOutEnd, remoteInEnd);
const isRemoteToMainSmConnected = checkConnection(remoteSmOutEnd, mainInEnd);
const isRemoteToMainMmConnected = checkConnection(remoteMmOutEnd, mainInEnd);
// 任一对连接成功即认为连接成功
const isConnected = (isMainToRemoteSmConnected && isRemoteToMainSmConnected) ||
(isMainToRemoteMmConnected && isRemoteToMainMmConnected);
result.CFPConnectStatus = isConnected;
// 如果连接存在,获取详细状态信息
if (isConnected) {
let connectionKey1, connectionKey2;
if (isMainToRemoteSmConnected && isRemoteToMainSmConnected) {
// 单模连接
connectionKey1 = Object.keys(connectionMap).find(key =>
(connectionMap[key].connectedTo === mainSmOutEnd && key === remoteInEnd) ||
(connectionMap[key].connectedTo === remoteInEnd && key === mainSmOutEnd)
);
connectionKey2 = Object.keys(connectionMap).find(key =>
(connectionMap[key].connectedTo === remoteSmOutEnd && key === mainInEnd) ||
(connectionMap[key].connectedTo === mainInEnd && key === remoteSmOutEnd)
);
} else {
// 多模连接
connectionKey1 = Object.keys(connectionMap).find(key =>
(connectionMap[key].connectedTo === mainMmOutEnd && key === remoteInEnd) ||
(connectionMap[key].connectedTo === remoteInEnd && key === mainMmOutEnd)
);
connectionKey2 = Object.keys(connectionMap).find(key =>
(connectionMap[key].connectedTo === remoteMmOutEnd && key === mainInEnd) ||
(connectionMap[key].connectedTo === mainInEnd && key === remoteMmOutEnd)
);
}
if (connectionKey1 && connectionKey2) {
const connection1 = connectionMap[connectionKey1];
const connection2 = connectionMap[connectionKey2];
result.CFPMainIn = connection1.fiberstatus;
result.CFPMainOut = connection2.fiberstatus;
}
} else {
result.CFPMainIn = 'unknown';
result.CFPMainOut = 'unknown';
}
} else {
// 其他模块的处理逻辑
const isConnected = Object.keys(connectionMap).some(key => {
const connection = connectionMap[key];
return (connection.connectedTo === mainEnd && key === remoteEnd) ||
(connection.connectedTo === remoteEnd && key === mainEnd);
});
// 设置连接状态
const statusPrefix = moduleType.toUpperCase();
result[`${statusPrefix}ConnectStatus`] = isConnected;
// 如果连接存在,获取详细状态信息
if (isConnected) {
const connectionKey = Object.keys(connectionMap).find(key =>
(connectionMap[key].connectedTo === mainEnd && key === remoteEnd) ||
(connectionMap[key].connectedTo === remoteEnd && key === mainEnd)
);
if (connectionKey) {
const connection = connectionMap[connectionKey];
switch(moduleType) {
case '8000':
result.CopperWiremapStatus = connection.wiremapstatus;
result.CopperPerformanceStatus = connection.performancestatus;
break;
}
}
} else {
// 未连接时的默认状态
switch(moduleType) {
case '8000':
result.CopperWiremapStatus = 'unknown';
result.CopperPerformanceStatus = 'unknown';
break;
case 'cfp':
result.CFPMainIn = 'unknown';
result.CFPMainOut = 'unknown';
break;
}
}
}
} else {
// 无效连接时的默认状态
const statusPrefix = moduleType.toUpperCase();
result[`${statusPrefix}ConnectStatus`] = false;
switch(moduleType) {
case '8000':
result.CopperWiremapStatus = 'unknown';
result.CopperPerformanceStatus = 'unknown';
break;
case 'cfp':
result.CFPMainIn = 'unknown';
result.CFPMainOut = 'unknown';
break;
}
}
} catch (error) {
console.error('获取连接配置错误:', error);
// 错误时的默认状态
const statusPrefix = moduleType.toUpperCase();
result[`${statusPrefix}ConnectStatus`] = false;
switch(moduleType) {
case '8000':
result.CopperWiremapStatus = 'unknown';
result.CopperPerformanceStatus = 'unknown';
break;
case 'cfp':
result.CFPMainIn = 'unknown';
result.CFPMainOut = 'unknown';
break;
case 'ofp':
result.OFPStatus = 'unknown';
break;
}
}
};
// 组装最终输出的分析结果
async function analyzeAllConnections(connections) {
const deviceStore = useDeviceStore.getState();
const mainModule = deviceStore.mainUnitModules[0]?.id;
const remoteModule = deviceStore.remoteUnitModules[0]?.id;
// 处理没有连接的情况
if (!connections || connections.length === 0) {
return { mainModule, remoteModule };
}
const graph = buildConnectionGraph(connections);
const result = { mainModule, remoteModule };
if (mainModule?.includes('8000')) {
Object.assign(result, analyze8000(graph));
} else if (mainModule?.includes('cfp')) {
Object.assign(result, analyzeCFP(graph));
} else if (mainModule?.includes('ofp')) {
Object.assign(result, analyzeOFP(graph));
} else {
result.unknownPaths = {};
Object.keys(graph).forEach(port => {
result.unknownPaths[port] = traceFullPath(graph, port);
});
}
// 检查端口互通状态并更新结果
const moduleTypeMap = {
'8000': '8000',
'cfp': 'cfp',
'ofp': 'ofp'
};
const currentModuleType = moduleTypeMap[mainModule?.split('-')[0]] || '8000';
await setConnectionStatus(result, currentModuleType,connections);
return result;
}
// 同步分析结果到deivceStore
const ConnectionAnalyzer = () => {
const connectionPaths = useDeviceStore(state => state.connectionPaths);
const updateConnectionStatus = useDeviceStore(state => state.updateConnectionStatus);
useEffect(() => {
// 创建一个异步函数来处理分析
const analyzeConnections = async () => {
try {
const result = await analyzeAllConnections(connectionPaths);
updateConnectionStatus(result);
//console.log('连接分析结果:', result);
} catch (error) {
console.error('连接分析错误:', error);
// 发生错误时更新状态为未连接
updateConnectionStatus({
mainModule: null,
remoteModule: null,
});
}
};
// 执行分析
analyzeConnections();
}, [connectionPaths, updateConnectionStatus]);
return null;
};
export default ConnectionAnalyzer;

View File

@@ -0,0 +1,37 @@
import { useEffect, useRef } from 'react';
export default function ContextMenu({ x, y, onClose, items }) {
const menuRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
onClose();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [onClose]);
return (
<div
ref={menuRef}
className="fixed bg-[#1E293B] border border-[#0ff]/20 rounded-lg shadow-lg py-1 z-1001"
style={{ left: x, top: y }}
>
{items.map((item, index) => (
<button
key={index}
className="w-full px-4 py-2 text-left text-sm text-[#0ff] hover:bg-[#0ff]/10 hover:text-[#00ff7f]"
onClick={() => {
item.onClick();
onClose();
}}
>
{item.label}
</button>
))}
</div>
);
}

372
src/components/Cursors.js Normal file
View File

@@ -0,0 +1,372 @@
import { useState, useEffect, useRef } from 'react';
import useDeviceStore from '@/store/deviceStore';
import useDisplayStore from '@/store/displayStore';
import { shallow } from 'zustand/shallow';
import StatusToast from '@/components/lib/StatusToast';
import { API_URLS } from '@/config/api';
const ToneSound = typeof Audio !== 'undefined' ? new Audio('/sounds/tone.mp3') : null;
const CleanSound = typeof Audio !== 'undefined' ? new Audio('/sounds/clean.mp3') : null;
export default function Cursors() {
const { selectedTool, connectionStatus, setTotalToastMessage } = useDeviceStore();
const [connectionMap, setConnectionMap] = useState({});
const navigation = useDisplayStore(state => state.navigation, shallow);
const connectionMapRef = useRef(null); // 添加ref用于缓存connectionMap
const [showOverlay, setShowOverlay] = useState(false); // 添加遮罩层状态
const [cleanStatus, setCleanStatus] = useState(null); // 添加清洁状态
const [toneStatus, setToneStatus] = useState(null); // 添加寻线音频状态
const mainModule = useDeviceStore.getState().mainUnitModules[0]?.id;
const seatUUID = useDeviceStore(state => state.seatUUID);
// 从localStorage加载清洁状态
const loadCleanState = () => {
if (!seatUUID) return;
// 使用setTimeout确保DOM元素已加载
setTimeout(() => {
const lccleanStates = JSON.parse(localStorage.getItem(`cleanState_${seatUUID}_lc`) || '{}');
const mpoCleanStates = JSON.parse(localStorage.getItem(`cleanState_${seatUUID}_mpo`) || '{}');
// 恢复LC端口状态
const lcElements = document.querySelectorAll('[lcclean]');
lcElements.forEach(el => {
const elementId = el.id || el.getAttribute('data-id');
if (elementId && lccleanStates[elementId] !== undefined) {
el.setAttribute('lcclean', lccleanStates[elementId].toString());
}
});
// 恢复MPO端口状态
const mpoElements = document.querySelectorAll('[mpoClean]');
mpoElements.forEach(el => {
const elementId = el.id || el.getAttribute('data-id');
if (elementId && mpoCleanStates[elementId] !== undefined) {
el.setAttribute('mpoClean', mpoCleanStates[elementId].toString());
}
});
}, 500);
};
// 保存清洁状态到localStorage
const saveCleanState = () => {
if (!seatUUID) return;
// 保存LC端口状态
const lcElements = document.querySelectorAll('[lcclean]');
const lccleanStates = {};
lcElements.forEach(el => {
const elementId = el.id || el.getAttribute('data-id');
if (elementId) {
lccleanStates[elementId] = el.getAttribute('lcclean') === 'true';
}
});
localStorage.setItem(`cleanState_${seatUUID}_lc`, JSON.stringify(lccleanStates));
// 保存MPO端口状态
const mpoElements = document.querySelectorAll('[mpoClean]');
const mpoCleanStates = {};
mpoElements.forEach(el => {
const elementId = el.id || el.getAttribute('data-id');
if (elementId) {
mpoCleanStates[elementId] = el.getAttribute('mpoClean') === 'true';
}
});
localStorage.setItem(`cleanState_${seatUUID}_mpo`, JSON.stringify(mpoCleanStates));
};
// 清理非本场seatUUID的清洁状态缓存
const clearOtherCleanState = () => {
if (!seatUUID) return;
// 清理所有非本场seatUUID的cleanState_前缀的缓存项
Object.keys(localStorage).forEach(key => {
if (key.startsWith('cleanState_') && !key.startsWith(`cleanState_${seatUUID}`)) {
localStorage.removeItem(key);
}
});
};
// 监听seatUUID变化
useEffect(() => {
if (seatUUID) {
loadCleanState();
// 加载完成后清理其他场次的缓存
setTimeout(() => {
clearOtherCleanState();
}, 1000);
}
}, [seatUUID]);
// 获取连接配置
const fetchConnectionMap = async () => {
try {
// 如果已经有缓存的connectionMap直接返回
if (connectionMapRef.current) {
setConnectionMap(connectionMapRef.current);
return;
}
const deviceStore = useDeviceStore.getState();
const faultScenario = deviceStore.faultScenarios || '';
const apiUrl = API_URLS.CONNECTION.MAP_WITH_SCENE(faultScenario);
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error('获取连接配置失败');
}
const data = await response.json();
connectionMapRef.current = data || {}; // 缓存connectionMap
setConnectionMap(data || {});
} catch (error) {
console.error('获取连接配置错误:', error);
setConnectionMap({});
}
};
// 处理鼠标按下事件用于lc-clean工具
const handleMouseDown = () => {
if (selectedTool?.id === 'lc-clean') {
document.body.style.cursor = `url('/custom-clean2.png'), auto`;
CleanSound?.play().catch(console.error);
} else if (selectedTool?.id === 'mpo-clean') {
document.body.style.cursor = `url('/custom-clean-mpo2.png'), auto`;
CleanSound?.play().catch(console.error);
}
};
// 处理鼠标释放事件用于lc-clean工具
const handleMouseUp = (event) => {
if (selectedTool?.id === 'lc-clean') {
document.body.style.cursor = `url('/custom-clean.png'), auto`;
const x = event.clientX;
const y = event.clientY;
const elements = document.elementsFromPoint(x, y);
let cleanableElement = null;
elements.forEach(element => {
if (element.hasAttribute('lcclean')) {
cleanableElement = element;
}
});
if (cleanableElement) {
let hasConnectedEndpoint = false;
elements.forEach(element => {
if (element.classList.contains('jtk-connected')) {
hasConnectedEndpoint = true;
setTotalToastMessage('该端口已连接,请断开连接进行清洁');
}
});
if (!hasConnectedEndpoint) {
cleanableElement.setAttribute('lcclean', 'true');
setCleanStatus('已清洁');
saveCleanState(); // 保存状态
} else {
setCleanStatus(null);
}
} else {
setCleanStatus(null);
}
} else if (selectedTool?.id === 'mpo-clean') {
document.body.style.cursor = `url('/custom-clean-mpo.png'), auto`;
const x = event.clientX;
const y = event.clientY;
const elements = document.elementsFromPoint(x, y);
let foundCleanElement = false;
elements.forEach(element => {
if (element.hasAttribute('mpoClean')) {
element.setAttribute('mpoClean', 'true');
setCleanStatus('已清洁');
foundCleanElement = true;
saveCleanState(); // 保存状态
}
});
if (!foundCleanElement) {
setCleanStatus(null);
}
}
};
// 这些函数已合并到 handleMouseDown 和 handleMouseUp 中
// 处理鼠标移动事件用于lc-clean工具
const handleCleanMouseMove = (event) => {
if (selectedTool?.id === 'lc-clean') {
const x = event.clientX;
const y = event.clientY;
const elements = document.elementsFromPoint(x, y);
let foundCleanElement = false;
let currentStatus = null;
elements.forEach(element => {
if (element.hasAttribute('lcclean')) {
foundCleanElement = true;
const isClean = element.getAttribute('lcclean') === 'true';
currentStatus = isClean ? '已清洁' : '未清洁';
}
});
if (foundCleanElement) {
setCleanStatus(currentStatus);
} else {
setCleanStatus(null);
}
}
};
// 处理鼠标移动事件用于MPO-clean工具
const handleMPOCleanMouseMove = (event) => {
if (selectedTool?.id === 'mpo-clean') {
const x = event.clientX;
const y = event.clientY;
const elements = document.elementsFromPoint(x, y);
let foundCleanElement = false;
let currentStatus = null;
elements.forEach(element => {
if (element.hasAttribute('mpoClean')) {
foundCleanElement = true;
const isClean = element.getAttribute('mpoClean') === 'true';
currentStatus = isClean ? '已清洁' : '未清洁';
}
});
if (foundCleanElement) {
setCleanStatus(currentStatus);
} else {
setCleanStatus(null);
}
}
};
// 根据selectedTool更新鼠标样式和事件监听
useEffect(() => {
// 移除现有的事件监听器
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleCleanMouseMove);
// 根据工具类型设置鼠标样式
if (!selectedTool) {
document.body.style.cursor = 'default';
fetchConnectionMap();
setShowOverlay(false);
setCleanStatus(null);
} else if (selectedTool.id === 'ILT200') {
document.body.style.cursor = `url('/cursor-ILT.png'), auto`;
setShowOverlay(true);
setCleanStatus(null);
// 只在tone视图下获取连接配置和添加事件监听
if (mainModule==='8000' && navigation.current.view === 'tone') {
document.addEventListener('mousemove', handleMouseMove);
}
} else if (selectedTool.id === 'lc-clean') {
document.body.style.cursor = `url('/custom-clean.png'), auto`;
setShowOverlay(true);
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleCleanMouseMove);
} else if (selectedTool.id === 'mpo-clean') {
document.body.style.cursor = `url('/custom-clean-mpo.png'), auto`;
setShowOverlay(true);
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMPOCleanMouseMove);
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleCleanMouseMove);
document.removeEventListener('mousemove', handleMPOCleanMouseMove);
};
}, [selectedTool, navigation.current.view]);
// 检查端口互通
const checkPortConnection = (elementId) => {
if (!connectionStatus?.mainPaths) return false;
// 获取最新的mainPathKey
const mainPathKey = Object.keys(connectionStatus.mainPaths)[0];
if (!mainPathKey) return false;
const mainEnd = connectionStatus.mainPaths[mainPathKey].end;
if (!mainEnd) return false;
// 使用最新的connectionMap进行判断
return Object.keys(connectionMap).some(key => {
const connection = connectionMap[key];
const isConnected = (connection.connectedTo === mainEnd && key === elementId) ||
(connection.connectedTo === elementId && key === mainEnd);
return isConnected;
});
};
// 监听faultScenario变化并重新获取connectionMap
const faultScenario = useDeviceStore(state => state.faultScenarios);
useEffect(() => {
connectionMapRef.current = null; // 清除缓存
fetchConnectionMap();
}, [faultScenario]); // 添加对faultScenario的依赖
// 处理鼠标移动事件
const handleMouseMove = (event) => {
const x = event.clientX;
const y = event.clientY;
const elements = document.elementsFromPoint(x, y);
elements.forEach(element => {
if (element.getAttribute('jstype') && element.id) {
const isConnected = checkPortConnection(element.id);
if (isConnected) {
ToneSound?.play().catch(console.error);
setToneStatus('🔊');
// 1200ms 后清除状态
setTimeout(() => {
setToneStatus(null);
}, 1200);
console.log(isConnected);
}
}
});
};
return (
<>
{showOverlay && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'transparent',
zIndex: 2000,
pointerEvents: 'auto',
}}
/>
)}
{(selectedTool?.id === 'lc-clean' || selectedTool?.id === 'mpo-clean') && cleanStatus && (
<StatusToast status={cleanStatus} />
)}
{selectedTool?.id === 'ILT200' && toneStatus && (
<StatusToast status={toneStatus} />
)}
</>
);
}

72
src/components/DisPlay.js Normal file
View File

@@ -0,0 +1,72 @@
import React, { useEffect } from 'react';
import HomePage from './dsxpage/HomePage';
import Project from './dsxpage/Project';
import Operators from './dsxpage/Operators';
import CableId from './dsxpage/CableId';
import Tools from './dsxpage/Tools';
import Result from './dsxpage/Result';
import TestConfig from './dsxpage/TestConfig';
import MenuList from './dsxpage/MenuList';
import Testing from './dsxpage/Testing';
import ResultInfo from './dsxpage/ResultInfo';
import CopperPerformance from './dsxpage/CopperPerformance';
import Toast from './lib/Toast';
import useDisplayStore from '@/store/displayStore';
const touchSound = typeof Audio !== 'undefined' ? new Audio('/sounds/key_click.wav') : null;
export default function DisPlay() {
const { navigation, navigateTo, toastMessage} = useDisplayStore();
const renderPage = () => {
const pageName = navigation?.current?.name || 'home';
switch (pageName) {
case 'home':
return <HomePage />;
case 'project':
return <Project />;
case 'operators':
return <Operators />;
case 'cableId':
return <CableId />;
case 'result':
return <Result />;
case 'tools':
return <Tools />;
case 'testConfig':
return <TestConfig />;
case 'menulist':
return <MenuList/>
case 'testing':
return <Testing/>
case 'resultinfo':
return <ResultInfo/>
case 'copperperformance':
return <CopperPerformance/>
// 可以添加更多页面
default:
return <HomePage />;
}
};
const handleClick = () => {
if (touchSound) {
touchSound.currentTime = 0;
touchSound.play();
}
};
return (
<div
className="w-[480px] h-[640px] bg-[#fff] flex flex-col overflow-hidden"
onClick={handleClick}
>
{toastMessage && <Toast />}
{renderPage()}
</div>
);
}

View File

@@ -0,0 +1,746 @@
import { useEffect, useRef } from 'react';
import useDeviceStore from '@/store/deviceStore';
export default function JsPlumbInit() {
const containerRef = useRef(null);
const jsPlumbInstanceRef = useRef(null);
const {
connectionItems,
mainUnitModules,
remoteUnitModules,
mainUnitAdapter,
remoteUnitAdapter,
addConnectionPath,
removeConnectionPath,
clearConnectionPaths,
faultScenarios,
WorldSkillScenarios
} = useDeviceStore();
useEffect(() => {
// 确保在客户端环境中运行
if (typeof window === 'undefined') return;
// 动态导入jsplumb
const initJsPlumb = async () => {
try {
const { jsPlumb } = await import('jsplumb');
if (!containerRef.current) return;
// 初始化jsPlumb实例
if (!jsPlumbInstanceRef.current) {
jsPlumbInstanceRef.current = jsPlumb.getInstance({
Container: containerRef.current,
// 连接线默认配置
Connector: ['Bezier', { curviness: 50 }],
// 端点默认配置
Endpoint: ['Dot', { radius: 10 }],
// 连接线样式
PaintStyle: {
stroke: '#0ff',
strokeWidth: 2
},
// 端点样式
EndpointStyle: {
fill: '#0ff',
radius: 15
},
// 鼠标悬停样式
HoverPaintStyle: {
stroke: '#00ff7f',
strokeWidth: 3
},
EndpointHoverStyle: {
fill: '#00ff7f'
}
});
// 添加连接建立事件处理
jsPlumbInstanceRef.current.bind('connection', (info) => {
const targetElement = document.getElementById(info.targetId);
if (targetElement && targetElement.getAttribute('jstype') === 'modelport-fiber') {
// 如果当前状态是 false说明是新建立的连接
if (targetElement.getAttribute('fibersource') === 'false') {
targetElement.setAttribute('fibersource', 'true');
}
}
});
// 添加连接断开事件处理
jsPlumbInstanceRef.current.bind('connectionDetached', (info) => {
const targetElement = document.getElementById(info.targetId);
if (targetElement && targetElement.getAttribute('jstype') === 'modelport-fiber') {
targetElement.setAttribute('fibersource', 'false');
}
});
// 添加连接拖动开始事件处理
jsPlumbInstanceRef.current.bind('connectionDrag', (info) => {
const targetElement = document.getElementById(info.targetId);
if (targetElement && targetElement.getAttribute('jstype') === 'modelport-fiber') {
// 在拖动开始时就将状态设置为 abnormal
targetElement.setAttribute('fibersource', 'abnormal');
}
});
// 添加beforeDrop事件处理
jsPlumbInstanceRef.current.bind('beforeDrop', (info) => {
// 获取源和目标元素
const sourceId = info.sourceId;
const targetId = info.targetId;
// 如果源和目标是同一个元素返回false阻止连接
if (sourceId === targetId) {
//console.log('不允许自连接');
return false;
}
// 获取源和目标元素的id和jstype属性
const sourceElement = document.querySelector(`[id="${sourceId}"]`);
const targetElement = document.querySelector(`[id="${targetId}"]`);
if (sourceElement && targetElement) {
const sourceType = sourceElement.getAttribute('jstype');
const targetType = targetElement.getAttribute('jstype');
// 检查是否是permanent端口
const isPermanentSource = sourceId.includes('permanent');
const isPermanentTarget = targetId.includes('permanent');
// permanent端口只能作为源不能作为目标
if (isPermanentTarget) {
//console.log('permanent端口不能作为连接目标');
return false;
}
// permanent端口只能连接到testport-copper
if (isPermanentSource && targetType !== 'modelport-copper' && targetType !== 'testport-copper') {
return false;
}
// connector类型只能作为目标
if (sourceType === 'connector') {
//console.log('连接器只能作为连接目标');
return false;
}
// cable类型不能连接同类
if (sourceType.startsWith('cable-') && targetType.startsWith('cable-')) {
//console.log('跳线不能连接跳线');
return false;
}
// 铜缆跳线只能连接铜缆端口
if (sourceType === 'cable-copper-left' && targetType !== 'modelport-copper' && targetType !== 'testport-copper') {
//console.log('铜缆跳线只能连接铜缆端口');
return false;
}
if (sourceType === 'cable-copper-right' && targetType !== 'modelport-copper' && targetType !== 'testport-copper') {
//console.log('铜缆跳线只能连接铜缆端口');
return false;
}
// CAM跳线只能连接Channel/PachCode端口
if (sourceType === 'testport-cam' && targetType !== 'modelport-copper') {
//console.log('铜缆跳线只能连接铜缆端口');
return false;
}
// ARM跳线只能连接M12 D端口
if (sourceType === 'testport-arm' && targetType !== 'modelport-m12-d') {
return false;
}
// M12X跳线只能连接M12X
if (sourceType === 'cable-copper-m12-x-left' && targetType !== 'modelport-m12-x' && targetType !== 'testport-m12-x') {
//console.log('M12 X-Code跳线只能连接M12 X-Code端口');
return false;
}
if (sourceType === 'cable-copper-m12-x-right' && targetType !== 'modelport-m12-x' && targetType !== 'testport-m12-x') {
//console.log('M12 X-Code跳线只能连接M12 X-Code端口');
return false;
}
// M12D跳线只能连接M12D
if (sourceType === 'cable-copper-m12-d-left' && targetType !== 'modelport-m12-d' && targetType !== 'testport-m12-d') {
//console.log('M12 D-Code跳线只能连接M12 D-Code端口');
return false;
}
if (sourceType === 'cable-copper-m12-d-right' && targetType !== 'modelport-m12-d' && targetType !== 'testport-m12-d') {
//console.log('M12 D-Code跳线只能连接M12 D-Code端口');
return false;
}
// 铜缆端口不能连接光纤测试端口
if (sourceType === 'modelport-copper' && targetType === 'testport-fiber') {
//console.log('铜缆端口不能连接光纤测试端口');
return false;
}
// 光纤跳线只能连接光纤端口或连接器
if (sourceType === 'cable-fiber-left' && targetType !== 'modelport-fiber' && targetType !== 'connector-left' && targetType !== 'connector-right' && targetType !== 'testport-fiber' && targetType !== 'mpo-fiberbox-lc') {
//console.log('光纤跳线只能连接光纤端口或连接器');
return false;
}
if (sourceType === 'cable-fiber-right' && targetType !== 'modelport-fiber' && targetType !== 'connector-left' && targetType !== 'connector-right' && targetType !== 'testport-fiber' && targetType !== 'mpo-fiberbox-lc') {
//console.log('光纤跳线只能连接光纤端口或连接器');
return false;
}
// MPO跳线只能连接MPO端口或连接器
if (sourceType === 'cable-fiber-mpo-left' && targetType !== 'testport-mpo' && targetType !== 'mpo-fiberbox-mpo') {
//console.log('光纤跳线只能连接光纤端口或连接器');
return false;
}
if (sourceType === 'cable-fiber-mpo-right' && targetType !== 'testport-mpo'&& targetType !== 'mpo-fiberbox-mpo') {
//console.log('光纤跳线只能连接光纤端口或连接器');
return false;
}
}
return true;
});
}
const instance = jsPlumbInstanceRef.current;
// 移除所有现有端点
instance.deleteEveryEndpoint();
// 获取所有需要添加连接点的元素
const connectorLeftElements = document.querySelectorAll('[jstype="connector-left"]');
const connectorRightElements = document.querySelectorAll('[jstype="connector-right"]');
const cableCopperLeftElements = document.querySelectorAll('[jstype="cable-copper-left"]');
const cableCopperRightElements = document.querySelectorAll('[jstype="cable-copper-right"]');
const cableM12XLeftElements = document.querySelectorAll('[jstype="cable-copper-m12-x-left"]');
const cableM12XRightElements = document.querySelectorAll('[jstype="cable-copper-m12-x-right"]');
const cableM12DLeftElements = document.querySelectorAll('[jstype="cable-copper-m12-d-left"]');
const cableM12DRightElements = document.querySelectorAll('[jstype="cable-copper-m12-d-right"]');
const cableFiberLeftElements = document.querySelectorAll('[jstype="cable-fiber-left"]');
const cableFiberRightElements = document.querySelectorAll('[jstype="cable-fiber-right"]');
const modelPortCopperElements = document.querySelectorAll('[jstype="modelport-copper"]');
const modelPortM12DElements = document.querySelectorAll('[jstype="modelport-m12-d"]');
const modelPortM12XElements = document.querySelectorAll('[jstype="modelport-m12-x"]');
const modelPortFiberElements = document.querySelectorAll('[jstype="modelport-fiber"]');
const testCopperPortElements = document.querySelectorAll('[jstype="testport-copper"]');
const testM12DPortElements = document.querySelectorAll('[jstype="testport-m12-d"]');
const testM12XPortElements = document.querySelectorAll('[jstype="testport-m12-x"]');
const testCamPortElements = document.querySelectorAll('[jstype="testport-cam"]');
const testArmPortElements = document.querySelectorAll('[jstype="testport-arm"]');
const testFiberPortElements = document.querySelectorAll('[jstype="testport-fiber"]');
const testTestMPOPortElements = document.querySelectorAll('[jstype="testport-mpo"]');
const testMPOPortLeftElements = document.querySelectorAll('[jstype="cable-fiber-mpo-left"]');
const testMPOPortRightElements = document.querySelectorAll('[jstype="cable-fiber-mpo-right"]');
const testMPOBOXPortElements = document.querySelectorAll('[jstype="mpo-fiberbox-mpo"]');
const testMPOLCBOXPortElements = document.querySelectorAll('[jstype="mpo-fiberbox-lc"]');
// 为connector类型元素添加端点只能作为目标
connectorLeftElements.forEach((element) => {
const id = element.getAttribute('id');
//console.log(`初始化连接器: ${id}`);
// 添加左侧端点
instance.addEndpoint(element, {
anchor: 'Left',
isSource: false,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
connectorRightElements.forEach((element) => {
const id = element.getAttribute('id');
//console.log(`初始化连接器: ${id}`);
// 添加右侧端点
instance.addEndpoint(element, {
anchor: 'Right',
isSource: false,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为铜缆跳线添加端点
cableCopperLeftElements.forEach((element) => {
const id = element.getAttribute('id');
//console.log(`初始化铜缆跳线: ${id}`);
// 添加左侧端点
instance.addEndpoint(element, {
anchor: 'Center',
isSource: true,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
cableCopperRightElements.forEach((element) => {
const id = element.getAttribute('id');
//console.log(`初始化铜缆跳线: ${id}`);
// 添加右侧端点
instance.addEndpoint(element, {
anchor: 'Center',
isSource: true,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为M12跳线添加端点
cableM12XLeftElements.forEach((element) => {
const id = element.getAttribute('id');
//console.log(`初始化铜缆跳线: ${id}`);
// 添加左侧端点
instance.addEndpoint(element, {
anchor: 'Center',
isSource: true,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
cableM12XRightElements.forEach((element) => {
const id = element.getAttribute('id');
//console.log(`初始化铜缆跳线: ${id}`);
// 添加右侧端点
instance.addEndpoint(element, {
anchor: 'Center',
isSource: true,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
cableM12DLeftElements.forEach((element) => {
const id = element.getAttribute('id');
//console.log(`初始化铜缆跳线: ${id}`);
// 添加左侧端点
instance.addEndpoint(element, {
anchor: 'Center',
isSource: true,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
cableM12DRightElements.forEach((element) => {
const id = element.getAttribute('id');
//console.log(`初始化铜缆跳线: ${id}`);
// 添加右侧端点
instance.addEndpoint(element, {
anchor: 'Center',
isSource: true,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为光纤跳线添加端点
cableFiberLeftElements.forEach((element) => {
const id = element.getAttribute('id');
//console.log(`初始化光纤跳线: ${id}`);
// 添加左侧端点
instance.addEndpoint(element, {
anchor: 'Center',
isSource: true,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
cableFiberRightElements.forEach((element) => {
const id = element.getAttribute('id');
//console.log(`初始化光纤跳线: ${id}`);
// 添加右侧端点
instance.addEndpoint(element, {
anchor: 'Center',
isSource: true,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为铜缆端口添加端点
modelPortCopperElements.forEach((element) => {
const id = element.getAttribute('id');
const isPermanent = id.includes('permanent');
//console.log(`初始化铜缆端口: ${id} (${isPermanent ? '永久链路' : '普通端口'})`);
instance.addEndpoint(element, {
anchor: 'Top',
isSource: isPermanent,
isTarget: !isPermanent,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为铜缆端口添加端点
modelPortM12DElements.forEach((element) => {
const id = element.getAttribute('id');
const isPermanent = id.includes('permanent');
//console.log(`初始化铜缆端口: ${id} (${isPermanent ? '永久链路' : '普通端口'})`);
instance.addEndpoint(element, {
anchor: 'Top',
isSource: isPermanent,
isTarget: !isPermanent,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为铜缆端口添加端点
modelPortM12XElements.forEach((element) => {
const id = element.getAttribute('id');
const isPermanent = id.includes('permanent');
//console.log(`初始化铜缆端口: ${id} (${isPermanent ? '永久链路' : '普通端口'})`);
instance.addEndpoint(element, {
anchor: 'Top',
isSource: isPermanent,
isTarget: !isPermanent,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为光纤端口添加端点
modelPortFiberElements.forEach((element) => {
const id = element.getAttribute('id');
// 初始化时设置 fibersource 属性为 false
element.setAttribute('fibersource', 'false');
instance.addEndpoint(element, {
anchor: 'Center',
isSource: false,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为测试铜端口添加端点
testCopperPortElements.forEach((element) => {
const id = element.getAttribute('id');
instance.addEndpoint(element, {
anchor: 'Center',
isSource: false,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#00000000', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为测试M12D端口添加端点
testM12DPortElements.forEach((element) => {
const id = element.getAttribute('id');
instance.addEndpoint(element, {
anchor: 'Center',
isSource: false,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#00000000', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为测试M12X端口添加端点
testM12XPortElements.forEach((element) => {
const id = element.getAttribute('id');
instance.addEndpoint(element, {
anchor: 'Center',
isSource: false,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#00000000', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为Cam铜端口添加端点
testCamPortElements.forEach((element) => {
const id = element.getAttribute('id');
instance.addEndpoint(element, {
anchor: 'Center',
isSource: true,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#00000000', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为测试ARM端口添加端点
testArmPortElements.forEach((element) => {
const id = element.getAttribute('id');
instance.addEndpoint(element, {
anchor: 'Center',
isSource: true,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#00000000', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为测试光端口添加端点
testFiberPortElements.forEach((element) => {
const id = element.getAttribute('id');
instance.addEndpoint(element, {
anchor: 'Center',
isSource: false,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#00000000', },
hoverPaintStyle: { fill: '#80BEBEBE' },
});
});
// 为MPO测试端口添加端点
testTestMPOPortElements.forEach((element) => {
const id = element.getAttribute('id');
instance.addEndpoint(element, {
anchor: 'Center',
isSource: false,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#00000000', },
hoverPaintStyle: { fill: '#80BEBEBE' },
});
});
// 为MPO 跳线添加端口
testMPOPortLeftElements.forEach((element) => {
const id = element.getAttribute('id');
//console.log(`初始化光纤跳线: ${id}`);
// 添加左侧端点
instance.addEndpoint(element, {
anchor: 'BottomLeft',
isSource: true,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
testMPOPortRightElements.forEach((element) => {
const id = element.getAttribute('id');
//console.log(`初始化光纤跳线: ${id}`);
// 添加右侧端点
instance.addEndpoint(element, {
anchor: 'TopRight',
isSource: true,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#0ff', },
hoverPaintStyle: { fill: '#00ff7f' }
});
});
// 为MPO BOX端口添加端点
testMPOBOXPortElements.forEach((element) => {
const id = element.getAttribute('id');
//console.log(`初始化光纤跳线: ${id}`);
// 添加顶部居中端点
instance.addEndpoint(element, {
anchor: 'Center',
isSource: false,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#00000000', },
hoverPaintStyle: { fill: '#80BEBEBE' },
});
});
testMPOLCBOXPortElements.forEach((element) => {
const id = element.getAttribute('id');
instance.addEndpoint(element, {
anchor: 'Center',
isSource: false,
isTarget: true,
maxConnections: 1,
connectorStyle: { stroke: '#0ff', strokeWidth: 2 },
connectorHoverStyle: { stroke: '#00ff7f', strokeWidth: 3 },
paintStyle: { fill: '#00000000', },
hoverPaintStyle: { fill: '#80BEBEBE' },
});
});
// 添加连接事件监听
instance.bind('connection', (info) => {
const sourceId = info.sourceId;
const targetId = info.targetId;
// 先移除源端点的所有历史连接
instance.getAllConnections().forEach(conn => {
if (conn.sourceId === sourceId) {
removeConnectionPath(conn.sourceId, conn.targetId);
}
});
// 添加新的连接路径
addConnectionPath({
source: sourceId,
target: targetId,
connection: info.connection
});
});
// 清理端口连接状态的函数
const cleanupPortStatus = (elementId, retryCount = 3) => {
const cleanupAttempt = (attemptCount) => {
const element = document.getElementById(elementId);
if (element && element.classList.contains('jtk-connected')) {
element.classList.remove('jtk-connected');
// 再次检查以确保状态被清理
if (element.classList.contains('jtk-connected') && attemptCount > 0) {
setTimeout(() => cleanupAttempt(attemptCount - 1), 50);
}
}
};
// 立即执行第一次清理
cleanupAttempt(retryCount);
// 延迟100ms后再次检查,以处理快速操作的情况
setTimeout(() => cleanupAttempt(retryCount), 100);
};
// 添加断开连接事件监听
instance.bind('connectionDetached', (info) => {
const sourceId = info.sourceId;
const targetId = info.targetId;
// 移除连接路径
removeConnectionPath(sourceId, targetId);
// 清理源端口和目标端口的状态
cleanupPortStatus(sourceId);
cleanupPortStatus(targetId);
});
// 添加连接移动事件监听
instance.bind('connectionMoved', (info) => {
// 移除原始连接
removeConnectionPath(info.originalSourceId, info.originalTargetId);
// 清理原始源端口和目标端口的状态
cleanupPortStatus(info.originalSourceId);
cleanupPortStatus(info.originalTargetId);
// 添加新连接
addConnectionPath({
source: info.newSourceId,
target: info.newTargetId,
connection: info.connection
});
});
// 重绘所有连接
instance.repaintEverything();
} catch (error) {
console.error('Failed to initialize jsPlumb:', error);
}
};
initJsPlumb();
// 清理函数
return () => {
if (jsPlumbInstanceRef.current) {
// 清空连接路径
clearConnectionPaths();
jsPlumbInstanceRef.current.reset();
jsPlumbInstanceRef.current = null;
}
};
}, [connectionItems, mainUnitModules, remoteUnitModules, mainUnitAdapter, remoteUnitAdapter, addConnectionPath, removeConnectionPath, clearConnectionPaths, faultScenarios, WorldSkillScenarios]);
return (
<div
ref={containerRef}
className="fixed inset-0 z-[1000] pointer-events-none"
/>
);
}

View File

@@ -0,0 +1,247 @@
import React, { useState } from 'react';
import useDeviceStore from '@/store/deviceStore';
const ResultTable = () => {
const [records, setRecords] = useState([
{
id: 1,
linkName: '',
testResult: 'pass',
faults: [{
id: 1,
type: '',
location: '',
reason: ''
}]
}
]);
// 添加新记录
const addRecord = () => {
const newRecord = {
id: Date.now(),
linkName: '',
testResult: 'pass',
faults: [{
id: Date.now(),
type: '',
location: '',
reason: ''
}]
};
setRecords([...records, newRecord]);
};
// 删除记录
const deleteRecord = (recordId) => {
setRecords(records.filter(record => record.id !== recordId));
};
// 更新记录基本信息
const updateRecord = (recordId, field, value) => {
setRecords(records.map(record =>
record.id === recordId
? { ...record, [field]: value }
: record
));
};
// 添加故障类型
const addFault = (recordId) => {
setRecords(records.map(record =>
record.id === recordId
? {
...record,
faults: [...record.faults, {
id: Date.now(),
type: '',
location: '',
reason: ''
}]
}
: record
));
};
// 删除故障类型
const deleteFault = (recordId, faultId) => {
setRecords(records.map(record =>
record.id === recordId
? {
...record,
faults: record.faults.filter(fault => fault.id !== faultId)
}
: record
));
};
// 更新故障信息
const updateFault = (recordId, faultId, field, value) => {
setRecords(records.map(record =>
record.id === recordId
? {
...record,
faults: record.faults.map(fault =>
fault.id === faultId
? { ...fault, [field]: value }
: fault
)
}
: record
));
};
// 提交表单
const handleSubmit = () => {
// 验证表单
const isValid = records.every(record => {
if (!record.linkName.trim()) return false;
return record.faults.every(fault => {
if (record.testResult === 'fail') {
return fault.type.trim() && fault.location.trim() && fault.reason.trim();
}
return true;
});
});
if (!isValid) {
alert('请填写完整的记录');
return;
}
// 直接使用当前的 records 更新 store
useDeviceStore.getState().updateReports(records);
alert('记录提交成功!');
};
return (
<div className="bg-[#1E293B] rounded-lg p-6 text-white">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-bold text-[#0ff]">故障检测分析表</h2>
<button
onClick={addRecord}
className="bg-[#0ff] text-black px-4 py-2 rounded hover:bg-[#0ff]/80 transition-colors"
>
添加记录
</button>
</div>
<div className="space-y-6 max-h-[70vh] overflow-y-auto custom-scrollbar">
{records.map((record, recordIndex) => (
<div key={record.id} className="bg-[#0F172A] rounded-lg p-4 border border-[#0ff]/20">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-[#0ff]">记录 {recordIndex + 1}</h3>
{records.length > 1 && (
<button
onClick={() => deleteRecord(record.id)}
className="text-red-400 hover:text-red-300 transition-colors"
>
删除记录
</button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium mb-2">链路名称 *</label>
<input
type="text"
value={record.linkName}
onChange={(e) => updateRecord(record.id, 'linkName', e.target.value)}
className="w-full bg-[#1E293B] border border-[#0ff]/30 rounded px-3 py-2 text-white focus:border-[#0ff] focus:outline-none"
placeholder="请输入链路名称"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">检测结果 *</label>
<select
value={record.testResult}
onChange={(e) => updateRecord(record.id, 'testResult', e.target.value)}
className="w-full bg-[#1E293B] border border-[#0ff]/30 rounded px-3 py-2 text-white focus:border-[#0ff] focus:outline-none"
>
<option value="pass">通过</option>
<option value="fail">失败</option>
</select>
</div>
</div>
{record.testResult === 'fail' && (
<div className="border-t border-[#0ff]/20 pt-4">
<div className="flex justify-between items-center mb-3">
<h4 className="text-md font-medium text-[#0ff]">故障信息</h4>
<button
onClick={() => addFault(record.id)}
className="bg-[#0ff]/20 text-[#0ff] px-3 py-1 rounded text-sm hover:bg-[#0ff]/30 transition-colors"
>
添加故障
</button>
</div>
{record.faults.map((fault, faultIndex) => (
<div key={fault.id} className="bg-[#1E293B]/50 rounded p-3 mb-3 border border-[#0ff]/10">
<div className="flex justify-between items-center mb-3">
<span className="text-sm text-gray-300">故障 {faultIndex + 1}</span>
{record.faults.length > 1 && (
<button
onClick={() => deleteFault(record.id, fault.id)}
className="text-red-400 hover:text-red-300 text-sm transition-colors"
>
删除
</button>
)}
</div>
<div className="grid grid-cols-1 gap-3">
<div>
<label className="block text-sm font-medium mb-1">故障类型 *</label>
<input
type="text"
value={fault.type}
onChange={(e) => updateFault(record.id, fault.id, 'type', e.target.value)}
className="w-full bg-[#0F172A] border border-[#0ff]/20 rounded px-3 py-2 text-white text-sm focus:border-[#0ff] focus:outline-none"
placeholder="请输入故障类型"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">故障位置 *</label>
<input
type="text"
value={fault.location}
onChange={(e) => updateFault(record.id, fault.id, 'location', e.target.value)}
className="w-full bg-[#0F172A] border border-[#0ff]/20 rounded px-3 py-2 text-white text-sm focus:border-[#0ff] focus:outline-none"
placeholder="请输入故障位置"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">故障原因 *</label>
<textarea
value={fault.reason}
onChange={(e) => updateFault(record.id, fault.id, 'reason', e.target.value)}
className="w-full bg-[#0F172A] border border-[#0ff]/20 rounded px-3 py-2 text-white text-sm focus:border-[#0ff] focus:outline-none resize-none"
placeholder="请输入故障原因"
rows={2}
/>
</div>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
<div className="mt-6 flex justify-center">
<button
onClick={handleSubmit}
className="bg-[#0ff] text-black px-8 py-3 rounded-lg font-semibold hover:bg-[#0ff]/80 transition-colors"
>
保存记录
</button>
</div>
</div>
);
};
export default ResultTable;

View File

@@ -0,0 +1,101 @@
import { useState, useEffect, useRef } from 'react';
import useDeviceStore from '@/store/deviceStore';
import useDisplayStore from '@/store/displayStore';
export default function SourceCheck() {
const { updataRef, getCurrentTestConfig } = useDisplayStore();
const { mainUnitModules, remoteUnitModules, connectionStatus } = useDeviceStore();
useEffect(() => {
const mainModule = mainUnitModules[0]?.id;
if (!mainModule) return;
const currentConfig = getCurrentTestConfig();
const cableType = currentConfig.params.cableType;
const isMultiMode = cableType.includes('OM');
let moduleType, mainSelector, remoteSelector;
// 根据主机模块类型决定检测逻辑
if (mainModule === 'cfp') {
const remoteCFP = remoteUnitModules[0]?.id;
if (!remoteCFP || remoteCFP !== 'cfp') return;
moduleType = 'cfp';
mainSelector = `#main-cfp-${isMultiMode ? 'mm' : 'sm'}-out`;
remoteSelector = `#remote-cfp-${isMultiMode ? 'mm' : 'sm'}-out`;
} else if (mainModule === 'ofp') {
moduleType = 'ofp';
mainSelector = `#main-ofp-${isMultiMode ? 'mm' : 'sm'}-out`;
remoteSelector = null; // OFP 模式不需要检查远端
} else {
return; // 其他模块类型不需要检测
}
// 检查元素是否存在
const checkElements = () => {
const mainElement = document.querySelector(mainSelector);
const remoteElement = remoteSelector ? document.querySelector(remoteSelector) : null;
return { mainElement, remoteElement };
};
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'fibersource') {
const oldValue = mutation.oldValue;
const newValue = mutation.target.getAttribute('fibersource');
// 只在 fibersource 从 true 变为其他状态时触发
if (oldValue === 'true' && newValue !== 'true') {
updataRef(moduleType, { connectStatus: false });
}
}
});
});
// 配置观察选项
const config = {
attributes: true,
attributeFilter: ['fibersource'],
attributeOldValue: true // 记录属性的旧值
};
// 开始观察元素
const startObserving = () => {
const { mainElement, remoteElement } = checkElements();
if (mainElement && (moduleType === 'ofp' || remoteElement)) {
observer.observe(mainElement, config);
if (moduleType === 'cfp' && remoteElement) {
observer.observe(remoteElement, config);
}
// 立即检查初始连接状态
const isMainConnected = mainElement.getAttribute('fibersource') === 'true';
const isRemoteConnected = moduleType === 'cfp' ?
remoteElement.getAttribute('fibersource') === 'true' :
true;
if (!isMainConnected || !isRemoteConnected) {
updataRef(moduleType, { connectStatus: false });
}
} else {
setTimeout(startObserving, 1000);
}
};
// 根据对应模块的状态决定是否开始观察
const { ref } = useDisplayStore.getState();
if ((moduleType === 'cfp' && ref?.cfp?.status) ||
(moduleType === 'ofp' && ref?.ofp?.status)) {
startObserving();
}
// 清理函数
return () => {
observer.disconnect();
};
}, [mainUnitModules, remoteUnitModules, updataRef, getCurrentTestConfig, connectionStatus]);
return null;
}

View File

@@ -0,0 +1,274 @@
import React, { useState, useEffect } from 'react';
import StatusBar from '../lib/StatusBar';
import TitleBar from '../lib/TitleBar';
import Keyboard from '../lib/Keyboard';
import useDisplayStore from '@/store/displayStore';
export default function CableId() {
const [showKeyboard, setShowKeyboard] = useState(true);
const [cursorPosition, setCursorPosition] = useState(0);
const [inputValue, setInputValue] = useState('');
const [inputValue2, setInputValue2] = useState('');
const [activeInput, setActiveInput] = useState(1); // 1 表示第一个输入框2 表示第二个输入框
const {
getCurrentProject,
getCurrentCableId,
getCurrentCableId2,
updateProject,
goBack
} = useDisplayStore();
const currentProject = getCurrentProject();
// 获取当前选中的电缆ID
const currentCableId = getCurrentCableId().name;
// 获取当前选中的电缆ID2
const currentCableId2 = getCurrentCableId2().name;
const { view } = useDisplayStore.getState().navigation.current;
// 只在cfp视图下初始化第二个输入框
useEffect(() => {
setInputValue(currentCableId);
if (view === 'cfp') {
setInputValue2(currentCableId2);
}
}, [currentCableId, currentCableId2, view]);
// 计算下一个序号的ID
const getNextId = (currentId) => {
if (!currentId) return '';
// 检查是否以数字结尾
const numMatch = currentId.match(/^(.*?)(\d+)$/);
if (numMatch) {
const [, prefix, num] = numMatch;
const nextNum = String(Number(num) + 1).padStart(num.length, '0');
return prefix + nextNum;
}
// 检查是否以字母结尾
const letterMatch = currentId.match(/^(.*?)([a-zA-Z]+)$/);
if (letterMatch) {
const [, prefix, letters] = letterMatch;
// 将字母转换为数组以便处理
const letterArray = letters.split('');
let carry = true;
// 从右向左处理每个字母
for (let i = letterArray.length - 1; i >= 0 && carry; i--) {
if (letterArray[i] === 'z') {
letterArray[i] = 'a';
carry = true;
} else if (letterArray[i] === 'Z') {
letterArray[i] = 'A';
carry = true;
} else {
letterArray[i] = String.fromCharCode(letterArray[i].charCodeAt(0) + 1);
carry = false;
}
}
// 如果还有进位,说明需要在前面添加一个字母
if (carry) {
if (letters[0] >= 'a' && letters[0] <= 'z') {
letterArray.unshift('a');
} else {
letterArray.unshift('A');
}
}
return prefix + letterArray.join('');
}
// 如果既不是数字也不是字母结尾,直接返回原值
return currentId;
};
// 只在cfp视图下自动更新第二个输入框
useEffect(() => {
if (view === 'cfp' && inputValue) {
setInputValue2(getNextId(inputValue));
}
}, [inputValue, view]);
const handleComplete = () => {
if (view === 'main' && !inputValue.trim()) return;
if (view === 'cfp' && (!inputValue.trim() || !inputValue2.trim())) return;
const currentIndex = useDisplayStore.getState().selectedIndexes.projectIndex;
const currentCableIds = currentProject?.cableIds || [];
let cableIds;
if (view === 'main') {
// 在main视图下只更新选中的ID保留其他ID
const selectedId = getCurrentCableId().id;
cableIds = currentCableIds.map(cable =>
cable.id === selectedId ? { ...cable, name: inputValue.trim() } : cable
);
} else {
cableIds = [
{ id: '1', name: inputValue.trim() },
{ id: '2', name: inputValue2.trim() }
];
}
updateProject(currentIndex, { cableIds });
goBack();
};
const renderContent = () => {
switch (view) {
case 'main':
return (
<div className="flex-1 bg-[#303040] p-4 flex flex-col">
<div
className="relative mb-4 cursor-pointer"
onClick={() => setShowKeyboard(true)}
>
<input
type="text"
className="w-full h-[50px] bg-[#ffffe1] rounded-sm p-4 text-black"
value={inputValue}
placeholder="请输入线缆ID"
onChange={(e) => {
setInputValue(e.target.value);
setCursorPosition(e.target.selectionStart);
}}
onClick={(e) => {
setShowKeyboard(true);
setCursorPosition(e.target.selectionStart);
}}
onFocus={(e) => {
// 保存光标位置
const cursorPosition = e.target.selectionStart;
e.target.setSelectionRange(cursorPosition, cursorPosition);
}}
/>
</div>
{showKeyboard && (
<Keyboard
value={inputValue}
cursorPosition={cursorPosition}
onChange={(newValue, newPosition) => {
setInputValue(newValue);
setCursorPosition(newPosition);
}}
onComplete={() => {
setShowKeyboard(false);
}}
/>
)}
</div>
);
case 'cfp':
return (
<div className="flex-1 bg-[#303040] p-4 flex flex-col">
<div className="mb-8">
<div className="mb-1 text-white text-sm">输出光纤ID1</div>
<div className="relative cursor-pointer">
<input
type="text"
className="w-full h-[50px] bg-[#ffffe1] rounded-sm px-4 text-black overflow-x-auto whitespace-nowrap"
value={inputValue}
placeholder="请输入线缆ID1"
onChange={(e) => {
setInputValue(e.target.value);
setCursorPosition(e.target.selectionStart);
}}
onClick={(e) => {
setActiveInput(1);
setShowKeyboard(true);
setCursorPosition(e.target.selectionStart);
}}
onFocus={(e) => {
const cursorPosition = e.target.selectionStart;
e.target.setSelectionRange(cursorPosition, cursorPosition);
}}
/>
</div>
</div>
<div>
<div className="mb-1 text-white text-sm">输入光纤ID2</div>
<div className="relative cursor-pointer">
<input
type="text"
className="w-full h-[50px] bg-[#ffffe1] rounded-sm px-4 text-black overflow-x-auto whitespace-nowrap"
value={inputValue2}
placeholder="请输入线缆ID2"
onChange={(e) => {
setInputValue2(e.target.value);
setCursorPosition(e.target.selectionStart);
}}
onClick={(e) => {
setActiveInput(2);
setShowKeyboard(true);
setCursorPosition(e.target.selectionStart);
}}
onFocus={(e) => {
const cursorPosition = e.target.selectionStart;
e.target.setSelectionRange(cursorPosition, cursorPosition);
}}
/>
</div>
</div>
{showKeyboard && (
<Keyboard
value={activeInput === 1 ? inputValue : inputValue2}
cursorPosition={cursorPosition}
onChange={(newValue, newPosition) => {
if (activeInput === 1) {
setInputValue(newValue);
} else {
setInputValue2(newValue);
}
setCursorPosition(newPosition);
}}
onComplete={() => {
setShowKeyboard(false);
}}
/>
)}
</div>
);
default:
return null;
}
};
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<StatusBar />
<TitleBar
title="更改ID"
backTo={useDisplayStore.getState().navigation.previous?.name || 'home'}
view={useDisplayStore.getState().navigation.previous?.view || 'main'}
/>
{renderContent()}
{view === 'main' && (
<div className="h-[60px] bg-[#303030] flex items-center justify-end px-8">
{!showKeyboard && (<button
onClick={handleComplete}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
>
完成
</button>)}
</div>
)}
{view === 'cfp' && (
<div className="h-[60px] bg-[#303030] flex items-center justify-end px-8">
{!showKeyboard && (<button
onClick={handleComplete}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
>
完成
</button>)}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,349 @@
import React, { useState, useEffect } from 'react';
import StatusBar from '../lib/StatusBar';
import ResultTitleBar from '../lib/ResultTitleBar';
import useDisplayStore from '@/store/displayStore';
import FrequencyChart from '../lib/FrequencyChart';
import HDTDChart from '../lib/HDTDChart';
export default function CopperPerformance( ) {
const { navigation, navigateTo,goBack } = useDisplayStore();
const { view } = navigation.current;
const curtitle = navigation.current.params.curtitle;
const testResult = navigation.current.params.testResult;
const limitValue = testResult.testconfig.params.limitValue;
const [limitdata, setLimitdata] = useState(null);
const wireOrder = testResult?.testconfig?.params?.wireOrder;
useEffect(() => {
// 异步加载限制数据
const loadLimitData = async () => {
if (limitValue) {
try {
// 从limitValue中提取基础名称(移除+PoE和+ALL后缀)
const baseName = limitValue.split(' (+')[0];
// 使用基础名称加载带有(+ALL)后缀的文件
const data = await import(`@/store/COPPER/${baseName} (+ALL).json`);
setLimitdata(data);
} catch (error) {
console.error('Error loading limit data:', error);
}
}
};
loadLimitData();
}, [limitValue]);
const [poeTab, setPoeTab] = useState('回路');
const renderResultTitleBar = () => {
const handleBack = () => {
goBack();
};
let title = '';
switch (view) {
case 'LENGTH':
title = '长度';
break;
case 'OHM':
title = '电阻';
break;
case 'PoE':
title = 'PoE';
break;
case 'DRAW':
title = '图表';
break;
};
return <ResultTitleBar title={curtitle} onBack={handleBack} />;
};
const renderContent = () => {
switch (view) {
case 'LENGTH':
return (
<div className="w-full h-[490px] bg-[#6b6d6b]">
<div className="w-full h-full bg-white p-4 rounded-md">
<div className="w-full">
<div className="grid grid-cols-4 text-center border-b border-gray-300 pb-2">
<div className="flex flex-col items-center">
<span className="text-blue-600 text-lg"></span>
</div>
<div className="flex flex-col items-center">
<span className="text-blue-600 text-lg">传输延迟</span>
<span className="text-gray-500">(ns)</span>
</div>
<div className="flex flex-col items-center">
<span className="text-blue-600 text-lg">延迟时差</span>
<span className="text-gray-500">(ns)</span>
</div>
<div className="flex flex-col items-center">
<span className="text-blue-600 text-lg">长度</span>
<span className="text-gray-500">(m)</span>
</div>
</div>
<div className="mt-4 space-y-4">
{[
{ pair: '1,2', color: 'text-orange-500', delay: testResult?.resultdata?.performance?.DELAY?.PAIR12, delaySkew: testResult?.resultdata?.performance?.DELAYSKEW?.PAIR12, length: testResult?.resultdata?.performance?.LENGTH?.PAIR12 },
{ pair: '3,6', color: 'text-green-500', delay: testResult?.resultdata?.performance?.DELAY?.PAIR36, delaySkew: testResult?.resultdata?.performance?.DELAYSKEW?.PAIR36, length: testResult?.resultdata?.performance?.LENGTH?.PAIR36 },
...(wireOrder !== 'Ethernet Two-Pair' && wireOrder !== 'M12-D Two-Pair' ? [
{ pair: '4,5', color: 'text-blue-500', delay: testResult?.resultdata?.performance?.DELAY?.PAIR45, delaySkew: testResult?.resultdata?.performance?.DELAYSKEW?.PAIR45, length: testResult?.resultdata?.performance?.LENGTH?.PAIR45 },
{ pair: '7,8', color: 'text-yellow-500', delay: testResult?.resultdata?.performance?.DELAY?.PAIR78, delaySkew: testResult?.resultdata?.performance?.DELAYSKEW?.PAIR78, length: testResult?.resultdata?.performance?.LENGTH?.PAIR78 }
] : [])
].map(({ pair, color, delay, delaySkew, length }) => (
<div key={pair} className={`grid grid-cols-4 text-center border-gray-300 pt-2 ${color}`}>
<div className="font-bold">{pair}</div>
<div className={limitdata && delay > limitdata.DELAY ? 'bg-red-100 rounded-md' : ''}>{delay}</div>
<div className={limitdata && delaySkew > limitdata.DELAYSKEW ? 'bg-red-100 rounded-md' : ''}>{delaySkew}</div>
<div className={`text-center ${(limitdata && length > limitdata.LENGTH) && !(limitValue?.includes('ISO') || limitValue?.includes('Profinet')) ? 'bg-red-200 rounded-md' : ''}`}>{length}</div>
</div>
))}
{limitdata && (
<div className="grid grid-cols-4 text-center border-t border-gray-300 pt-2">
<div className="font-bold">极限</div>
<div>{limitdata.DELAY}</div>
<div>{limitdata.DELAYSKEW}</div>
<div>{limitdata.LENGTH}</div>
</div>
)}
</div>
</div>
</div>
</div>
);
case 'OHM':
return (
<div className="w-full h-[490px] bg-[#6b6d6b]">
<div className="w-full h-full bg-white p-0 rounded-md">
<div className="w-full">
{/* 选项卡 */}
<div className="flex mb-4">
{['回路'].map((tab) => (
<button
key={tab}
onClick={() => setPoeTab(tab)}
className={`flex-1 py-2 text-center text-xl font-bold ${poeTab === tab ? 'bg-gradient-to-b from-[#b0b0b0] via-[#e0e4e0] to-[#fff] text-black' : 'bg-[#303030] text-[#fffe92]'}`}
>
{tab}
</button>
))}
</div>
{/* 数据展示区域 */}
{poeTab === '回路' && (
<div className="mt-4 space-y-4">
<div className="grid grid-cols-2 text-center border-b border-gray-300 pb-2">
<div className="font-bold"> </div>
<div >
<span className="text-gray-500 text-lg">(Ω)</span>
</div>
</div>
{[
{ pair: '1,2', color: 'text-orange-500', value: testResult?.resultdata?.performance?.OHM?.LOOP?.PAIR12 },
{ pair: '3,6', color: 'text-green-500', value: testResult?.resultdata?.performance?.OHM?.LOOP?.PAIR36 },
...(wireOrder !== 'Ethernet Two-Pair' && wireOrder !== 'M12-D Two-Pair' ? [
{ pair: '4,5', color: 'text-blue-500', value: testResult?.resultdata?.performance?.OHM?.LOOP?.PAIR45 },
{ pair: '7,8', color: 'text-yellow-500', value: testResult?.resultdata?.performance?.OHM?.LOOP?.PAIR78 }
] : [])
].map(({ pair, color, value }) => (
<div key={pair} className="grid grid-cols-2 text-center py-2">
<div className={`font-bold ${color}`}>{pair}</div>
<div>{value || 'N/A'}</div>
</div>
))}
<div className="grid grid-cols-2 text-center border-t border-gray-300 pt-2">
<div className="font-bold">极限</div>
{limitdata && (
<div>{limitdata.LOOP}</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
);
case 'PoE':
return (
<div className="w-full h-[490px] bg-[#6b6d6b] ">
<div className="w-full h-full bg-white p-0 rounded-md">
<div className="w-full">
{/* 选项卡 */}
<div className="flex mb-4">
{['回路', '线对UBL', 'P2P UBL'].map((tab) => (
<button
key={tab}
onClick={() => setPoeTab(tab)}
className={`flex-1 py-1 text-center text-lg font-bold ${poeTab === tab ? 'bg-gradient-to-b from-[#b0b0b0] via-[#e0e4e0] to-[#fff] text-black' : 'bg-[#303030] text-[#fffe92]'}`}
>
{tab}
</button>
))}
</div>
{/* 数据展示区域 */}
{poeTab === '回路' && (
<div className="mt-4 space-y-4">
<div className="grid grid-cols-2 text-center border-b border-gray-300 pb-2">
<div className="font-bold"> </div>
<div >
<span className="text-gray-500 text-lg">(Ω)</span>
</div>
</div>
{[
{ pair: '1,2', color: 'text-orange-500', value: testResult?.resultdata?.performance?.OHM?.LOOP?.PAIR12 },
{ pair: '3,6', color: 'text-green-500', value: testResult?.resultdata?.performance?.OHM?.LOOP?.PAIR36 },
...(wireOrder !== 'Ethernet Two-Pair' && wireOrder !== 'M12-D Two-Pair' ? [
{ pair: '4,5', color: 'text-blue-500', value: testResult?.resultdata?.performance?.OHM?.LOOP?.PAIR45 },
{ pair: '7,8', color: 'text-yellow-500', value: testResult?.resultdata?.performance?.OHM?.LOOP?.PAIR78 }
] : [])
].map(({ pair, color, value }) => (
<div key={pair} className="grid grid-cols-2 text-center py-2">
<div className={`font-bold ${color}`}>{pair}</div>
<div className={`${value > limitdata?.LOOP ? 'bg-red-100' : ''}`}>
{value || 'N/A'}
</div>
</div>
))}
<div className="grid grid-cols-2 text-center border-t border-gray-300 pt-2">
<div className="font-bold">极限</div>
{limitdata && (
<div>{limitdata.LOOP}</div>
)}
</div>
</div>
)}
{poeTab === '线对UBL' && (
<div className="mt-4 space-y-4">
<div className="grid grid-cols-3 text-center border-b border-gray-300 pb-2">
<div className="font-bold"> </div>
<div className="flex flex-col items-center">
<span className="text-gray-500 text-lg">(Ω)</span>
</div>
<div className="flex flex-col items-center">
<span className="text-gray-500 text-lg">极限(Ω)</span>
</div>
</div>
{[
{ pair: '1,2', color: 'text-orange-500', value: testResult?.resultdata?.performance?.OHM?.PAIRUBL?.PAIR12 },
{ pair: '3,6', color: 'text-green-500', value: testResult?.resultdata?.performance?.OHM?.PAIRUBL?.PAIR36 },
...(wireOrder !== 'Ethernet Two-Pair' && wireOrder !== 'M12-D Two-Pair' ? [
{ pair: '4,5', color: 'text-blue-500', value: testResult?.resultdata?.performance?.OHM?.PAIRUBL?.PAIR45 },
{ pair: '7,8', color: 'text-yellow-500', value: testResult?.resultdata?.performance?.OHM?.PAIRUBL?.PAIR78 }
] : [])
].map(({ pair, color, value }) => (
<div key={pair} className="grid grid-cols-3 text-center py-2">
<div className={`font-bold ${color}`}>{pair}</div>
<div className={`${value > limitdata?.PAIRUBL ? 'bg-red-100' : ''}`}>
{value || 'N/A'}
</div>
{limitdata && (
<div>{limitdata.PAIRUBL}</div>
)}
</div>
))}
</div>
)}
{poeTab === 'P2P UBL' && (
<div className="mt-4 space-y-4">
<div className="grid grid-cols-3 text-center border-b border-gray-300 pb-2">
<div className="font-bold"> </div>
<div className="flex flex-col items-center">
<span className="text-gray-500 text-lg">(Ω)</span>
</div>
<div className="flex flex-col items-center">
<span className="text-gray-500 text-lg">极限(Ω)</span>
</div>
</div>
{[
{ pair: '12-36', color: 'text-orange-500', value: testResult?.resultdata?.performance?.OHM?.P2PUBL?.PAIR1236 },
...(wireOrder !== 'Ethernet Two-Pair' && wireOrder !== 'M12-D Two-Pair' ? [
{ pair: '12-45', color: 'text-green-500', value: testResult?.resultdata?.performance?.OHM?.P2PUBL?.PAIR1245 },
{ pair: '12-78', color: 'text-blue-500', value: testResult?.resultdata?.performance?.OHM?.P2PUBL?.PAIR1278 },
{ pair: '36-45', color: 'text-yellow-500', value: testResult?.resultdata?.performance?.OHM?.P2PUBL?.PAIR3645 },
{ pair: '36-78', color: 'text-purple-500', value: testResult?.resultdata?.performance?.OHM?.P2PUBL?.PAIR3678 },
{ pair: '45-78', color: 'text-pink-500', value: testResult?.resultdata?.performance?.OHM?.P2PUBL?.PAIR4578 }
] : [])
].map(({ pair, color, value }) => (
<div key={pair} className="grid grid-cols-3 text-center py-2">
<div className={`font-bold ${color}`}>{pair}</div>
<div className={`${value > limitdata?.P2PUBL ? 'bg-red-100' : ''}`}>
{value || 'N/A'}
</div>
{limitdata && (
<div>{limitdata.P2PUBL}</div>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
);
case 'DRAW':
return (
<div className="w-full h-[full] bg-[#6b6d6b]">
<div className="w-full h-full bg-white p-4 rounded-md">
{limitdata && (
<FrequencyChart
curtitle={curtitle}
limitValue={limitValue}
limitdata={limitdata}
wireOrder={wireOrder}
data={testResult?.resultdata?.performance?.data}
/>
)}
</div>
</div>
);
case 'HDTD':
return (
<div className="w-full h-[full] bg-[#6b6d6b]">
<div className="w-full h-full bg-white p-4 rounded-md">
{limitdata && (
<HDTDChart
curtitle={curtitle}
HDTD={testResult?.resultdata?.HDTD}
/>
)}
</div>
</div>
);
default:
return null;
}
};
const renderFooter = () => {
return (
<div className="h-[60px] bg-[#303030] flex items-center justify-end px-8">
</div>
);
}
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<StatusBar />
{renderResultTitleBar()}
{renderContent()}
{renderFooter()}
</div>
);
}

View File

@@ -0,0 +1,474 @@
import React, { useEffect } from 'react';
import Image from 'next/image';
import StatusBar from '../lib/StatusBar';
import TitleBar from '../lib/TitleBar';
import useDisplayStore from '@/store/displayStore';
import useDeviceStore from '@/store/deviceStore';
export default function HomePage() {
const {
getCurrentProject,
getCurrentTestConfig,
getCurrentOperator,
getCurrentCableId,
getCurrentCableId2,
navigateTo,
hasPlayedSound,
setToastMessage
} = useDisplayStore();
const { ref } = useDisplayStore.getState();
useEffect(() => {
if (hasPlayedSound) {
useDisplayStore.setState({ hasPlayedSound: false });
}
}, [hasPlayedSound]);
const { connectionStatus, mainUnitAdapter, remoteUnitAdapter,mainUnitModules,setTotalToastMessage } = useDeviceStore();
// 获取当前项目数据
const currentProject = getCurrentProject();
// 获取当前选中的测试配置
const currentConfig = getCurrentTestConfig();
// 获取当前选中的操作员
const currentOperator = getCurrentOperator();
// 获取当前选中的电缆ID
const currentCableId = getCurrentCableId();
// 获取当前选中的电缆ID2
const currentCableId2 = getCurrentCableId2();
const passCount = currentProject?.testResults.filter(result => {
const copperStatus = result.CopperResultStatus;
const cfpStatus = result.CFPResultStatus;
const ofpStatus = result.ofpResultStatus;
if (copperStatus) {
return copperStatus === 'pass';
} else if (cfpStatus) {
return cfpStatus === 'pass';
} else if (ofpStatus) {
return ofpStatus === 'pass';
}
return false;
}).length;
const failCount = currentProject?.testResults.filter(result => {
const copperStatus = result.CopperResultStatus;
const cfpStatus = result.CFPResultStatus;
const ofpStatus = result.ofpResultStatus;
if (copperStatus) {
return copperStatus === 'fail';
} else if (cfpStatus) {
return cfpStatus === 'fail';
} else if (ofpStatus) {
return ofpStatus === 'fail';
}
return false;
}).length;
const ConnectStatus = connectionStatus?.CFPConnectStatus ?? connectionStatus?.['8000ConnectStatus'];
// 计算下一个线缆ID
const getNextCableId = (currentId) => {
const match = currentId.match(/^(.*?)(\d+)$/);
if (match) {
const prefix = match[1];
const number = parseInt(match[2]) + 1;
return `${prefix}${number.toString().padStart(match[2].length, '0')}`;
}
return currentId;
};
const handleProjectClick = () => {
navigateTo('project', 'main');
};
const handleOperatorsClick = () => {
navigateTo('operators', 'main');
};
// 渲染测试配置区域的内容
const renderTestConfigContent = () => {
if (!currentConfig) return null;
switch (currentConfig.moduleType) {
case '8000':
return (
<div className="flex flex-col justify-center h-full space-y-0.2 text-sm">
<div className="text-black text-sm">{currentConfig.params.limitValue}</div>
<div className="text-black text-sm">{currentConfig.params.cableType}</div>
<div className="text-black text-sm">{currentConfig.params.wireOrder}</div>
</div>
);
case 'cfp':
return (
<div className="flex flex-col justify-center h-full space-y-0.2 text-sm">
<div className="text-black text-sm">智能远端</div>
<div className="text-black text-sm">{currentConfig.params.limitValue}</div>
<div className="text-black text-sm">{currentConfig.params.cableType}</div>
<div className="text-black text-sm">{currentConfig.params.refJumper} 跳线参照</div>
</div>
);
case 'ofp':
return (
<div className="flex flex-col justify-center h-full space-y-0.2 text-sm">
<div className="text-black text-sm">自动OTDR</div>
<div className="text-black text-sm">{currentConfig.params.limitValue}</div>
<div className="text-black text-sm">{currentConfig.params.cableType}</div>
</div>
);
default:
return null;
}
};
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<StatusBar />
<TitleBar title="HOME" />
<div className="h-[490px] bg-[#303040] p-2 flex flex-col">
{/* 上方信息区域 */}
<div className="h-[315px] bg-[#c5c2c5] rounded-sm p-3 shadow-lg flex flex-col gap-2">
{/* 项目配置部分 */}
<div className="flex justify-between items-start cursor-pointer" onClick={handleProjectClick}>
<div>
<div className="text-lg font-bold mb-2">项目{currentProject?.name}</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
<span className="text-green-500">{passCount|| 0}</span>
<div className="w-3 h-3 relative">
<Image
src="/pass.png"
alt="通过"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
</div>
<div className="flex items-center gap-1">
<span className="text-red-500">{failCount|| 0}</span>
<div className="w-3 h-3 relative">
<Image
src="/fail.png"
alt="失败"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
</div>
</div>
</div>
<div className="w-6 h-6 relative">
<Image
src="/arrow.png"
alt="箭头"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
</div>
{/* 测试配置区域 */}
<div
className="h-[100px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => navigateTo('testConfig', 'main')}
>
{renderTestConfigContent()}
<div className="flex items-center gap-2">
<span className="text-gray-500 text-sm">{currentConfig?.modulelable}</span>
<div className="w-3 h-3 relative">
<Image
src="/arrow.png"
alt="箭头"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
</div>
</div>
{/* 线缆ID区域 */}
<div
className="h-[60px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => navigateTo('cableId', currentConfig?.moduleType === 'cfp' ? 'cfp' : 'main')}
>
<div className="flex flex-col justify-center h-full space-y-0.2 text-sm">
{currentConfig?.moduleType === 'cfp' ? (
<>
<div className="text-black">下一个输入ID: {currentCableId?.name}</div>
<div className="text-black">下一个输出ID: {currentCableId2?.name}</div>
</>
) : (
<div className="text-black">下一个ID:
<p>{currentCableId?.name}</p>
</div>
)}
</div>
<div className="w-3 h-3 relative">
<Image
src="/arrow.png"
alt="箭头"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
</div>
{/* 操作员区域 */}
<div
className="h-[40px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={handleOperatorsClick}
>
<div className="text-black text-sm">操作员: {currentOperator?.name}</div>
<div className="w-3 h-3 relative">
<Image
src="/arrow.png"
alt="箭头"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
</div>
</div>
{/* 底部按钮区域 */}
<div className="flex-1 flex items-center justify-center gap-16">
<div className="flex flex-col items-center gap-2"
onClick={() => navigateTo('tools', 'main')}>
<div className="w-20 h-20 rounded-sm shadow-lg flex items-center justify-center cursor-pointer">
<div className="w-full h-full relative">
<Image
src="/tools.png"
alt="工具"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
</div>
<span className="text-white text-sm">工具</span>
</div>
<div
className="flex flex-col items-center gap-2 cursor-pointer"
onClick={() => navigateTo('result', 'main')}
>
<div className="w-20 h-20 rounded-sm shadow-lg flex items-center justify-center">
<div className="w-full h-full relative">
<Image
src="/result.png"
alt="结果"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
</div>
<span className="text-white text-sm">结果</span>
</div>
</div>
</div>
{/* 底部栏 */}
<div className="h-[60px] bg-[#303030] flex items-center justify-between px-5">
{/* 设置参照按钮 */}
<div className="flex-1"></div>
<div className="flex-1 flex justify-center">
{(currentConfig?.moduleType === 'cfp' || currentConfig?.moduleType === 'ofp') && (
<button
className="w-[120px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
onClick={() => {
// 根据模块类型设置不同的参照逻辑
if (currentConfig?.moduleType === 'cfp') {
navigateTo('tools', 'ref-cfp');
} else if (currentConfig?.moduleType === 'ofp') {
navigateTo('tools', 'ref-ofp');
}
}}
>
{currentConfig?.moduleType === 'ofp' ? '设置基准' : '设置参照'}
</button>
)}
</div>
{/* 测试按钮 */}
<div className="flex-1 flex justify-end">
<button
className="w-[100px] h-[40px] bg-gradient-to-b from-[#ffd773] to-[#e7aa29] rounded-sm flex items-center justify-center text-black font-bold shadow-lg"
onClick={() => {
if (currentConfig?.moduleType === '8000' || currentConfig?.moduleType === 'cfp') {
if (!ConnectStatus) {
setToastMessage('未连接,请检查连接状态');
return;
}
}
if (currentConfig?.moduleType === '8000') {
if (mainUnitModules?.[0]?.id !== '8000') {
setToastMessage('当前模块与所选标准不兼容');
return;
}
const limitValue = currentConfig.params.limitValue;
if (limitValue.includes('Channel') || limitValue.includes('Ch')) {
if (!mainUnitAdapter.id.includes('channel') || !remoteUnitAdapter.id.includes('channel')) {
setToastMessage('当前适配器与所选标准不兼容');
return;
}
} else if (limitValue.includes('Perm') || limitValue.includes('PL')) {
if (!mainUnitAdapter.id.includes('permanent') || !remoteUnitAdapter.id.includes('permanent')) {
setToastMessage('当前适配器与所选标准不兼容');
return;
}
} else if (limitValue.includes('MPTL')) {
const hasPermanent = mainUnitAdapter.id.includes('permanent') || remoteUnitAdapter.id.includes('permanent');
const hasPatchcode = mainUnitAdapter.id.includes('patchcode') || remoteUnitAdapter.id.includes('patchcode');
if (!(hasPermanent && hasPatchcode)) {
setToastMessage('当前适配器与所选标准不兼容');
return;
}
}
}
if (currentConfig?.moduleType === 'cfp') {
if (mainUnitModules?.[0]?.id !== 'cfp') {
setToastMessage('当前模块与所选标准不兼容');
return;
}
// 检查光纤类型
const cableType = currentConfig.params.cableType;
const isMultiMode = cableType.includes('OM');
// 根据单/多模获取对应的路径
let mainOutPath, mainInPath, remoteOutPath, remoteInPath;
if (isMultiMode) {
// 多模光纤路径
mainOutPath = connectionStatus?.mainPaths?.['main-cfp-mm-out']?.path || [];
mainInPath = connectionStatus?.mainPaths?.['main-cfp-in']?.path || [];
remoteOutPath = connectionStatus?.remotePaths?.['remote-cfp-mm-out']?.path || [];
remoteInPath = connectionStatus?.remotePaths?.['remote-cfp-in']?.path || [];
// 检查是否所有端口都已连接
if (!mainOutPath.length || !mainInPath.length || !remoteOutPath.length || !remoteInPath.length) {
setTotalToastMessage('请确保所有光纤端口都已正确连接');
return;
}
// 检查是否存在单模跳线
const hasInvalidConnector = [...mainOutPath, ...mainInPath, ...remoteOutPath, ...remoteInPath].some(item => {
return item.includes('sm');
});
if (hasInvalidConnector) {
setTotalToastMessage('多模光纤链路中存在单模跳线,请检查连接');
return;
}
} else {
// 单模光纤路径
mainOutPath = connectionStatus?.mainPaths?.['main-cfp-sm-out']?.path || [];
mainInPath = connectionStatus?.mainPaths?.['main-cfp-in']?.path || [];
remoteOutPath = connectionStatus?.remotePaths?.['remote-cfp-sm-out']?.path || [];
remoteInPath = connectionStatus?.remotePaths?.['remote-cfp-in']?.path || [];
// 检查是否所有端口都已连接
if (!mainOutPath.length || !mainInPath.length || !remoteOutPath.length || !remoteInPath.length) {
setTotalToastMessage('请确保所有光纤端口都已正确连接');
return;
}
// 检查是否存在多模跳线
const hasInvalidConnector = [...mainOutPath, ...mainInPath, ...remoteOutPath, ...remoteInPath].some(item => {
return item.includes('mm');
});
if (hasInvalidConnector) {
setTotalToastMessage('单模光纤链路中存在多模跳线,请检查连接');
return;
}
}
}
if (currentConfig?.moduleType === 'ofp') {
if (mainUnitModules?.[0]?.id !== 'ofp') {
setToastMessage('当前模块与所选标准不兼容');
return;
}
// 检查光纤类型是否匹配
const cableType = currentConfig.params.cableType;
const isMultiMode = cableType.includes('OM');
const portType = Object.values(connectionStatus?.mainPaths || {})[0]?.start;
if (isMultiMode && portType !== 'main-ofp-mm-out') {
setTotalToastMessage('当前光纤类型必须使用多模端口测试');
return;
}
if (!isMultiMode && portType !== 'main-ofp-sm-out') {
setTotalToastMessage('当前光纤类型必须使用单模端口测试');
return;
}
// 检查光纤连接器类型是否匹配
const path = Object.values(connectionStatus?.mainPaths || {})[0]?.path || [];
const hasInvalidConnector = path.some(item => {
if (isMultiMode) {
return item.includes('sm-') || item.includes('smc-');
} else {
return item.includes('mm-') || item.includes('mmc-');
}
});
if (hasInvalidConnector) {
setTotalToastMessage(isMultiMode ? '未检测到多模光纤,请检查光纤连接' : '未检测到单模光纤,请检查光纤连接');
return;
}
if (!connectionStatus?.OFPStatus) {
setTotalToastMessage('请接入实际被测链路,进行测试');
return;
}
// 检查光纤连接数量
const fiberConnectorCount = path.filter(item => {
return item.includes('sm-') || item.includes('mm-') ||
item.includes('smc-') || item.includes('mmc-');
}).length;
if (fiberConnectorCount > 3) {
setTotalToastMessage('该场景仅支持使用一段光纤连接测试仪与链路');
return;
}
}
navigateTo('testing','nosave');
}}
>
测试
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,681 @@
import React, { useState,useEffect } from 'react';
import StatusBar from '../lib/StatusBar';
import TitleBar from '../lib/TitleBar';
import SubTitleBar from '../lib/SubTitleBar';
import useDisplayStore from '@/store/displayStore';
// 菜单数据定义
const menuData = {
MODULE: {
title: '模块',
items: [
{ value: '8000', label: 'DSX-8000' },
{ value: 'cfp', label: 'CertiFiber Pro-Quad' },
{ value: 'ofp', label: 'OptiFiber Pro-Quad' }
]
},
CABLE_TYPE: {
title: '电缆类型',
subTitle: '电缆类型',
items: [
{ value: 'custom', label: '定制', disabled: true },
{
value: 'general',
label: '通用',
children: [
// { value: 'Cat8 S/FTP', label: 'Cat8 S/FTP' },
// { value: 'Cat7A S/FTP', label: 'Cat7A S/FTP' },
// { value: 'Cat7 S/FTP', label: 'Cat7 S/FTP' },
// { value: 'Cat6A S/FTP', label: 'Cat6A S/FTP' },
{ value: 'Cat6A U/UTP', label: 'Cat6A U/UTP' },
{ value: 'Cat6A F/UTP', label: 'Cat6A F/UTP' },
{ value: 'Cat6 U/UTP', label: 'Cat6 U/UTP' },
{ value: 'Cat6 F/UTP', label: 'Cat6 F/UTP' },
// { value: 'Cat6 U/FTP', label: 'Cat6 U/FTP' },
{ value: 'Cat5e U/UTP', label: 'Cat5e U/UTP' },
{ value: 'Cat5e F/UTP', label: 'Cat5e F/UTP' },
// { value: 'Cat5 U/UTP', label: 'Cat5 U/UTP' },
// { value: 'Cat5 F/UTP', label: 'Cat5 F/UTP' }
]
},
{ value: 'coaxial', label: '通用同轴电缆', disabled: true },
{ value: 'manufacturer', label: '制造商', disabled: true }
]
},
LIMIT_VALUE: {
title: '测试极限值',
subTitle: '极限值组',
items: [
{
value: 'TIA',
label: 'TIA',
children: {
// Cat8: {
// label: 'Cat8',
// children: [
// { value: 'TIA Cat 8 Perm.Link', label: 'TIA Cat 8 Perm.Link' },
// { value: 'TIA Cat 8 Perm.Link (+ALL)', label: 'TIA Cat 8 Perm.Link (+ALL)' },
// { value: 'TIA Cat 8 Perm.Link (+PoE)', label: 'TIA Cat 8 Perm.Link (+PoE)' },
// { value: 'TIA Cat 8 Channel', label: 'TIA Cat 8 Channel' },
// { value: 'TIA Cat 8 Channel (+ALL)', label: 'TIA Cat 8 Channel (+ALL)' },
// { value: 'TIA Cat 8 Channel (+PoE)', label: 'TIA Cat 8 Channel (+PoE)' }
// ]
// },
Cat6A: {
label: 'Cat6A',
children: [
{ value: 'TIA Cat 6A Perm.Link', label: 'TIA Cat 6A Perm.Link' },
{ value: 'TIA Cat 6A Perm.Link (+ALL)', label: 'TIA Cat 6A Perm.Link (+ALL)' },
{ value: 'TIA Cat 6A Perm.Link (+PoE)', label: 'TIA Cat 6A Perm.Link (+PoE)' },
{ value: 'TIA Cat 6A Channel', label: 'TIA Cat 6A Channel' },
{ value: 'TIA Cat 6A Channel (+ALL)', label: 'TIA Cat 6A Channel (+ALL)' },
{ value: 'TIA Cat 6A Channel (+PoE)', label: 'TIA Cat 6A Channel (+PoE)' },
{ value: 'TIA Cat 6A MPTL', label: 'TIA Cat 6A MPTL' },
{ value: 'TIA Cat 6A MPTL (+PoE)', label: 'TIA Cat 6A MPTL (+PoE)' },
// { value: 'TIA 1005 Cat 6A Perm.Link', label: 'TIA 1005 Cat 6A Perm.Link' },
// { value: 'TIA 1005 Cat 6A Channel', label: 'TIA 1005 Cat 6A Channel' },
{ value: 'TIA 1005 Cat 6A Channel E1 (+ALL)', label: 'TIA 1005 Cat 6A Channel E1 (+ALL)' },
// { value: 'TIA 1005 Cat 6A Channel E1 (+PoE)', label: 'TIA 1005 Cat 6A Channel E1 (+PoE)' },
{ value: 'TIA 1005 Cat 6A Channel E2 (+ALL)', label: 'TIA 1005 Cat 6A Channel E2 (+ALL)' },
// { value: 'TIA 1005 Cat 6A Channel E2 (+PoE)', label: 'TIA 1005 Cat 6A Channel E2 (+PoE)' },
{ value: 'TIA 1005 Cat 6A Channel E3 (+ALL)', label: 'TIA 1005 Cat 6A Channel E3 (+ALL)' },
// { value: 'TIA 1005 Cat 6A Channel E3 (+PoE)', label: 'TIA 1005 Cat 6A Channel E3 (+PoE)' }
]
},
Cat6: {
label: 'Cat6',
children: [
{ value: 'TIA Cat 6 Perm.Link', label: 'TIA Cat 6 Perm.Link' },
{ value: 'TIA Cat 6 Perm.Link (+ALL)', label: 'TIA Cat 6 Perm.Link (+ALL)' },
{ value: 'TIA Cat 6 Perm.Link (+PoE)', label: 'TIA Cat 6 Perm.Link (+PoE)' },
{ value: 'TIA Cat 6 Channel', label: 'TIA Cat 6 Channel' },
{ value: 'TIA Cat 6 Channel (+ALL)', label: 'TIA Cat 6 Channel (+ALL)' },
{ value: 'TIA Cat 6 Channel (+PoE)', label: 'TIA Cat 6 Channel (+PoE)' },
{ value: 'TIA Cat 6 MPTL', label: 'TIA Cat 6 MPTL' },
{ value: 'TIA Cat 6 MPTL (+PoE)', label: 'TIA Cat 6 MPTL (+PoE)' },
// { value: 'TIA 1005 Cat 6 Perm.Link', label: 'TIA 1005 Cat 6 Perm.Link' },
// { value: 'TIA 1005 Cat 6 Channel', label: 'TIA 1005 Cat 6 Channel' },
// { value: 'TIA 1005 Cat 6 Channel (+ALL)', label: 'TIA 1005 Cat 6 Channel (+ALL)' },
// { value: 'TIA 1005 Cat 6 Channel (+PoE)', label: 'TIA 1005 Cat 6 Channel (+PoE)' },
{ value: 'TIA 1005 Cat 6 Channel E1 (+ALL)', label: 'TIA 1005 Cat 6 Channel E1 (+ALL)' },
// { value: 'TIA 1005 Cat 6 Channel E1 (+PoE)', label: 'TIA 1005 Cat 6 Channel E1 (+PoE)' },
{ value: 'TIA 1005 Cat 6 Channel E2 (+ALL)', label: 'TIA 1005 Cat 6 Channel E2 (+ALL)' },
// { value: 'TIA 1005 Cat 6 Channel E2 (+PoE)', label: 'TIA 1005 Cat 6 Channel E2 (+PoE)' },
{ value: 'TIA 1005 Cat 6 Channel E3 (+ALL)', label: 'TIA 1005 Cat 6 Channel E3 (+ALL)' },
// { value: 'TIA 1005 Cat 6 Channel E3 (+PoE)', label: 'TIA 1005 Cat 6 Channel E3 (+PoE)' }
]
},
Cat5e: {
label: 'Cat5e',
children: [
{ value: 'TIA Cat 5e Perm.Link', label: 'TIA Cat 5e Perm.Link' },
{ value: 'TIA Cat 5e Perm.Link (+ALL)', label: 'TIA Cat 5e Perm.Link (+ALL)' },
{ value: 'TIA Cat 5e Perm.Link (+PoE)', label: 'TIA Cat 5e Perm.Link (+PoE)' },
{ value: 'TIA Cat 5e Channel', label: 'TIA Cat 5e Channel' },
{ value: 'TIA Cat 5e Channel (+ALL)', label: 'TIA Cat 5e Channel (+ALL)' },
{ value: 'TIA Cat 5e Channel (+PoE)', label: 'TIA Cat 5e Channel (+PoE)' },
{ value: 'TIA Cat 5e MPTL', label: 'TIA Cat 5e MPTL' },
{ value: 'TIA Cat 5e MPTL (+PoE)', label: 'TIA Cat 5e MPTL (+PoE)' },
// { value: 'TIA 1005 Cat 5e Perm.Link', label: 'TIA 1005 Cat 5e Perm.Link' },
// { value: 'TIA 1005 Cat 5e Channel', label: 'TIA 1005 Cat 5e Channel' },
// { value: 'TIA 1005 Cat 5e Channel (+ALL)', label: 'TIA 1005 Cat 5e Channel (+ALL)' },
// { value: 'TIA 1005 Cat 5e Channel (+PoE)', label: 'TIA 1005 Cat 5e Channel (+PoE)' },
{ value: 'TIA 1005 Cat 5e Channel E1 (+ALL)', label: 'TIA 1005 Cat 5e Channel E1 (+ALL)' },
// { value: 'TIA 1005 Cat 5e Channel E1 (+PoE)', label: 'TIA 1005 Cat 5e Channel E1 (+PoE)' },
{ value: 'TIA 1005 Cat 5e Channel E2 (+ALL)', label: 'TIA 1005 Cat 5e Channel E2 (+ALL)' },
// { value: 'TIA 1005 Cat 5e Channel E2 (+PoE)', label: 'TIA 1005 Cat 5e Channel E2 (+PoE)' },
{ value: 'TIA 1005 Cat 5e Channel E3 (+ALL)', label: 'TIA 1005 Cat 5e Channel E3 (+ALL)' },
// { value: 'TIA 1005 Cat 5e Channel E3 (+PoE)', label: 'TIA 1005 Cat 5e Channel E3 (+PoE)' }
]
}
}
},
{
value: 'ISO',
label: 'ISO',
children: {
// 'Class Ⅱ': {
// label: 'Class Ⅱ',
// children: [
// { value: 'ISO11801 PL Class Ⅱ', label: 'ISO11801 PL Class Ⅱ' },
// { value: 'ISO11801 PL Class Ⅱ (+ALL)', label: 'ISO11801 PL Class Ⅱ (+ALL)' },
// { value: 'ISO11801 PL Class Ⅱ (+PoE)', label: 'ISO11801 PL Class Ⅱ (+PoE)' },
// { value: 'ISO11801 Channel Class Ⅱ', label: 'ISO11801 Channel Class Ⅱ' },
// { value: 'ISO11801 Channel Class Ⅱ (+ALL)', label: 'ISO11801 Channel Class Ⅱ (+ALL)' },
// { value: 'ISO11801 Channel Class Ⅱ (+PoE)', label: 'ISO11801 Channel Class Ⅱ (+PoE)' }
// ],
// disabled: true
// },
// 'Class ': {
// label: 'Class ',
// children: [
// { value: 'ISO11801 PL Class ', label: 'ISO11801 PL Class ' },
// { value: 'ISO11801 PL Class (+ALL)', label: 'ISO11801 PL Class (+ALL)' },
// { value: 'ISO11801 PL Class (+PoE)', label: 'ISO11801 PL Class (+PoE)' },
// { value: 'ISO11801 Channel Class ', label: 'ISO11801 Channel Class ' },
// { value: 'ISO11801 Channel Class (+ALL)', label: 'ISO11801 Channel Class (+ALL)' },
// { value: 'ISO11801 Channel Class (+PoE)', label: 'ISO11801 Channel Class (+PoE)' }
// ],
// disabled: true
// },
// 'Class Fa': {
// label: 'Class Fa',
// children: [
// { value: 'ISO11801 PL2 Class Fa', label: 'ISO11801 PL2 Class Fa' },
// { value: 'ISO11801 PL2 Class Fa (+ALL)', label: 'ISO11801 PL2 Class Fa (+ALL)' },
// { value: 'ISO11801 PL2 Class Fa (+PoE)', label: 'ISO11801 PL2 Class Fa (+PoE)' },
// { value: 'ISO11801 PL3 Class Fa', label: 'ISO11801 PL3 Class Fa' },
// { value: 'ISO11801 PL3 Class Fa (+ALL)', label: 'ISO11801 PL3 Class Fa (+ALL)' },
// { value: 'ISO11801 PL3 Class Fa (+PoE)', label: 'ISO11801 PL3 Class Fa (+PoE)' },
// { value: 'ISO11801 Channel Class Fa', label: 'ISO11801 Channel Class Fa' },
// { value: 'ISO11801 Channel Class Fa (+ALL)', label: 'ISO11801 Channel Class Fa (+ALL)' },
// { value: 'ISO11801 Channel Class Fa (+PoE)', label: 'ISO11801 Channel Class Fa (+PoE)' }
// ],
// disabled: true
// },
// 'Class F': {
// label: 'Class F',
// children: [
// { value: 'ISO11801 PL Class F', label: 'ISO11801 PL Class F' },
// { value: 'ISO11801 PL Class F (+ALL)', label: 'ISO11801 PL Class F (+ALL)' },
// { value: 'ISO11801 PL Class F (+PoE)', label: 'ISO11801 PL Class F (+PoE)' },
// { value: 'ISO11801 Channel Class F', label: 'ISO11801 Channel Class F' },
// { value: 'ISO11801 Channel Class F (+ALL)', label: 'ISO11801 Channel Class F (+ALL)' },
// { value: 'ISO11801 Channel Class F (+PoE)', label: 'ISO11801 Channel Class F (+PoE)' }
// ],
// disabled: true
// },
'Class Ea': {
label: 'Class Ea',
children: [
{ value: 'ISO11801 PL2 Class Ea', label: 'ISO11801 PL2 Class Ea' },
{ value: 'ISO11801 PL2 Class Ea (+ALL)', label: 'ISO11801 PL2 Class Ea (+ALL)' },
{ value: 'ISO11801 PL2 Class Ea (+PoE)', label: 'ISO11801 PL2 Class Ea (+PoE)' },
{ value: 'ISO11801 PL3 Class Ea', label: 'ISO11801 PL3 Class Ea' },
{ value: 'ISO11801 PL3 Class Ea (+ALL)', label: 'ISO11801 PL3 Class Ea (+ALL)' },
{ value: 'ISO11801 PL3 Class Ea (+PoE)', label: 'ISO11801 PL3 Class Ea (+PoE)' },
{ value: 'ISO11801 Channel Class Ea', label: 'ISO11801 Channel Class Ea' },
{ value: 'ISO11801 Channel Class Ea (+ALL)', label: 'ISO11801 Channel Class Ea (+ALL)' },
{ value: 'ISO11801 Channel Class Ea (+PoE)', label: 'ISO11801 Channel Class Ea (+PoE)' },
{ value: 'ISO MPTL Class Ea', label: 'ISO MPTL Class Ea' },
{ value: 'ISO MPTL Class Ea (+PoE)', label: 'ISO MPTL Class Ea (+PoE)' }
]
},
'Class E': {
label: 'Class E',
children: [
{ value: 'ISO11801 PL Class E', label: 'ISO11801 PL Class E' },
{ value: 'ISO11801 PL Class E (+ALL)', label: 'ISO11801 PL Class E (+ALL)' },
{ value: 'ISO11801 PL Class E (+PoE)', label: 'ISO11801 PL Class E (+PoE)' },
{ value: 'ISO11801 Channel Class E', label: 'ISO11801 Channel Class E' },
{ value: 'ISO11801 Channel Class E (+ALL)', label: 'ISO11801 Channel Class E (+ALL)' },
{ value: 'ISO11801 Channel Class E (+PoE)', label: 'ISO11801 Channel Class E (+PoE)' },
{ value: 'ISO MPTL Class E', label: 'ISO MPTL Class E' },
{ value: 'ISO MPTL Class E (+PoE)', label: 'ISO MPTL Class E (+PoE)' }
]
},
'Class D': {
label: 'Class D',
children: [
{ value: 'ISO11801 PL Class D', label: 'ISO11801 PL Class D' },
{ value: 'ISO11801 PL Class D (+ALL)', label: 'ISO11801 PL Class D (+ALL)' },
{ value: 'ISO11801 PL Class D (+PoE)', label: 'ISO11801 PL Class D (+PoE)' },
{ value: 'ISO11801 Channel Class D', label: 'ISO11801 Channel Class D' },
{ value: 'ISO11801 Channel Class D (+ALL)', label: 'ISO11801 Channel Class D (+ALL)' },
{ value: 'ISO11801 Channel Class D (+PoE)', label: 'ISO11801 Channel Class D (+PoE)' },
{ value: 'ISO MPTL Class D', label: 'ISO MPTL Class D' },
{ value: 'ISO MPTL Class D (+PoE)', label: 'ISO MPTL Class D (+PoE)' }
]
}
}
},
{
value: '区域',
label: '区域',
children: {
'中国': {
label: '中国',
children: [
// { value: 'GBT 50312-2016 Cat 7A PL no CP', label: 'GB/T 50312-2016 Cat 7A PL no CP' },
// { value: 'GBT 50312-2016 Cat 7A PL no CP (+ALL)', label: 'GB/T 50312-2016 Cat 7A PL no CP (+ALL)' },
// { value: 'GBT 50312-2016 Cat 7A PL no CP (+PoE)', label: 'GB/T 50312-2016 Cat 7A PL no CP (+PoE)' },
// { value: 'GBT 50312-2016 Cat 7A PL with CP', label: 'GB/T 50312-2016 Cat 7A PL with CP' },
// { value: 'GBT 50312-2016 Cat 7A PL with CP (+ALL)', label: 'GB/T 50312-2016 Cat 7A PL with CP (+ALL)' },
// { value: 'GBT 50312-2016 Cat 7A PL with CP (+PoE)', label: 'GB/T 50312-2016 Cat 7A PL with CP (+PoE)' },
// { value: 'GBT 50312-2016 Cat 7A Ch', label: 'GB/T 50312-2016 Cat 7A Ch' },
// { value: 'GBT 50312-2016 Cat 7A Ch (+ALL)', label: 'GB/T 50312-2016 Cat 7A Ch (+ALL)' },
// { value: 'GBT 50312-2016 Cat 7A Ch (+PoE)', label: 'GB/T 50312-2016 Cat 7A Ch (+PoE)' },
// { value: 'GBT 50312-2016 Cat 7 PL', label: 'GB/T 50312-2016 Cat 7 PL' },
// { value: 'GBT 50312-2016 Cat 7 PL (+ALL)', label: 'GB/T 50312-2016 Cat 7 PL (+ALL)' },
// { value: 'GBT 50312-2016 Cat 7 PL (+PoE)', label: 'GB/T 50312-2016 Cat 7 PL (+PoE)' },
// { value: 'GBT 50312-2016 Cat 7 Ch', label: 'GB/T 50312-2016 Cat 7 Ch' },
// { value: 'GBT 50312-2016 Cat 7 Ch (+ALL)', label: 'GB/T 50312-2016 Cat 7 Ch (+ALL)' },
// { value: 'GBT 50312-2016 Cat 7 Ch (+PoE)', label: 'GB/T 50312-2016 Cat 7 Ch (+PoE)' },
{ value: 'GBT 50312-2016 Cat 6A PL no CP', label: 'GB/T 50312-2016 Cat 6A PL no CP' },
{ value: 'GBT 50312-2016 Cat 6A PL no CP (+ALL)', label: 'GB/T 50312-2016 Cat 6A PL no CP (+ALL)' },
{ value: 'GBT 50312-2016 Cat 6A PL no CP (+PoE)', label: 'GB/T 50312-2016 Cat 6A PL no CP (+PoE)' },
{ value: 'GBT 50312-2016 Cat 6A PL with CP', label: 'GB/T 50312-2016 Cat 6A PL with CP' },
{ value: 'GBT 50312-2016 Cat 6A PL with CP (+ALL)', label: 'GB/T 50312-2016 Cat 6A PL with CP (+ALL)' },
{ value: 'GBT 50312-2016 Cat 6A PL with CP (+PoE)', label: 'GB/T 50312-2016 Cat 6A PL with CP (+PoE)' },
{ value: 'GBT 50312-2016 Cat 6A Ch', label: 'GB/T 50312-2016 Cat 6A Ch' },
{ value: 'GBT 50312-2016 Cat 6A Ch (+ALL)', label: 'GB/T 50312-2016 Cat 6A Ch (+ALL)' },
{ value: 'GBT 50312-2016 Cat 6A Ch (+PoE)', label: 'GB/T 50312-2016 Cat 6A Ch (+PoE)' },
{ value: 'GBT 50312-2016 Cat 6 PL', label: 'GB/T 50312-2016 Cat 6 PL' },
{ value: 'GBT 50312-2016 Cat 6 PL (+ALL)', label: 'GB/T 50312-2016 Cat 6 PL (+ALL)' },
{ value: 'GBT 50312-2016 Cat 6 PL (+PoE)', label: 'GB/T 50312-2016 Cat 6 PL (+PoE)' },
{ value: 'GBT 50312-2016 Cat 6 Ch', label: 'GB/T 50312-2016 Cat 6 Ch' },
{ value: 'GBT 50312-2016 Cat 6 Ch (+ALL)', label: 'GB/T 50312-2016 Cat 6 Ch (+ALL)' },
{ value: 'GBT 50312-2016 Cat 6 Ch (+PoE)', label: 'GB/T 50312-2016 Cat 6 Ch (+PoE)' },
{ value: 'GBT 50312-2016 Cat 5e PL', label: 'GB/T 50312-2016 Cat 5e PL' },
{ value: 'GBT 50312-2016 Cat 5e PL (+ALL)', label: 'GB/T 50312-2016 Cat 5e PL (+ALL)' },
{ value: 'GBT 50312-2016 Cat 5e PL (+PoE)', label: 'GB/T 50312-2016 Cat 5e PL (+PoE)' },
{ value: 'GBT 50312-2016 Cat 5e Ch', label: 'GB/T 50312-2016 Cat 5e Ch' },
{ value: 'GBT 50312-2016 Cat 5e Ch (+ALL)', label: 'GB/T 50312-2016 Cat 5e Ch (+ALL)' },
{ value: 'GBT 50312-2016 Cat 5e Ch (+PoE)', label: 'GB/T 50312-2016 Cat 5e Ch (+PoE)' }
]
}
}
},
{
value: '跳线',
label: '跳线',
disabled: true,
children: {
'Cat6A Patch Cords': {
label: 'Cat6A Patch Cords',
disabled: true,
children: []
},
'Cat6 Patch Cords': {
label: 'Cat6 Patch Cords',
disabled: true,
children: []
},
'Cat5e Patch Cords': {
label: 'Cat5e Patch Cords',
disabled: true,
children: []
},
'M12 Patch Cords': {
label: 'M12 Patch Cords',
disabled: true,
children: []
}
}
},
{
value: '应用',
label: '应用',
disabled: false,
children: [
{ value: 'Profinet', label: 'Profinet' },
]
}
]
},
WIRE_ORDER: {
title: '插座配置',
items: [
{ value: 'T568A', label: 'T568A' , disabled: true},
{ value: 'T568B', label: 'T568B' },
{ value: 'Ethernet Two-Pair', label: 'Ethernet Two-Pair' },
{ value: 'M12-D Two-Pair', label: 'M12-D Two-Pair' },
]
},
FIBER_TYPE: {
title: '光纤类型',
subTitle: '电缆组',
items: [
{
value: '定制',
label: '定制',
disabled: true,
children: {}
},
{
value: 'general',
label: '通用',
children: [
{ value: 'OM1 Mulitmode 62.5', label: 'OM1 Mulitmode 62.5' },
{ value: 'OM2 Mulitmode 50', label: 'OM2 Mulitmode 50' },
{ value: 'OM3 Mulitmode 50', label: 'OM3 Mulitmode 50' },
{ value: 'OM4 Mulitmode 50', label: 'OM4 Mulitmode 50' },
{ value: 'OM5 Mulitmode 50', label: 'OM5 Mulitmode 50' },
{ value: 'OS1 Singlemode', label: 'OS1 Singlemode' },
{ value: 'OS2 Singlemode', label: 'OS2 Singlemode' }
]
},
{
value: '制造商',
label: '制造商',
disabled: true,
children: {}
}
]
},
CFP_LIMIT: {
title: '测试极限值',
subTitle: '极限值组',
items: [
{
value: 'TIA',
label: 'TIA',
children: [
{ value: 'TIA-568.3-E Multimode (STD)', label: 'TIA-568.3-E Multimode (STD)' },
{ value: 'TIA-568.3-E Multimode (REF)', label: 'TIA-568.3-E Multimode (REF)' },
{ value: 'TIA-568.3-E Singlemode ISP (STD)', label: 'TIA-568.3-E Singlemode ISP (STD)' },
{ value: 'TIA-568.3-E Singlemode OSP (STD)', label: 'TIA-568.3-E Singlemode OSP (STD)' },
{ value: 'TIA-568.3-E Singlemode ISP (REF)', label: 'TIA-568.3-E Singlemode ISP (REF)' },
{ value: 'TIA-568.3-E Singlemode OSP (REF)', label: 'TIA-568.3-E Singlemode OSP (REF)' }
]
},
{
value: 'China',
label: '中国',
children: [
{ value: 'GB/T 50312-2016 Fiber Link', label: 'GB/T 50312-2016 Fiber Link' },
{ value: 'GB/T 50312-2016 OF-300 Ch', label: 'GB/T 50312-2016 OF-300 Ch' },
{ value: 'GB/T 50312-2016 OF-500 Ch', label: 'GB/T 50312-2016 OF-500 Ch' },
{ value: 'GB/T 50312-2016 OF-2000 Ch', label: 'GB/T 50312-2016 OF-2000 Ch' }
]
},
{
value: 'ISO',
label: 'ISO',
children: [
{ value: 'ISO/IEC 11801-2002 Fibre Link', label: 'ISO/IEC 11801-2002 Fibre Link' },
{ value: 'ISO/IEC 11801-2002 OF-300 CH', label: 'ISO/IEC 11801-2002 OF-300 CH' },
{ value: 'ISO/IEC 11801-2002 OF-500 CH', label: 'ISO/IEC 11801-2002 OF-500 CH' },
{ value: 'ISO/IEC 11801-2002 OF-2000 CH', label: 'ISO/IEC 11801-2002 OF-2000 CH' },
{ value: 'ISO/IEC 14763-3:2024 (Draft)', label: 'ISO/IEC 14763-3:2024 (Draft)' },
{ value: 'ISO/IEC 14763-3:2014', label: 'ISO/IEC 14763-3:2014' },
{ value: 'ISO/IEC 14763-3', label: 'ISO/IEC 14763-3' }
]
}
]
},
OFP_LIMIT: {
title: 'OFP极限值',
items: [
{
value: 'TIA',
label: 'TIA',
children: [
{ value: 'ANSI/TIA-568.3-E', label: 'ANSI/TIA-568.3-E' },
{ value: 'ANSI/TIA-568.3-E RL = 20 dB', label: 'ANSI/TIA-568.3-E RL = 20 dB' },
{ value: 'ANSI/TIA-568.3-E RL = 35 dB', label: 'ANSI/TIA-568.3-E RL = 35 dB' },
{ value: 'ANSI/TIA-568.3-E RL = 55 dB', label: 'ANSI/TIA-568.3-E RL = 55 dB' }
]
},
{
value: 'China',
label: '中国',
children: [
{ value: 'GB/T 50312-2016 Fiber Link', label: 'GB/T 50312-2016 Fiber Link' },
{ value: 'GB/T 50312-2016 OF-300 Ch', label: 'GB/T 50312-2016 OF-300 Ch' },
{ value: 'GB/T 50312-2016 OF-500 Ch', label: 'GB/T 50312-2016 OF-500 Ch' },
{ value: 'GB/T 50312-2016 OF-2000 Ch', label: 'GB/T 50312-2016 OF-2000 Ch' }
]
},
{
value: 'ISO',
label: 'ISO',
children: [
{ value: 'ISO/IEC 11801-2002 Fibre Link', label: 'ISO/IEC 11801-2002 Fibre Link' },
{ value: 'ISO/IEC 11801-2002 OF-300 CH', label: 'ISO/IEC 11801-2002 OF-300 CH' },
{ value: 'ISO/IEC 11801-2002 OF-500 CH', label: 'ISO/IEC 11801-2002 OF-500 CH' },
{ value: 'ISO/IEC 11801-2002 OF-2000 CH', label: 'ISO/IEC 11801-2002 OF-2000 CH' },
{ value: 'ISO/IEC 14763-3:2024 (Draft)', label: 'ISO/IEC 14763-3:2024 (Draft)' },
{ value: 'ISO/IEC 14763-3:2024 RL = 20 (Draft)', label: 'ISO/IEC 14763-3:2024 RL = 20 (Draft)' },
{ value: 'ISO/IEC 14763-3:2024 RL = 35 (Draft)', label: 'ISO/IEC 14763-3:2024 RL = 35 (Draft)' },
{ value: 'ISO/IEC 14763-3:2024 RL = 60 (Draft)', label: 'ISO/IEC 14763-3:2024 RL = 60 (Draft)' },
{ value: 'ISO/IEC 14763-3:2014', label: 'ISO/IEC 14763-3:2014' },
{ value: 'ISO/IEC 14763-3', label: 'ISO/IEC 14763-3' }
]
}
]
}
};
export default function MenuList() {
const {
navigation,
getCurrentProject,
updateProject,
navigateTo,
goBack
} = useDisplayStore();
const currentProject = getCurrentProject();
const { menuType, selectedConfigId } = navigation.current.params;
const [selectedPath, setSelectedPath] = useState([]);
const [currentMenu, setCurrentMenu] = useState(menuData[menuType]);
// 获取当前选中的配置
const currentConfig = currentProject.testConfigs.find(
config => config.id === selectedConfigId
);
// 处理菜单选项选择
const handleMenuSelect = (item) => {
if (item.disabled) return;
if (item.children) {
// 如果有子菜单,更新路径和当前菜单
setSelectedPath([...selectedPath, item]);
setCurrentMenu(item.children);
return;
}
// 如果是最终选项,更新配置并返回
let updatedConfig = { ...currentConfig };
switch (menuType) {
case 'CABLE_TYPE':
updatedConfig.params.cableType = item.value;
break;
case 'LIMIT_VALUE':
updatedConfig.params.limitValue = item.value;
break;
case 'WIRE_ORDER':
updatedConfig.params.wireOrder = item.value;
break;
case 'FIBER_TYPE':
updatedConfig.params.cableType = item.value;
if (updatedConfig.moduleType === 'cfp') {
if (item.value.includes('OM')) {
updatedConfig.params.limitValue = 'TIA-568.3-E Multimode (STD)';
} else {
updatedConfig.params.limitValue = 'TIA-568.3-E Singlemode ISP (STD)';
}
}
break;
case 'CFP_LIMIT':
updatedConfig.params.limitValue = item.value;
break;
case 'OFP_LIMIT':
updatedConfig.params.limitValue = item.value;
break;
case 'MODULE':
// 如果模块类型发生变化,完全替换配置对象
if (currentConfig.moduleType !== item.value) {
switch (item.value) {
case '8000':
updatedConfig = {
id: currentConfig.id,
moduleType: '8000',
modulelable: 'DSX-8000',
params: {
limitValue: 'TIA Cat 6 Channel',
cableType: 'Cat6 U/UTP',
wireOrder: 'T568B'
}
};
break;
case 'cfp':
updatedConfig = {
id: currentConfig.id,
moduleType: 'cfp',
modulelable: 'CertiFiber Pro-Quad',
params: {
limitValue: 'TIA-568.3-E Multimode (STD)',
cableType: 'OM3 Multimode 50',
refJumper: '1',
spliceCount: '0',
connectorCount: '2'
}
};
break;
case 'ofp':
updatedConfig = {
id: currentConfig.id,
moduleType: 'ofp',
modulelable: 'OptiFiber Pro-Quad',
params: {
limitValue: 'General Fiber RL = 35 dB',
cableType: 'OM3 Multimode 50'
}
};
break;
}
} else {
updatedConfig.moduleType = item.value;
updatedConfig.modulelable = item.label;
}
break;
}
// 更新项目数据
const updatedConfigs = currentProject.testConfigs.map(config => {
if (config.id === selectedConfigId) {
return updatedConfig;
}
return config;
});
const projectIndex = useDisplayStore.getState().selectedIndexes.projectIndex;
updateProject(projectIndex, { testConfigs: updatedConfigs });
// 返回上一页
goBack();
};
// 处理返回上一级
const handleBack = () => {
if (selectedPath.length > 0) {
const newPath = selectedPath.slice(0, -1);
setSelectedPath(newPath);
if (newPath.length === 0) {
setCurrentMenu(menuData[menuType]);
} else {
let current = menuData[menuType];
for (const item of newPath) {
current = current.items.find(i => i.value === item.value).children;
}
setCurrentMenu(current);
}
} else {
goBack();
}
};
// 渲染菜单选项
const renderMenuOptions = () => {
if (!currentMenu) return null;
// 如果是子菜单,直接渲染子菜单项
if (Array.isArray(currentMenu)) {
return (
<div className="w-full h-[490px]">
{currentMenu.map((item) => (
<div
key={item.value}
className={`h-[60px] w-full bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] p-4 shadow-lg flex items-center ${
item.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
} ${
currentConfig.params.cableType === item.value ? 'bg-blue-500' : ''
}`}
onClick={() => handleMenuSelect(item)}
>
<div className="text-black text-sm">{item.label}</div>
</div>
))}
</div>
);
}
// 如果是对象形式的子菜单(如 LIMIT_VALUE 的二级菜单)
if (typeof currentMenu === 'object' && !currentMenu.items) {
return (
<div className="w-full h-[490px]">
{Object.entries(currentMenu).map(([key, item]) => (
<div
key={key}
className={`h-[60px] w-full bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] p-4 shadow-lg flex items-center cursor-pointer ${
currentConfig.params.cableType === item.value ? 'bg-blue-500' : ''
}`}
onClick={() => handleMenuSelect(item)}
>
<div className="text-black text-sm">{item.label}</div>
</div>
))}
</div>
);
}
// 如果是主菜单,渲染主菜单项
return (
<div className="w-full h-[490px]">
{currentMenu.items.map((item) => (
<div
key={item.value}
className={`h-[60px] w-full bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] p-4 shadow-lg flex items-center ${
item.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
} ${
currentConfig.params.cableType === item.value ? 'bg-blue-500' : ''
}`}
onClick={() => handleMenuSelect(item)}
>
<div className="text-black text-sm">{item.label}</div>
</div>
))}
</div>
);
};
// 获取当前菜单标题(返回 null 表示不显示)
const getCurrentTitle = () => {
if (selectedPath.length === 0) {
const subTitle = menuData[menuType]?.subTitle;
return subTitle ? subTitle : null; // 如果 subTitle 为空则返回 null
}
return selectedPath[selectedPath.length - 1].label;
};
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<StatusBar />
<TitleBar
title={menuData[menuType].title}
backTo={selectedPath.length > 0 ? 'menulist' : navigation.previous?.name}
view={selectedPath.length > 0 ? 'setup' : navigation.previous?.view}
onBack={handleBack}
/>
{/* 只有当 getCurrentTitle() 返回值不为 null 时才渲染 SubTitleBar */}
{getCurrentTitle() !== null && (
<SubTitleBar title={getCurrentTitle()} />
)}
<div className="flex-1 bg-[#303040]">
<div className="h-full overflow-y-auto [&::-webkit-scrollbar]:hidden">
{renderMenuOptions()}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,190 @@
import React, { useState } from 'react';
import StatusBar from '../lib/StatusBar';
import TitleBar from '../lib/TitleBar';
import Keyboard from '../lib/Keyboard';
import useDisplayStore from '@/store/displayStore';
export default function Operators() {
const [inputValue, setInputValue] = useState('');
const [cursorPosition, setCursorPosition] = useState(0);
const [selectedOperators, setSelectedOperators] = useState([]);
const {
getCurrentProject,
setSelectedIndexes,
updateCurrentView,
addOperator,
updateProject,
goBack,
setToastMessage
} = useDisplayStore();
const currentProject = getCurrentProject();
const { view } = useDisplayStore.getState().navigation.current;
const handleDeleteOperators = () => {
if (selectedOperators.length === currentProject.operators.length) {
setToastMessage('至少需要保留一个操作员');
return;
}
// 过滤掉被选中的操作员
const newOperators = currentProject.operators.filter(
operator => !selectedOperators.includes(operator.id)
);
// 更新项目
const currentIndex = useDisplayStore.getState().selectedIndexes.projectIndex;
updateProject(currentIndex, { operators: newOperators });
// 如果当前选中的操作员被删除了重置选中的操作员索引为0
if (selectedOperators.includes(currentProject.operators[useDisplayStore.getState().selectedIndexes.operatorIndex].id)) {
setSelectedIndexes({ operatorIndex: 0 });
}
// 清空选中列表并返回main视图
setSelectedOperators([]);
updateCurrentView('main');
};
const renderContent = () => {
switch (view) {
case 'main':
return (
<div className="flex-1 bg-[#303040] p-0">
<div className="space-y-0">
{currentProject.operators.map((operator, index) => (
<div
key={operator.id}
className={`h-[50px] p-4 shadow-lg flex items-center cursor-pointer ${index === useDisplayStore.getState().selectedIndexes.operatorIndex ? 'bg-blue-500' : 'bg-gradient-to-b from-[#e6e3e6] to-[#7b797b]'}`}
onClick={() => {
setSelectedIndexes({ operatorIndex: index });
goBack();
}}
>
<div className="text-black text-sm">{operator.name}</div>
</div>
))}
</div>
</div>
);
case 'delete':
return (
<div className="flex-1 bg-[#303040] p-0">
<div className="space-y-0">
{currentProject.operators.map((operator) => (
<div
key={operator.id}
className="h-[50px] p-4 shadow-lg flex items-center justify-between bg-gradient-to-b from-[#e6e3e6] to-[#7b797b]"
>
<div className="text-black text-sm">{operator.name}</div>
<input
type="checkbox"
className="w-4 h-4"
checked={selectedOperators.includes(operator.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedOperators([...selectedOperators, operator.id]);
} else {
setSelectedOperators(selectedOperators.filter(id => id !== operator.id));
}
}}
/>
</div>
))}
</div>
</div>
);
case 'new':
return (
<div className="flex-1 bg-[#303040] p-4 flex flex-col">
<div className="relative">
<input
type="text"
className="w-full h-[50px] bg-[#ffffe1] rounded-sm p-4 text-black"
value={inputValue}
placeholder="请输入操作员名称"
onChange={(e) => {
setInputValue(e.target.value);
setCursorPosition(e.target.selectionStart);
}}
onClick={(e) => {
setCursorPosition(e.target.selectionStart);
}}
onFocus={(e) => {
// 保存光标位置
const cursorPosition = e.target.selectionStart;
e.target.setSelectionRange(cursorPosition, cursorPosition);
}}
/>
</div>
<Keyboard
value={inputValue}
cursorPosition={cursorPosition}
onChange={(newValue, newPosition) => {
setInputValue(newValue);
setCursorPosition(newPosition);
}}
onComplete={() => {
if (inputValue.trim()) {
const newOperator = {
id: Math.random().toString(36).substr(2, 9),
name: inputValue.trim()
};
addOperator(newOperator);
setInputValue('');
updateCurrentView('main');
}
}}
/>
</div>
);
default:
return null;
}
};
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<StatusBar />
<TitleBar
title={
view === 'main' ? "操作员" :
view === 'new' ? "新操作员" :
"删除操作员"
}
backTo={view === 'main' ? goBack : useDisplayStore.getState().navigation.previous?.name || 'home'}
view={useDisplayStore.getState().navigation.previous?.view || 'main'}
/>
{renderContent()}
{view === 'main' && (
<div className="h-[60px] bg-[#303030] flex items-center justify-between px-8">
<button
onClick={() => updateCurrentView('delete')}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
>
编辑列表
</button>
<button
onClick={() => updateCurrentView('new')}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
>
新操作员
</button>
</div>
)}
{view === 'delete' && (
<div className="h-[60px] bg-[#303030] flex items-center justify-end px-8">
<button
onClick={handleDeleteOperators}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
>
删除
</button>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,629 @@
import React, { useState } from 'react';
import Image from 'next/image';
import StatusBar from '../lib/StatusBar';
import TitleBar from '../lib/TitleBar';
import SubTitleBar from '../lib/SubTitleBar';
import Keyboard from '../lib/Keyboard';
import useDisplayStore from '@/store/displayStore';
export default function Project() {
const [inputValue, setInputValue] = useState('');
const [cursorPosition, setCursorPosition] = useState(0);
const {
getCurrentProject,
navigation,
navigateTo,
updateCurrentView,
goBack,
deleteProject,
setSelectedIndexes,
updateProject,
addProject,
setToastMessage
} = useDisplayStore();
// 获取当前项目数据
const currentProject = getCurrentProject();
const passCount = currentProject?.testResults.filter(result => {
const copperStatus = result.CopperResultStatus;
const cfpStatus = result.CFPResultStatus;
const ofpStatus = result.ofpResultStatus;
if (copperStatus) {
return copperStatus === 'pass';
} else if (cfpStatus) {
return cfpStatus === 'pass';
} else if (ofpStatus) {
return ofpStatus === 'pass';
}
return false;
}).length;
const failCount = currentProject?.testResults.filter(result => {
const copperStatus = result.CopperResultStatus;
const cfpStatus = result.CFPResultStatus;
const ofpStatus = result.ofpResultStatus;
if (copperStatus) {
return copperStatus === 'fail';
} else if (cfpStatus) {
return cfpStatus === 'fail';
} else if (ofpStatus) {
return ofpStatus === 'fail';
}
return false;
}).length;
const renderContent = () => {
const { view } = navigation.current;
switch (view) {
case 'choose':
return (
<div className="flex-1 bg-[#303040] p-4">
<div className="space-y-2">
{useDisplayStore.getState().projects.map((project, index) => (
<div
key={project.id}
className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => {
setSelectedIndexes({ projectIndex: index });
setSelectedIndexes({ testConfigIndex: 0 });
updateCurrentView('main');
}}
>
<div className="text-black text-sm">{project.name}</div>
<div className="w-3 h-3 relative">
<Image
src="/arrow.png"
alt="箭头"
fill
className="object-contain"
/>
</div>
</div>
))}
</div>
</div>
);
case 'rxtx':
return (
<div className="flex-1 bg-[#303040] p-4">
<button
className="w-full h-[200px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex items-center justify-center"
onClick={() => updateCurrentView('rxtx2')}
>
<div className="text-black text-lg">选择存储设备</div>
</button>
</div>
);
case 'rxtx2':
return (
<div className="flex-1 bg-[#303040] p-4 space-y-4">
<button className="w-full h-[100px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg">
<div className="text-black text-lg">导出</div>
</button>
<button className="w-full h-[100px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg">
<div className="text-black text-lg">导入</div>
</button>
</div>
);
case 'setup':
return (
<div className="flex-1 bg-[#303040] p-4 space-y-4">
<button
className="w-full h-[100px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg"
onClick={() => {
if (currentProject?.id === 'default') {
setToastMessage('默认项目不能重命名');
return;
}
setInputValue(currentProject?.name || '');
updateCurrentView('rename');
}}
>
<div className="text-black text-lg">重命名</div>
</button>
<button
className="w-full h-[100px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg"
onClick={() => {
if (currentProject?.id === 'default') {
setToastMessage('默认项目不能删除');
return;
}
// 删除当前项目
deleteProject(useDisplayStore.getState().selectedIndexes.projectIndex);
// 切换到默认项目索引0
setSelectedIndexes({ projectIndex: 0 });
updateCurrentView('main');
}}
>
<div className="text-black text-lg">删除</div>
</button>
</div>
);
case 'rename':
return (
<div className="flex-1 bg-[#303040] p-4 flex flex-col">
<div className="relative">
<input
type="text"
className="w-full h-[50px] bg-[#ffffe1] rounded-sm p-4 text-black mb-4"
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
setCursorPosition(e.target.selectionStart);
}}
onClick={(e) => {
setCursorPosition(e.target.selectionStart);
}}
onFocus={(e) => {
const cursorPosition = e.target.selectionStart;
e.target.setSelectionRange(cursorPosition, cursorPosition);
}}
/>
</div>
<Keyboard
value={inputValue}
cursorPosition={cursorPosition}
onChange={(newValue, newPosition) => {
setInputValue(newValue);
setCursorPosition(newPosition);
}}
onComplete={() => {
if (currentProject?.id === 'default') {
setToastMessage('默认项目不能重命名');
setInputValue('');
updateCurrentView('main');
return;
}
if (inputValue.trim()) {
const currentIndex = useDisplayStore.getState().selectedIndexes.projectIndex;
updateProject(currentIndex, { name: inputValue.trim() });
setInputValue('');
updateCurrentView('main');
}
}}
/>
</div>
);
case 'new':
return (
<div className="flex-1 bg-[#303040] p-4 flex flex-col">
<div className="relative">
<input
type="text"
className="w-full h-[50px] bg-[#ffffe1] rounded-sm p-4 text-black mb-4"
value={inputValue}
placeholder="请输入项目名称"
onChange={(e) => {
setInputValue(e.target.value);
setCursorPosition(e.target.selectionStart);
}}
onClick={(e) => {
setCursorPosition(e.target.selectionStart);
}}
onFocus={(e) => {
const cursorPosition = e.target.selectionStart;
e.target.setSelectionRange(cursorPosition, cursorPosition);
}}
/>
</div>
<Keyboard
value={inputValue}
cursorPosition={cursorPosition}
onChange={(newValue, newPosition) => {
setInputValue(newValue);
setCursorPosition(newPosition);
}}
onComplete={() => {
if (inputValue.trim()) {
// 生成唯一ID的函数
const generateId = () => Math.random().toString(36).substr(2, 9);
// 创建新项目
const newProject = {
id: generateId(),
name: inputValue.trim(),
testResults: [],
testConfigs: [{
id: generateId(),
moduleType: '8000',
modulelable: 'DSX-8000',
params: {
limitValue: 'TIA Cat 6 Channel',
cableType: 'Cat6 U/UTP',
wireOrder: 'T568B'
}
}],
cableIds: [{
id: generateId(),
name: '001'
},
{
id: generateId(),
name: '002'
},
],
operators: [{
id: generateId(),
name: 'Bob'
}]
};
// 添加新项目
addProject(newProject);
// 获取新项目的索引
const newIndex = useDisplayStore.getState().projects.length - 1;
// 更新选中索引,重置所有相关索引
setSelectedIndexes({
projectIndex: newIndex,
testConfigIndex: 0,
operatorIndex: 0,
cableIdIndex: 0
});
// 清空输入并更新视图
setInputValue('');
updateCurrentView('main');
}
}}
/>
</div>
);
case 'main':
default:
return (
<div className="flex-1 bg-[#303040] p-4 space-y-4">
{/* 操作员信息 */}
<div
className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => navigateTo('operators', 'main')}
>
<div className="text-black text-sm">操作员: {currentProject?.operators[useDisplayStore.getState().selectedIndexes.operatorIndex]?.name}</div>
<div className="w-3 h-3 relative">
<Image
src="/arrow.png"
alt="箭头"
fill
className="object-contain"
/>
</div>
</div>
{/* 结果统计 */}
<div className="space-y-1">
<div className="text-sm text-gray-400">
结果 {currentProject?.testResults[0]?.date} - {currentProject?.testResults[currentProject.testResults.length - 1]?.date}
</div>
<div
className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => navigateTo('result', 'main')}
>
<div className="flex items-center gap-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
<span className="text-green-500">{passCount|| 0}</span>
<div className="w-3 h-3 relative">
<Image
src="/pass.png"
alt="通过"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
</div>
<div className="flex items-center gap-1">
<span className="text-red-500">{failCount|| 0}</span>
<div className="w-3 h-3 relative">
<Image
src="/fail.png"
alt="失败"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
</div>
</div>
</div>
<div className="w-3 h-3 relative">
<Image
src="/arrow.png"
alt="箭头"
fill
className="object-contain"
/>
</div>
</div>
</div>
{/* 测试设置 */}
<div className="space-y-1 flex-1 overflow-hidden">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-400">测试设置</span>
<button
className="bg-[#404040] text-white px-3 py-1 rounded-sm text-sm"
onClick={() => {
// 生成唯一ID
const generateId = () => Math.random().toString(36).substr(2, 9);
// 创建新的测试配置
const newConfig = {
id: generateId(),
name: '新测试配置',
moduleType: '8000',
modulelable: 'DSX-8000',
params: {
limitValue: 'TIA Cat 6 Channel',
cableType: 'Cat6 U/UTP',
wireOrder: 'T568B',
refJumper: '1',
spliceCount: '0',
connectorCount: '2'
}
};
// 更新项目,添加新的测试配置
const currentProjectIndex = useDisplayStore.getState().selectedIndexes.projectIndex;
const updatedProject = {
...currentProject,
testConfigs: [...currentProject.testConfigs, newConfig]
};
updateProject(currentProjectIndex, updatedProject);
// 设置新配置为选中状态
const newConfigIndex = updatedProject.testConfigs.length - 1;
setSelectedIndexes({ testConfigIndex: newConfigIndex });
// 跳转到测试配置页面并进入编辑视图
navigateTo('testConfig', 'setup');
}}
>
新测试
</button>
</div>
<div className="h-[100px] overflow-y-auto space-y-2 pr-2">
{currentProject?.testConfigs.map((config, index) => (
<div
key={config.id}
className="h-[100px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg cursor-pointer"
onClick={() => navigateTo('testConfig', 'main')}
>
<div className="flex justify-between">
<div className="space-y-0.2">
{config.moduleType === '8000' && (
<>
<div className="text-black text-sm">{config.params.limitValue}</div>
<div className="text-black text-sm">{config.params.cableType}</div>
<div className="text-black text-sm">{config.params.wireOrder}</div>
</>
)}
{config.moduleType === 'cfp' && (
<>
<div className="text-black text-sm">智能远端</div>
<div className="text-black text-sm">{config.params.limitValue}</div>
<div className="text-black text-sm">{config.params.cableType}</div>
<div className="text-black text-sm">{config.params.refJumper} 跳线参照</div>
</>
)}
{config.moduleType === 'ofp' && (
<>
<div className="text-black text-sm">自动OTDR</div>
<div className="text-black text-sm">{config.params.limitValue}</div>
<div className="text-black text-sm">{config.params.cableType}</div>
</>
)}
</div>
<div className="flex flex-col items-end justify-between">
<span className="text-gray-500 text-sm">{config.modulelable}</span>
<div className="flex gap-2">
<button
className="w-6 h-6 bg-[#404040] rounded-sm flex items-center justify-center"
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡
const currentProjectIndex = useDisplayStore.getState().selectedIndexes.projectIndex;
const currentTestConfigIndex = useDisplayStore.getState().selectedIndexes.testConfigIndex;
const selectedConfigId = currentProject.testConfigs[currentTestConfigIndex]?.id;
// 禁止删除最后一项配置
if (currentProject.testConfigs.length <= 1) {
setToastMessage('不能删除最后一项配置');
return;
}
// 更新项目,删除选中的测试配置
const updatedProject = {
...currentProject,
testConfigs: currentProject.testConfigs.filter(
testConfig => testConfig.id !== config.id
)
};
updateProject(currentProjectIndex, updatedProject);
// 更新选中索引
const newIndex = updatedProject.testConfigs.findIndex(
testConfig => testConfig.id === selectedConfigId
);
// 如果删除的是当前选中的配置或者找不到选中的配置ID则设置索引为0
setSelectedIndexes({
testConfigIndex: newIndex === -1 ? 0 : newIndex
});
}}
>
<div className="w-4 h-4 relative">
<Image
src="/delete.png"
alt="删除"
fill
className="object-contain"
/>
</div>
</button>
</div>
</div>
</div>
</div>
))}
</div>
</div>
{/* 电缆ID集 */}
<div className="space-y-1">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-400">电缆ID集</span>
</div>
<div
className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => navigateTo('cableId', 'main')}
>
<div className="text-black text-sm">
下一个ID: {currentProject?.cableIds?.[0]?.name || '001'}
</div>
<div className="w-3 h-3 relative">
<Image
src="/arrow.png"
alt="箭头"
fill
className="object-contain"
/>
</div>
</div>
</div>
</div>
);
}
};
const renderTitleBar = () => {
const { view } = navigation.current;
switch (view) {
case 'choose':
return (
<>
<TitleBar title="更改项目" backTo={null} view="main" />
<SubTitleBar title="选择您要的项目" />
</>
);
case 'rxtx':
return (
<>
<TitleBar title="传输项目" backTo={null} view="main" />
<SubTitleBar title="选择存储设备" />
</>
);
case 'rxtx2':
return (
<>
<TitleBar title="传输项目" backTo={null} view="rxtx" />
<SubTitleBar title="选择所需操作" />
</>
);
case 'setup':
return (
<>
<TitleBar title="管理项目" backTo={null} view="main" />
<SubTitleBar title={currentProject?.name || 'DEFAULT'} />
</>
);
case 'rename':
return (
<>
<TitleBar title="重命名" backTo={null} view="setup" />
</>
);
case 'new':
return (
<>
<TitleBar title="新项目" backTo={null} view="choose" />
</>
);
case 'main':
default:
return (
<>
<TitleBar title="项目" backTo="home" view="main" />
<SubTitleBar title={currentProject?.name || 'DEFAULT'} />
</>
);
}
};
const renderFooter = () => {
const { view } = navigation.current;
switch (view) {
case 'choose':
return (
<div className="h-[60px] bg-[#303030] flex items-center justify-between px-8">
<button
onClick={() => updateCurrentView('new')}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
>
新项目
</button>
</div>
);
case 'rxtx':
case 'rxtx2':
case 'setup':
case 'rename':
case 'new':
return null;
case 'main':
default:
return (
<div className="h-[60px] bg-[#303030] flex items-center justify-between px-8">
<button
onClick={() => updateCurrentView('choose')}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
>
更改项目
</button>
<button
onClick={() => updateCurrentView('rxtx')}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
>
传输
</button>
<button
onClick={() => updateCurrentView('setup')}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
>
管理
</button>
</div>
);
}
};
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<StatusBar />
{renderTitleBar()}
{renderContent()}
{renderFooter()}
</div>
);
}

View File

@@ -0,0 +1,661 @@
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import StatusBar from '../lib/StatusBar';
import TitleBar from '../lib/TitleBar';
import SubTitleBar from '../lib/SubTitleBar';
import useDisplayStore from '@/store/displayStore';
import Keyboard from '../lib/Keyboard';
export default function Result() {
const { view } = useDisplayStore.getState().navigation.current;
const { getCurrentProject,setToastMessage } = useDisplayStore();
const currentProject = getCurrentProject();
const [isManageMode, setIsManageMode] = useState(false);
const [selectedResults, setSelectedResults] = useState([]);
const [inputValue, setInputValue] = useState('');
const [showKeyboard, setShowKeyboard] = useState(true);
const [cursorPosition, setCursorPosition] = useState(0);
// 获取当前项目名称
const projectName = currentProject?.name || '';
// 获取测试结果并统计通过/失败数量
const testResults = currentProject?.testResults || [];
const passCount = currentProject?.testResults.filter(result => {
const copperStatus = result.CopperResultStatus;
const cfpStatus = result.CFPResultStatus;
const ofpStatus = result.ofpResultStatus;
if (copperStatus) {
return copperStatus === 'pass';
} else if (cfpStatus) {
return cfpStatus === 'pass';
} else if (ofpStatus) {
return ofpStatus === 'pass';
}
return false;
}).length;
const failCount = currentProject?.testResults.filter(result => {
const copperStatus = result.CopperResultStatus;
const cfpStatus = result.CFPResultStatus;
const ofpStatus = result.ofpResultStatus;
if (copperStatus) {
return copperStatus === 'fail';
} else if (cfpStatus) {
return cfpStatus === 'fail';
} else if (ofpStatus) {
return ofpStatus === 'fail';
}
return false;
}).length;
// 对测试结果进行排序失败在前通过在后相同结果按name排序数字部分按数值大小排序
const sortedResults = [...testResults].sort((a, b) => {
const aPass = a.resultdata?.result === 'pass';
const bPass = b.resultdata?.result === 'pass';
// 如果通过状态相同则按name排序
if (aPass === bPass) {
// 将名称分割成文本和数字部分
const splitName = (name) => {
const match = name.match(/^(.*?)(\d+)$/);
if (match) {
return {
text: match[1],
number: parseInt(match[2], 10)
};
}
return { text: name, number: 0 };
};
const nameA = splitName(a.name);
const nameB = splitName(b.name);
// 先比较文本部分
if (nameA.text !== nameB.text) {
return nameA.text.localeCompare(nameB.text);
}
// 文本相同则比较数字部分
return nameA.number - nameB.number;
}
return aPass ? 1 : -1;
});
// 处理选择结果
const handleSelect = (result) => {
setSelectedResults(prev => {
if (prev.includes(result.name)) {
return prev.filter(name => name !== result.name);
} else {
return [...prev, result.name];
}
});
};
// 处理删除选中的结果
const handleDelete = () => {
if (selectedResults.length === 0) return;
const newResults = testResults.filter(result => !selectedResults.includes(result.name));
const updatedProject = {
...currentProject,
testResults: newResults
};
const projectIndex = useDisplayStore.getState().projects.findIndex(p => p.id === currentProject.id);
useDisplayStore.getState().updateProject(projectIndex, updatedProject);
setSelectedResults([]);
setIsManageMode(false);
setShowDeleteDialog(false);
};
// 重命名逻辑
useEffect(() => {
if (view === 'rename' && selectedResults.length === 1) {
const selectedResult = testResults.find(result => result.name === selectedResults[0]);
setInputValue(selectedResult.name);
}
}, [view, selectedResults, testResults]);
// 处理重命名完成
const handleRename = () => {
if (!inputValue.trim()) return;
// 检查是否与其他结果名称冲突
const isDuplicate = testResults.some(result =>
result.name !== selectedResults[0] && result.name === inputValue.trim()
);
if (isDuplicate) {
setToastMessage('该名称已存在,请使用其他名称');
return;
}
const newResults = testResults.map(result => {
console.log(testResults);
console.log(result);
if (result.name === selectedResults[0]) {
// 处理CFP模块类型的特殊重命名逻辑
if (result.testconfig?.moduleType === 'cfp') {
const oldName = result.name;
const newName = inputValue.trim();
// 判断旧名字是input还是output
if (oldName === result.inputname) {
// 查找关联结果inputname相同但name不同的结果
const relatedResult = testResults.find(r => r.inputname === result.inputname && r.name !== result.name);
console.log("找到的关联结果:", relatedResult);
// 如果是当前选中的结果更新name和inputname
if (result.name === selectedResults[0]) {
// 更新选中结果的name和inputname
const updatedResult = { ...result, name: newName, inputname: newName };
// 查找并更新关联结果的inputname
const relatedResult = testResults.find(r => r.inputname === result.inputname && r.name !== result.name);
if (relatedResult) {
relatedResult.inputname = newName;
}
return updatedResult;
}
return result;
} else if (oldName === result.outname) {
// 查找关联结果outname相同但name不同的结果
if (result.name === selectedResults[0]) {
// outname
const updatedResult = { ...result, name: newName, outname: newName };
// outname
const relatedResult = testResults.find(r => r.outname === result.outname && r.name !== result.name);
if (relatedResult) {
relatedResult.outname = newName;
}
return updatedResult;
}
return result;
}
}
return { ...result, name: inputValue.trim() };
}
return result;
});
const updatedProject = {
...currentProject,
testResults: newResults
};
const projectIndex = useDisplayStore.getState().projects.findIndex(p => p.id === currentProject.id);
useDisplayStore.getState().updateProject(projectIndex, updatedProject);
useDisplayStore.getState().navigateTo('result', 'main');
// 重置管理状态和选中结果状态
setIsManageMode(false);
setSelectedResults([]);
};
//确认删除提示框
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
//确认移动提示框
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [targetProject, setTargetProject] = useState(null);
const [showOverwriteDialog, setShowOverwriteDialog] = useState(false);
const [duplicateResults, setDuplicateResults] = useState([]);
const moveResults = (project, index) => {
// 检查是否选择了结果和目标项目
if (selectedResults.length === 0 || !project) return;
// 检查是否在同一个项目中移动
if (project.id === currentProject.id) {
setToastMessage('不能移动到相同项目');
return;
}
// 检查目标项目中是否有重名结果
const duplicates = selectedResults.filter(resultName =>
project.testResults.some(targetResult => targetResult.name === resultName)
);
if (duplicates.length > 0) {
setDuplicateResults(duplicates);
setTargetProject(project);
setShowOverwriteDialog(true);
} else {
setTargetProject(project);
setShowConfirmDialog(true);
}
};
const handleMove = (overwrite = false) => {
if (!targetProject) return;
// 获取要移动的结果对象
const resultsToMove = testResults.filter(result => selectedResults.includes(result.name));
// 更新目标项目
const updatedTargetResults = overwrite
? [...targetProject.testResults.filter(result => !selectedResults.includes(result.name)), ...resultsToMove]
: [...targetProject.testResults, ...resultsToMove];
const updatedTargetProject = {
...targetProject,
testResults: updatedTargetResults
};
// 更新当前项目
const updatedCurrentResults = testResults.filter(result => !selectedResults.includes(result.name));
const updatedCurrentProject = {
...currentProject,
testResults: updatedCurrentResults
};
// 保存更改
const targetIndex = useDisplayStore.getState().projects.findIndex(p => p.id === targetProject.id);
const currentIndex = useDisplayStore.getState().projects.findIndex(p => p.id === currentProject.id);
useDisplayStore.getState().updateProject(targetIndex, updatedTargetProject);
useDisplayStore.getState().updateProject(currentIndex, updatedCurrentProject);
// 重置状态
setSelectedResults([]);
setIsManageMode(false);
setShowConfirmDialog(false);
setShowOverwriteDialog(false);
setTargetProject(null);
setDuplicateResults([]);
// 显示成功消息并返回主视图
setToastMessage('移动成功');
useDisplayStore.getState().navigateTo('result', 'main');
};
// 渲染标题栏
const renderTitleBar = () => {
const { view } = useDisplayStore.getState().navigation.current;
switch (view) {
case 'rename':
return (
<TitleBar
title="重命名结果"
backTo={useDisplayStore.getState().navigation.previous?.name || 'home'}
view={useDisplayStore.getState().navigation.previous?.view || 'main'}
/>
);
case 'move':
return (
<TitleBar
title="移至"
backTo={useDisplayStore.getState().navigation.previous?.name || 'home'}
view={useDisplayStore.getState().navigation.previous?.view || 'main'}
/>
);
case 'main':
default:
return (
<TitleBar
title="结果"
backTo={'home'}
view={'main'}
/>
);
}
};
// 渲染主要内容
const renderContent = () => {
const { view } = useDisplayStore.getState().navigation.current;
switch (view) {
case 'rename':
return (
<div className="flex-1 bg-[#303040] p-4 flex flex-col">
<div
className="relative mb-4 cursor-pointer"
onClick={() => setShowKeyboard(true)}
>
<input
type="text"
className="w-full h-[50px] bg-[#ffffe1] rounded-sm p-4 text-black"
value={inputValue}
placeholder="请输入测试结果名称"
onChange={(e) => {
setInputValue(e.target.value);
setCursorPosition(e.target.selectionStart);
}}
onClick={(e) => {
setShowKeyboard(true);
setCursorPosition(e.target.selectionStart);
}}
onFocus={(e) => {
const cursorPosition = e.target.selectionStart;
e.target.setSelectionRange(cursorPosition, cursorPosition);
}}
/>
</div>
{showKeyboard && (
<Keyboard
value={inputValue}
cursorPosition={cursorPosition}
onChange={(newValue, newPosition) => {
setInputValue(newValue);
setCursorPosition(newPosition);
}}
onComplete={() => {
setShowKeyboard(false);
}}
/>
)}
</div>
);
case 'move':
return (
<div className="flex-1 bg-[#303040] flex flex-col relative">
<SubTitleBar title="选择您要的项目" />
<div className="space-y-2 p-4">
{useDisplayStore.getState().projects.map((project, index) => (
<div
key={project.id}
className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => {
moveResults(project, index);
}}
>
<div className="text-black text-sm">{project.name}</div>
<div className="w-3 h-3 relative">
<Image
src="/arrow.png"
alt="箭头"
fill
className="object-contain"
/>
</div>
</div>
))}
</div>
</div>
);
case 'main':
default:
return (
<>
<div className="h-[60px] bg-gradient-to-b from-[#b0b0b0] via-[#e0e4e0] to-[#b0b0b0] px-4 flex flex-col justify-center">
<div className="text-black">{projectName}</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
<span className="text-green-500">{passCount}</span>
<div className="w-3 h-3 relative">
<Image
src="/pass.png"
alt="通过"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
</div>
<div className="flex items-center gap-1">
<span className="text-red-500">{failCount}</span>
<div className="w-3 h-3 relative">
<Image
src="/fail.png"
alt="失败"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
</div>
</div>
</div>
{isManageMode && (
<div className="h-[40px] px-4 bg-[#efebe6] flex items-center border-b border-gray-600">
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={selectedResults.length === sortedResults.length}
onChange={() => {
if (selectedResults.length === sortedResults.length) {
setSelectedResults([]);
} else {
setSelectedResults(sortedResults.map(result => result.name));
}
}}
className="w-4 h-4"
/>
<span className="text-black">选择全部</span>
</div>
</div>
)}
<div className="flex-1 bg-[#303040] overflow-y-auto" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
<div className="space-y-0">
{sortedResults.map((result, index) => (
<div
key={index}
className="h-[60px] px-4 bg-[#efebe6] flex items-center justify-between border-b border-gray-600 cursor-pointer"
onClick={() => !isManageMode && useDisplayStore.getState().navigateTo('resultinfo', 'save', result)}
>
<div className="flex items-center gap-4">
{isManageMode && (
<input
type="checkbox"
checked={selectedResults.includes(result.name)}
onChange={() => handleSelect(result)}
className="w-4 h-4"
/>
)}
<span className="text-black">{result.name}</span>
</div>
<img
src={
result.CopperResultStatus === 'pass' ||
result.ofpResultStatus === 'pass' ||
(result.name === result.inputname && result.InPortStatus === 'pass') ||
(result.name === result.outname && result.OutPortStatus === 'pass')
? '/pass.png'
: '/fail.png'
}
alt={
result.CopperResultStatus === 'pass' ||
result.ofpResultStatus === 'pass' ||
(result.name === result.inputname && result.InPortStatus === 'pass') ||
(result.name === result.outname && result.OutPortStatus === 'pass')
? '通过'
: '失败'
}
className="w-7 h-7"
/>
</div>
))}
</div>
</div>
</>
);
}
};
// 渲染底部按钮
const renderFooter = () => {
const { view } = useDisplayStore.getState().navigation.current;
switch (view) {
case 'rename':
return (
<div className="h-[60px] bg-[#303030] flex items-center justify-end px-8">
{!showKeyboard && (
<button
onClick={handleRename}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
>
完成
</button>
)}
</div>
);
case 'move':
return(
<div className="h-[60px] bg-[#303030] flex items-center justify-between px-5">
</div>
);
case 'main':
default:
return (
<div className="h-[60px] bg-[#303030] flex items-center justify-between px-5">
<div className="flex-1">
<button
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
onClick={() => {
if (selectedResults.length > 0) {
useDisplayStore.getState().navigateTo('result', 'move');
}
}}
disabled={selectedResults.length === 0}
style={{ opacity: selectedResults.length > 0 ? 1 : 0.5 }}
>
移动
</button>
</div>
<div className="flex-1 flex justify-center">
<button
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
onClick={() => {
if (selectedResults.length === 1) {
const selectedResult = testResults.find(result => result.name === selectedResults[0]);
useDisplayStore.getState().navigateTo('result', 'rename', selectedResult);
}
}}
disabled={selectedResults.length !== 1}
style={{ opacity: selectedResults.length === 1 ? 1 : 0.5 }}
>
重命名
</button>
</div>
<div className="flex-1 flex justify-end">
<button
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
onClick={() => isManageMode ? setShowDeleteDialog(true) : setIsManageMode(true)}
>
{isManageMode ? '删除' : '管理'}
</button>
</div>
</div>
);
}
};
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<StatusBar />
{renderTitleBar()}
{renderContent()}
{renderFooter()}
{/* 确认删除对话框 */}
{showDeleteDialog && (
<div className="w-[480px] h-[640px] bg-[#002842d4] absolute z-9999">
<div className='pl-10 pt-60'>
<div className="bg-[#2B3C5B] rounded-lg p-6 w-[400px] min-h-[200px] flex flex-col">
<h3 className="text-white text-xl font-bold mb-4">提示</h3>
<div className="flex-1 flex items-center justify-center">
<div className="text-white text-lg">是否删除选中的测试结果</div>
</div>
<div className="flex justify-center mt-4">
<div className="flex justify-center gap-10">
<button
className="text-white text-s font-bold px-4 py-2 bg-[#1d3155] rounded-md hover:bg-[#2b4466]"
onClick={() => setShowDeleteDialog(false)}
>
取消
</button>
<button
className="text-white text-s font-bold px-4 py-2 bg-[#1d3155] rounded-md hover:bg-[#2b4466]"
onClick={() => handleDelete()}
>
确认
</button>
</div>
</div>
</div>
</div>
</div>
)}
{/* 确认移动对话框 */}
{showConfirmDialog && (
<div className="w-[480px] h-[640px] bg-[#002842d4] absolute z-9999">
<div className='pl-10 pt-60'>
<div className="bg-[#2B3C5B] rounded-lg p-6 w-[400px] min-h-[200px] flex flex-col">
<h3 className="text-white text-xl font-bold mb-4">提示</h3>
<div className="flex-1 flex items-center justify-center">
<div className="text-white text-lg">是否将选中的测试结果移动到项目 {targetProject?.name}</div>
</div>
<div className="flex justify-center mt-4">
<div className="flex justify-center gap-10">
<button
className=" text-white text-s font-bold px-4 py-2 bg-[#1d3155] rounded-md hover:bg-[#2b4466]"
onClick={() => setShowConfirmDialog(false)}
>
取消
</button>
<button
className=" text-white text-s font-bold px-4 py-2 bg-[#1d3155] rounded-md hover:bg-[#2b4466]"
onClick={() => handleMove(false)}
>
确认
</button>
</div>
</div>
</div>
</div>
</div>
)}
{/* 重名覆盖确认对话框 */}
{showOverwriteDialog && (
<div className="w-[480px] h-[640px] bg-[#002842d4] absolute z-9999">
<div className='pl-10 pt-60'>
<div className="bg-[#2B3C5B] rounded-lg p-6 w-[400px] min-h-[200px] flex flex-col">
<h3 className="text-white text-xl font-bold mb-4">提示</h3>
<div className="flex-1 flex flex-col items-center justify-center">
<div className="text-white text-lg mb-4">以下测试结果在目标项目中已存在</div>
<ul className="mb-4 list-disc pl-6">
{duplicateResults.map((name, index) => (
<li key={index} className="text-red-500">{name}</li>
))}
</ul>
<div className="text-white text-lg">是否覆盖这些结果</div>
</div>
<div className="flex justify-center mt-4">
<div className="flex justify-center gap-10">
<button
className="text-white text-s font-bold px-4 py-2 bg-[#1d3155] rounded-md hover:bg-[#2b4466]"
onClick={() => {
setShowOverwriteDialog(false);
setTargetProject(null);
setDuplicateResults([]);
}}
>
取消
</button>
<button
className="text-white text-s font-bold px-4 py-2 bg-[#1d3155] rounded-md hover:bg-[#2b4466]"
onClick={() => handleMove(true)}
>
覆盖
</button>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,658 @@
import React, { useState, useEffect } from 'react';
import StatusBar from '../lib/StatusBar';
import ResultTitleBar from '../lib/ResultTitleBar';
import TitleBar from '../lib/TitleBar';
import CopperResultMain from '../lib/CopperResultMain';
import OLTSResultMain from '../lib/OLTSResultMain';
import OTDRResultMain from '../lib/OTDRResultMain';
import Keyboard from '../lib/Keyboard';
import useDisplayStore from '@/store/displayStore';
// 初始化测试结果音效对象
const testPassSound = typeof Audio !== 'undefined' ? new Audio('/sounds/test_pass.wav') : null;
const testFailSound = typeof Audio !== 'undefined' ? new Audio('/sounds/test_fail.wav') : null;
// 自定义确认弹窗组件
const ConfirmDialog = ({ message, onConfirm, onCancel }) => (
<div className="w-[480px] h-[640px] bg-[#002842d4] absolute z-[9999] top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<div className='pl-10 pt-60'>
<div className="bg-[#2B3C5B] rounded-lg p-6 w-[400px] min-h-[200px] flex flex-col">
<h3 className="text-white text-xl font-bold mb-4">提示</h3>
<div className="flex-1 flex items-center justify-center">
<div className="text-white text-lg">{message}</div>
</div>
<div className="flex justify-center gap-4 mt-4">
<button
onClick={onConfirm}
className="bg-[#354e7a] text-white px-6 py-2 rounded hover:bg-[#1E293B] transition-colors"
>
确定
</button>
<button
onClick={onCancel}
className="bg-[#4a4a4a] text-white px-6 py-2 rounded hover:bg-[#3a3a3a] transition-colors"
>
取消
</button>
</div>
</div>
</div>
</div>
);
export default function ResultInfo() {
const [showKeyboard, setShowKeyboard] = useState(true);
const [cursorPosition, setCursorPosition] = useState(0);
const [inputValue, setInputValue] = useState('');
const [inputValue2, setInputValue2] = useState('');
const [activeInput, setActiveInput] = useState(1); // 1 表示第一个输入框2 表示第二个输入框
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [confirmDialogMessage, setConfirmDialogMessage] = useState('');
const [confirmDialogCallback, setConfirmDialogCallback] = useState(null);
const { navigation } = useDisplayStore.getState();
const tempTestResult = navigation.current.params;
// 根据测试结果播放音效
const { hasPlayedSound } = useDisplayStore.getState();
useEffect(() => {
// 只有从测试页面进入nosave视图时才播放声音
if (navigation.previous.name === 'testing' &&
tempTestResult &&
!hasPlayedSound) {
if (tempTestResult.CopperResultStatus === 'pass'||tempTestResult.CFPResultStatus === 'pass' ||tempTestResult.ofpResultStatus === 'pass') {
testPassSound?.play().catch(console.error);
} else {
testFailSound?.play().catch(console.error);
}
useDisplayStore.setState({ hasPlayedSound: true });
}
}, [navigation, tempTestResult, hasPlayedSound]);
// 从URL参数中获取临时测试结果
const {
getCurrentProject,
getCurrentCableId,
getCurrentCableId2,
updateCurrentView,
getCurrentTestConfig,
navigateTo,
setToastMessage,
updateProject
} = useDisplayStore();
const currentProject = getCurrentProject();
const { view } = useDisplayStore.getState().navigation.current;
// 获取当前的线缆ID
const currentCableId = getCurrentCableId().name || '';
const currentCableId2 = getCurrentCableId2().name || '';
// 计算下一个序号的ID
const getNextId = (currentId) => {
if (!currentId) return '';
// 检查是否以数字结尾
const numMatch = currentId.match(/^(.*?)(\d+)$/);
if (numMatch) {
const [, prefix, num] = numMatch;
const nextNum = String(Number(num) + 1).padStart(num.length, '0');
return prefix + nextNum;
}
// 检查是否以字母结尾
const letterMatch = currentId.match(/^(.*?)([a-zA-Z]+)$/);
if (letterMatch) {
const [, prefix, letters] = letterMatch;
// 将字母转换为数组以便处理
const letterArray = letters.split('');
let carry = true;
// 从右向左处理每个字母
for (let i = letterArray.length - 1; i >= 0 && carry; i--) {
if (letterArray[i] === 'z') {
letterArray[i] = 'a';
carry = true;
} else if (letterArray[i] === 'Z') {
letterArray[i] = 'A';
carry = true;
} else {
letterArray[i] = String.fromCharCode(letterArray[i].charCodeAt(0) + 1);
carry = false;
}
}
// 如果还有进位,说明需要在前面添加一个字母
if (carry) {
if (letters[0] >= 'a' && letters[0] <= 'z') {
letterArray.unshift('a');
} else {
letterArray.unshift('A');
}
}
return prefix + letterArray.join('');
}
// 如果既不是数字也不是字母结尾,直接返回原值
return currentId;
};
// 初始化输入值为当前的线缆ID
useEffect(() => {
setInputValue(currentCableId);
setInputValue2(currentCableId2);
}, [currentCableId, currentCableId2]);
// 当第一个输入框值变化时,仅在用户手动输入时自动更新第二个输入框
useEffect(() => {
if (tempTestResult?.testconfig?.moduleType === 'cfp' && inputValue && inputValue !== currentCableId) {
setInputValue2(getNextId(inputValue));
}
}, [inputValue, view, currentCableId]);
//创建保存结果存储逻辑
const handleComplete = () => {
const currentId = inputValue.trim();
// 检查是否存在重名的测试结果
const currentProject = getCurrentProject();
const existingResults = currentProject?.testResults || [];
// 检查两个ID是否重复
const isDuplicate = existingResults.some(result => result.name === currentId);
// 重名替换
if (isDuplicate) {
setConfirmDialogMessage('该线缆ID已使用是否覆盖');
setConfirmDialogCallback(() => () => {
// 更新临时测试结果的名称
const updatedTestResult = {
...tempTestResult,
name: currentId
};
// 更新当前项目的测试结果
const currentIndex = useDisplayStore.getState().projects.findIndex(p => p === currentProject);
if (currentIndex !== -1) {
// 移除旧的测试结果并添加新的
const newResults = existingResults.filter(result => result.name !== currentId);
updateProject(currentIndex, {
testResults: [...newResults, updatedTestResult]
});
// 更新navigation.current.params中的测试结果名称
useDisplayStore.setState({
navigation: {
...navigation,
current: {
...navigation.current,
params: updatedTestResult
}
}
});
}
updateCurrentView('save');
setShowConfirmDialog(false);
});
setShowConfirmDialog(true);
return;
}
// 更新临时测试结果的名称并保存到项目中
if (tempTestResult) {
const updatedTestResult = {
...tempTestResult,
name: currentId
};
// 更新当前项目的cableIds.name为下一个ID
const currentIndex = useDisplayStore.getState().projects.findIndex(p => p === currentProject);
if (currentIndex !== -1) {
//更新测试结果
updateProject(currentIndex, {
testResults: [...(currentProject.testResults || []), updatedTestResult]
});
// 更新navigation.current.params中的测试结果名称
const navigation = useDisplayStore.getState().navigation;
useDisplayStore.setState({
navigation: {
...navigation,
current: {
...navigation.current,
params: updatedTestResult
}
}
});
// 获取下一个ID
const nextId = (() => {
const currentId = inputValue.trim();
if (!currentId) return currentId;
// 获取最后一个字符
const lastChar = currentId.slice(-1);
const prefix = currentId.slice(0, -1);
// 如果最后一个字符是数字
if (/\d/.test(lastChar)) {
const match = currentId.match(/^(.*?)(\d+)$/);
if (match) {
const numPrefix = match[1];
const number = parseInt(match[2]) + 1;
return `${numPrefix}${number.toString().padStart(match[2].length, '0')}`;
}
}
// 如果最后一个字符是字母
if (/[A-Za-z]/.test(lastChar)) {
const nextChar = String.fromCharCode(lastChar.charCodeAt(0) + 1);
// 如果超过Z或z回到A或a
if ((lastChar === 'Z' && nextChar > 'Z') || (lastChar === 'z' && nextChar > 'z')) {
const baseChar = lastChar === 'Z' ? 'A' : 'a';
return `${prefix}${baseChar}`;
}
return `${prefix}${nextChar}`;
}
return currentId;
})();
// 获取当前项目的所有cableIds
const currentCableIds = currentProject?.cableIds || [];
const selectedId = getCurrentCableId().id;
// 只更新选中的ID保留其他ID不变
const updatedCableIds = currentCableIds.map(cable =>
cable.id === selectedId ? { ...cable, name: nextId } : cable
);
// 更新项目
updateProject(currentIndex, {
cableIds: updatedCableIds
});
}
updateCurrentView('save');
}
}
const handleComplete2 = () => {
const currentId = inputValue.trim();
const currentId2 = inputValue2.trim();
// 检查两个ID是否相同
if (currentId === currentId2) {
setConfirmDialogMessage('输入输出ID不能相同请检查');
setConfirmDialogCallback(() => () => {
setShowConfirmDialog(false);
});
setShowConfirmDialog(true);
return;
}
// 检查是否存在重名的测试结果
const currentProject = getCurrentProject();
const existingResults = currentProject?.testResults || [];
// 检查两个ID是否重复
const isDuplicate1 = existingResults.some(result => result.name === currentId);
const isDuplicate2 = existingResults.some(result => result.name === currentId2);
const currentConfig = getCurrentTestConfig();
const cableType = currentConfig.params.cableType;
const isMultiMode = cableType.includes('OM');
if (isDuplicate1 || isDuplicate2) {
const message = [];
if (isDuplicate1) message.push(`线缆ID ${currentId}`);
if (isDuplicate2) message.push(`线缆ID ${currentId2}`);
setConfirmDialogMessage(`${message.join(' 和 ')}已使用,是否覆盖?`);
setConfirmDialogCallback(() => () => {
// 更新临时测试结果的名称
const updatedTestResult1 = {
...tempTestResult,
name: currentId,
inputname: isMultiMode ? currentId2 : currentId,
outname: isMultiMode ? currentId : currentId2,
};
const updatedTestResult2 = {
...tempTestResult,
name: currentId2,
inputname: isMultiMode ? currentId2 : currentId,
outname: isMultiMode ? currentId : currentId2,
};
// 更新当前项目的测试结果
const currentIndex = useDisplayStore.getState().projects.findIndex(p => p === currentProject);
if (currentIndex !== -1) {
// 移除旧的测试结果并添加新的
const newResults = existingResults.filter(result =>
result.name !== currentId && result.name !== currentId2
);
updateProject(currentIndex, {
testResults: [...newResults, updatedTestResult1, updatedTestResult2]
});
// 更新navigation.current.params中的测试结果名称
useDisplayStore.setState({
navigation: {
...navigation,
current: {
...navigation.current,
params: updatedTestResult1
}
}
});
}
updateCurrentView('save');
setShowConfirmDialog(false);
});
setShowConfirmDialog(true);
return;
}
// 更新临时测试结果的名称并保存到项目中
if (tempTestResult) {
// 创建两个测试结果
const updatedTestResult1 = {
...tempTestResult,
name: currentId,
inputname: isMultiMode ? currentId2 : currentId,
outname: isMultiMode ? currentId : currentId2,
};
const updatedTestResult2 = {
...tempTestResult,
name: currentId2,
inputname: isMultiMode ? currentId2 : currentId,
outname: isMultiMode ? currentId : currentId2,
};
// 更新当前项目的cableIds.name为下一个ID
const currentIndex = useDisplayStore.getState().projects.findIndex(p => p === currentProject);
if (currentIndex !== -1) {
//更新测试结果
updateProject(currentIndex, {
testResults: [...(currentProject.testResults || []), updatedTestResult1,updatedTestResult2]
});
// 更新navigation.current.params中的测试结果名称
const navigation = useDisplayStore.getState().navigation;
useDisplayStore.setState({
navigation: {
...navigation,
current: {
...navigation.current,
params: updatedTestResult1
}
}
});
// 获取下一个ID
const nextId = getNextId(getNextId(inputValue.trim()));
// 获取下一个ID2
const nextId2 = getNextId(getNextId(inputValue2.trim()));
// 获取当前项目的所有cableIds
const currentCableIds = currentProject?.cableIds || [];
const selectedId = getCurrentCableId().id;
const selectedId2 = getCurrentCableId2().id;
// 只更新选中的ID保留其他ID不变
const updatedCableIds = currentCableIds.map(cable =>
cable.id === selectedId ? { ...cable, name: nextId } :
cable.id === selectedId2 ? { ...cable, name: nextId2 } :
cable
);
// 更新项目,添加两个测试结果
updateProject(currentIndex, {
cableIds: updatedCableIds,
});
}
updateCurrentView('save');
}
}
// 创建测试结果的视图
const renderContent = () => {
const renderResultMain = () => {
const moduleType = tempTestResult?.testconfig?.moduleType;
switch (moduleType) {
case '8000':
return <CopperResultMain testResult={tempTestResult} />;
case 'cfp':
return <OLTSResultMain testResult={tempTestResult} />;
case 'ofp':
return <OTDRResultMain testResult={tempTestResult} />;
default:
return <CopperResultMain testResult={tempTestResult} />;
}
};
const renderSetName = () => {
const moduleType = tempTestResult?.testconfig?.moduleType;
switch (moduleType) {
case 'cfp':
return(
<div className="flex-1 bg-[#303040] p-4 flex flex-col">
<div className="mb-8">
<div className="mb-1 text-white text-sm">输出光纤ID1</div>
<div className="relative cursor-pointer">
<input
type="text"
className="w-full h-[50px] bg-[#ffffe1] rounded-sm px-4 text-black overflow-x-auto whitespace-nowrap"
value={inputValue}
placeholder="请输入线缆ID1"
onChange={(e) => {
setInputValue(e.target.value);
setCursorPosition(e.target.selectionStart);
}}
onClick={(e) => {
setActiveInput(1);
setShowKeyboard(true);
setCursorPosition(e.target.selectionStart);
}}
onFocus={(e) => {
const cursorPosition = e.target.selectionStart;
e.target.setSelectionRange(cursorPosition, cursorPosition);
}}
/>
</div>
</div>
<div>
<div className="mb-1 text-white text-sm">输入光纤ID2</div>
<div className="relative cursor-pointer">
<input
type="text"
className="w-full h-[50px] bg-[#ffffe1] rounded-sm px-4 text-black overflow-x-auto whitespace-nowrap"
value={inputValue2}
placeholder="请输入线缆ID2"
onChange={(e) => {
setInputValue2(e.target.value);
setCursorPosition(e.target.selectionStart);
}}
onClick={(e) => {
setActiveInput(2);
setShowKeyboard(true);
setCursorPosition(e.target.selectionStart);
}}
onFocus={(e) => {
const cursorPosition = e.target.selectionStart;
e.target.setSelectionRange(cursorPosition, cursorPosition);
}}
/>
</div>
</div>
{showKeyboard && (
<Keyboard
value={activeInput === 1 ? inputValue : inputValue2}
cursorPosition={cursorPosition}
onChange={(newValue, newPosition) => {
if (activeInput === 1) {
setInputValue(newValue);
} else {
setInputValue2(newValue);
}
setCursorPosition(newPosition);
}}
onComplete={() => {
setShowKeyboard(false);
}}
/>
)}
</div>
);
default:
return (
<div className="flex-1 bg-[#303040] p-4 flex flex-col">
<div
className="relative mb-4 cursor-pointer"
onClick={() => setShowKeyboard(true)}
>
<input
type="text"
className="w-full h-[50px] bg-[#ffffe1] rounded-sm p-4 text-black"
value={inputValue}
placeholder="请输入线缆ID"
onChange={(e) => {
setInputValue(e.target.value);
setCursorPosition(e.target.selectionStart);
}}
onClick={(e) => {
setShowKeyboard(true);
setCursorPosition(e.target.selectionStart);
}}
onFocus={(e) => {
// 保存光标位置
const cursorPosition = e.target.selectionStart;
e.target.setSelectionRange(cursorPosition, cursorPosition);
}}
/>
</div>
{showKeyboard && (
<Keyboard
value={inputValue}
cursorPosition={cursorPosition}
onChange={(newValue, newPosition) => {
setInputValue(newValue);
setCursorPosition(newPosition);
}}
onComplete={() => {
setShowKeyboard(false);
}}
/>
)}
</div>
);
}
};
switch (view) {
case 'nosave':
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<StatusBar />
{tempTestResult?.testconfig?.moduleType !== 'cfp' ? (
<ResultTitleBar title="未保存结果" testResult={tempTestResult} backTo="home" view="main" />
) : (<ResultTitleBar title=" " testResult={tempTestResult} backTo="home" view="main" />
)}
{renderResultMain()}
<div className="h-[60px] bg-[#303030] flex items-center justify-end px-4">
<button
onClick={() => updateCurrentView('setname')}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#ffd773] to-[#e7aa29] rounded-sm flex items-center justify-center text-black font-bold shadow-lg"
>
保存
</button>
</div>
</div>
);
case 'setname':
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<StatusBar />
<TitleBar
title="保存结果"
backTo={useDisplayStore.getState().navigation.previous?.name || 'home'}
view={useDisplayStore.getState().navigation.previous?.view || 'main'}
/>
<div className="flex-1 bg-[#303040] p-2 flex flex-col">
{renderSetName()}
</div>
<div className="h-[60px] bg-[#303030] flex items-center justify-end px-4">
{tempTestResult?.testconfig?.moduleType === "cfp" ? (
!showKeyboard && (
<button
onClick={() => handleComplete2()}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#ffd773] to-[#e7aa29] rounded-sm flex items-center justify-center text-black font-bold shadow-lg"
>
保存
</button>
)
) : (
!showKeyboard && (<button
onClick={() => handleComplete()}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#ffd773] to-[#e7aa29] rounded-sm flex items-center justify-center text-black font-bold shadow-lg"
>
保存
</button>
)
)}
</div>
</div>
);
case 'save':
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<StatusBar />
{tempTestResult?.testconfig?.moduleType !== 'cfp' ? (
<ResultTitleBar testResult={tempTestResult} backTo="result" view="main" />
) : (<ResultTitleBar title=" " testResult={tempTestResult} backTo="result" view="main" />
)}
{renderResultMain()}
<div className="h-[60px] bg-[#303030] flex items-center justify-end px-4">
<button
onClick={() => {
navigateTo('home', 'main');
}}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#ffd773] to-[#e7aa29] rounded-sm flex items-center justify-center text-black font-bold shadow-lg"
>
主页
</button>
</div>
</div>
);
default:
return null;
}
};
const content = renderContent();
return (
<div className="relative w-full h-full">
{content}
{showConfirmDialog && (
<ConfirmDialog
message={confirmDialogMessage}
onConfirm={confirmDialogCallback}
onCancel={() => setShowConfirmDialog(false)}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,660 @@
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import StatusBar from '../lib/StatusBar';
import TitleBar from '../lib/TitleBar';
import SubTitleBar from '../lib/SubTitleBar';
import useDisplayStore from '@/store/displayStore';
import { v4 as uuidv4 } from 'uuid';
import MenuList from './MenuList.js';
export default function TestConfig() {
const [selectedConfigId, setSelectedConfigId] = useState(null);
const {
getCurrentProject,
navigation,
navigateTo,
updateCurrentView,
goBack,
setSelectedIndexes,
updateProject,
addProject,
setToastMessage
} = useDisplayStore();
// 获取当前项目数据
const currentProject = getCurrentProject();
// 单多模判断
const { getCurrentTestConfig } = useDisplayStore.getState();
const currentConfig = getCurrentTestConfig();
const cableType = currentConfig.params.cableType;
const isMultiMode = cableType.includes('OM');
// 设置默认选中的配置
useEffect(() => {
// 优先使用导航参数中的配置ID
const configIdFromParams = navigation.current.params?.selectedConfigId;
if (configIdFromParams) {
setSelectedConfigId(configIdFromParams);
return;
}
// 如果没有参数,使用当前选中的配置索引
const currentTestConfigIndex = useDisplayStore.getState().selectedIndexes.testConfigIndex;
if (currentProject?.testConfigs && currentTestConfigIndex >= 0) {
setSelectedConfigId(currentProject.testConfigs[currentTestConfigIndex].id);
}
// 确保导航历史正确记录
if (navigation.current.name === 'testconfig' && navigation.current.view === 'main') {
const current = navigation.current;
const previous = navigation.previous;
if (!previous || previous.name !== 'testconfig' || previous.view !== 'main') {
useDisplayStore.getState().navigateTo('testconfig', 'main', current.params);
}
}
}, [currentProject, navigation.current.params]);
// 渲染主视图内容
const renderMainContent = () => {
return (
<div className="h-[490px] flex-1 bg-[#303040] p-4">
<div className="h-full overflow-y-auto space-y-2 pr-2">
{currentProject?.testConfigs.map((config) => (
<div
key={config.id}
className="h-[100px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex items-center gap-4 cursor-pointer"
onClick={() => {
setSelectedConfigId(config.id);
// 保存选中的配置ID到导航参数
updateCurrentView('main', { selectedConfigId: config.id });
}}
>
<div className="w-6 h-6 border-2 border-gray-600 rounded-full flex items-center justify-center">
{selectedConfigId === config.id && (
<div className="w-4 h-4 bg-blue-500 rounded-full" />
)}
</div>
<div className="flex-1 relative">
<div className="mt-1">
{config.moduleType === '8000' && (
<>
<div className="text-black text-sm">{config.params.limitValue}</div>
<div className="text-black text-sm">{config.params.cableType}</div>
<div className="text-black text-sm">{config.params.wireOrder}</div>
</>
)}
{config.moduleType === 'cfp' && (
<>
<div className="text-black text-sm">智能远端</div>
<div className="text-black text-sm">{config.params.limitValue}</div>
<div className="text-black text-sm">{config.params.cableType}</div>
<div className="text-black text-sm">{config.params.refJumper} 跳线参照</div>
</>
)}
{config.moduleType === 'ofp' && (
<>
<div className="text-black text-sm">自动OTDR</div>
<div className="text-black text-sm">{config.params.limitValue}</div>
<div className="text-black text-sm">{config.params.cableType}</div>
</>
)}
</div>
<div className="text-gray-400 text-sm absolute top-0 right-0">{config.modulelable}</div>
</div>
<div className="w-3 h-3 relative">
<Image
src="/arrow.png"
alt="箭头"
fill
className="object-contain"
/>
</div>
</div>
))}
</div>
</div>
);
};
// 渲染设置视图内容
const renderSetupContent = () => {
const currentConfig = currentProject?.testConfigs.find(
config => config.id === selectedConfigId
);
if (!currentConfig) return null;
const renderConfigContent = () => {
switch (currentConfig.moduleType) {
case '8000':
return (
<div className="space-y-2">
<div className="space-y-0">
<div
className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-t-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => navigateTo('menulist', 'setup', {
menuType: 'CABLE_TYPE',
selectedConfigId: selectedConfigId,
backTo: 'testconfig',
backView: 'setup'
})}
>
<div className="text-black text-l font-bold">电缆类型{currentConfig.params.cableType}</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
<div className="h-[50px] bg-gradient-to-b from-[#ffffff] to-[#ffffff] rounded-b-sm p-4 shadow-lg flex justify-between items-center">
<div className="text-black text-l">NVP根据电缆类型生成</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
</div>
<div className="space-y-0">
<div
className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-t-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => navigateTo('menulist', 'setup', {
menuType: 'LIMIT_VALUE',
selectedConfigId: selectedConfigId,
backTo: 'testconfig',
backView: 'setup'
})}
>
<div className="text-black text-l font-bold">测试极限值{currentConfig.params.limitValue}</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
<div className="h-[50px] bg-gradient-to-b from-[#ffffff] to-[#ffffff] p-4 shadow-lg flex justify-between items-center">
<div className="text-black text-l">存储绘图数据</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
<div className="h-[50px] bg-gradient-to-b from-[#ffffff] to-[#ffffff] rounded-b-sm p-4 shadow-lg flex justify-between items-center">
<div className="text-black text-l">HDTDR/HDTDX仅限失败/通过</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
</div>
<div
className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => navigateTo('menulist', 'setup', {
menuType: 'WIRE_ORDER',
selectedConfigId: selectedConfigId,
backTo: 'testconfig',
backView: 'setup'
})}
>
<div className="text-black text-l font-bold">插座配置{currentConfig.params.wireOrder}</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
</div>
);
case 'cfp':
return (
<div className="space-y-4">
<div className="space-y-0">
<div className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-t-sm p-4 shadow-lg flex justify-between items-center">
<div className="text-black text-l font-bold">测试类型智能远端</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
<div className="h-[50px] bg-gradient-to-b from-[#ffffff] to-[#ffffff] rounded-b-sm p-4 shadow-lg flex justify-between items-center">
<div className="text-black text-l">双向</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
</div>
<div
className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => navigateTo('menulist', 'setup', {
menuType: 'FIBER_TYPE',
selectedConfigId: selectedConfigId,
backTo: 'testconfig',
backView: 'setup'
})}
>
<div className="text-black text-l font-bold">光纤类型{currentConfig.params.cableType}</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
<div className="space-y-0">
<div
className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-t-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => navigateTo('menulist', 'setup', {
menuType: 'CFP_LIMIT',
selectedConfigId: selectedConfigId,
backTo: 'testconfig',
backView: 'setup'
})}
>
<div className="text-black text-l font-bold">测试极限值{currentConfig.params.limitValue}</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
<div className="h-[50px] bg-gradient-to-b from-[#ffffff] to-[#ffffff] p-4 shadow-lg flex justify-between items-center">
<div className="text-black text-l ">参照方法{currentConfig.params.refJumper} 跳线</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
<div className="h-[50px] bg-gradient-to-b from-[#ffffff] to-[#ffffff] p-4 shadow-lg flex justify-between items-center">
<div className="text-black text-l">连接器类型LC</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
<div className="h-[50px] bg-gradient-to-b from-[#ffffff] to-[#ffffff] rounded-b-sm p-4 shadow-lg flex justify-between items-center cursor-pointer " onClick={() => updateCurrentView('cfp-conunt')}>
<div className="text-black text-l" >接线/接头的数量{currentConfig.params.connectorCount}/{currentConfig.params.spliceCount}</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
</div>
</div>
);
case 'ofp':
return (
<div className="space-y-4">
<div className="space-y-0">
<div className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-t-sm p-4 shadow-lg flex justify-between items-center">
<div className="text-black text-l font-bold">测试类型自动OTDR</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
<div className="h-[50px] bg-gradient-to-b from-[#ffffff] to-[#ffffff] p-4 shadow-lg flex justify-between items-center">
<div className="text-black text-l ">前导补偿</div>
<div className="w-3 h-3 relative">
{/* <Image src="/arrow.png" alt="箭头" fill className="object-contain" /> */}
</div>
</div>
<div className="h-[50px] bg-gradient-to-b from-[#ffffff] to-[#ffffff] rounded-b-sm p-4 shadow-lg flex justify-between items-center">
<div className="text-black text-l ">波长{currentConfig.params.cableType.includes('OM') ? '850 nm、1310nm' : '1310nm、1550nm'}</div>
<div className="w-3 h-3 relative">
{/* <Image src="/arrow.png" alt="箭头" fill className="object-contain" /> */}
</div>
</div>
</div>
<div
className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => navigateTo('menulist', 'setup', {
menuType: 'FIBER_TYPE',
selectedConfigId: selectedConfigId,
backTo: 'testconfig',
backView: 'setup'
})}
>
<div className="text-black text-l font-bold">光纤类型{currentConfig.params.cableType}</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
<div
className="h-[50px] bg-gradient-to-b from-[#e6e3e6] to-[#7b797b] rounded-sm p-4 shadow-lg flex justify-between items-center cursor-pointer"
onClick={() => navigateTo('menulist', 'setup', {
menuType: 'OFP_LIMIT',
selectedConfigId: selectedConfigId,
backTo: 'testconfig',
backView: 'setup'
})}
>
<div className="text-black text-l font-bold">测试极限值{currentConfig.params.limitValue}</div>
<div className="w-3 h-3 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
</div>
);
default:
return null;
}
};
return (
<div className="flex-1 bg-[#303040] p-4">
<div className="h-full bg-gradient-to-b from-[#c6c3c6] to-[#c6c3c6] rounded-sm p-4 shadow-lg flex flex-col">
<div
className="flex justify-between items-center mb-4 cursor-pointer"
onClick={() => navigateTo('menulist', 'setup', {
menuType: 'MODULE',
selectedConfigId: selectedConfigId,
backTo: 'testconfig',
backView: 'setup'
})}
>
<div className="text-black font-bold text-lg">模块{currentConfig.modulelable}</div>
<div className="w-5 h-5 relative">
<Image src="/arrow.png" alt="箭头" fill className="object-contain" />
</div>
</div>
<div className="flex-1 overflow-auto">
{renderConfigContent()}
</div>
</div>
</div>
);
};
// 渲染标题栏
const renderTitleBar = () => {
const { view } = navigation.current;
const handleBack = () => {
if (view === 'setup') {
updateCurrentView('main');
} else {
goBack();
}
};
switch (view) {
case 'main':
return (
<TitleBar
title="更改测试"
backTo={navigation.previous?.name || 'home'}
view={navigation.previous?.view || 'main'}
params={navigation.previous?.params || {}}
onBack={handleBack}
/>
);
case 'setup':
return (
<TitleBar
title="测试设置"
backTo={navigation.previous?.name || 'testconfig'}
view={navigation.previous?.view || 'main'}
params={navigation.previous?.params || {}}
onBack={handleBack}
/>
);
case 'cfp-conunt':
return (
<TitleBar
title=" "
view={'setup'}
params={navigation.previous?.params || {}}
/>
);
}
};
// 渲染底部栏
const renderFooter = () => {
const { view } = navigation.current;
switch (view) {
case 'main':
return (
<div className="h-[60px] bg-[#303030] flex items-center justify-between px-8">
<button
onClick={() => {
// 创建新的测试配置
const newConfig = {
id: uuidv4(),
moduleType: '8000',
modulelable: 'DSX-8000',
params: {
limitValue: 'TIA Cat 6 Channel',
cableType: 'Cat6 U/UTP',
wireOrder: 'T568B',
refJumper: '1',
spliceCount: '0',
connectorCount: '2'
}
};
// 更新项目,添加新的测试配置
const updatedProject = {
...currentProject,
testConfigs: [...currentProject.testConfigs, newConfig]
};
updateProject(useDisplayStore.getState().selectedIndexes.projectIndex, updatedProject);
// 设置新配置为选中状态并保存到导航参数
setSelectedConfigId(newConfig.id);
updateCurrentView('setup', { selectedConfigId: newConfig.id });
}}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
>
新测试
</button>
<button
onClick={() => {
if (!selectedConfigId) {
setToastMessage('请先选择一个测试配置');
return;
}
// 保存选中的配置ID到导航参数
updateCurrentView('setup', { selectedConfigId: selectedConfigId });
}}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
>
编辑
</button>
<button
onClick={() => {
if (!selectedConfigId) {
setToastMessage('请先选择一个测试配置');
return;
}
const configIndex = currentProject.testConfigs.findIndex(
config => config.id === selectedConfigId
);
setSelectedIndexes({ testConfigIndex: configIndex });
navigateTo('home', 'main');
}}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#ffd773] to-[#e7aa29] rounded-sm flex items-center justify-center text-black font-bold shadow-lg"
>
使用所选
</button>
</div>
);
case 'setup':
return (
<div className="h-[60px] bg-[#303030] flex items-center justify-end px-8">
<button
onClick={() => {
// 保存配置并返回主视图
updateCurrentView('main', { selectedConfigId: selectedConfigId });
}}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#ffd773] to-[#e7aa29] rounded-sm flex items-center justify-center text-black font-bold shadow-lg"
>
保存
</button>
</div>
);
case 'cfp-conunt':
return (
<div className="h-[60px] bg-[#303030] flex items-center justify-end px-8">
<button
onClick={() => {
// 保存配置并返回主视图
updateCurrentView('setup', { selectedConfigId: selectedConfigId });
}}
className="w-[100px] h-[40px] bg-gradient-to-b from-[#ffd773] to-[#e7aa29] rounded-sm flex items-center justify-center text-black font-bold shadow-lg"
>
保存
</button>
</div>
);
default:
return null;
}
};
const renderCFPCONUNTContent = () =>{
const currentConfig = currentProject?.testConfigs.find(
config => config.id === selectedConfigId
);
if (!currentConfig) return null;
const handleValueChange = (field, value) => {
const currentConfig = currentProject?.testConfigs.find(
config => config.id === selectedConfigId
);
if (!currentConfig) return;
// 更新当前配置的参数
const updatedConfig = {
...currentConfig,
params: {
...currentConfig.params,
[field]: value
}
};
// 更新项目中的测试配置
const updatedProject = {
...currentProject,
testConfigs: currentProject.testConfigs.map(config =>
config.id === selectedConfigId ? updatedConfig : config
)
};
// 调用displayStore的updateProject方法更新状态
updateProject(useDisplayStore.getState().selectedIndexes.projectIndex, updatedProject);
};
return (
<div className="flex-1 bg-[#0f172a] flex flex-col relative">
<div className="absolute inset-0">
<Image
src={`/olts-weldingPointSet.png`}
alt="CFPCOUNUNT"
fill
className="object-contain"
sizes="100vw"
/>
</div>
{currentConfig.params.connectorCount > 2 && (<div>
<div className="absolute flex flex-col gap-10 pl-56.5 pt-45">
<div className="relative">
<Image
src={`/icon-connector.svg`}
alt="connector"
width={30}
height={30}
className="object-contain"
/>
<span className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white">
{currentConfig.params.connectorCount-2}
</span>
</div>
</div>
<div className="absolute flex flex-col gap-10 pl-43 pt-45">
<div className="relative">
<Image
src={`/icon-connector.svg`}
alt="connector"
width={30}
height={30}
className="object-contain"
/>
<span className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white">
{currentConfig.params.connectorCount-2}
</span>
</div>
</div>
</div>)}
{currentConfig.params.spliceCount > 0 && (<div>
<div className="absolute flex flex-col gap-10 pl-57 pt-65">
<div className="relative">
<div className="w-6 h-6 rounded-full bg-[#104878]"></div>
<span className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white">
{currentConfig.params.spliceCount}
</span>
</div>
</div>
<div className="absolute flex flex-col gap-10 pl-44 pt-65">
<div className="relative">
<div className="w-6 h-6 rounded-full bg-[#104878]"></div>
<span className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white">
{currentConfig.params.spliceCount}
</span>
</div>
</div>
</div>)}
<div className="relative flex flex-col gap-10 pl-75 pt-30">
<div className="flex items-center">
<input
type="number"
min="2"
max="20"
defaultValue={currentConfig.params.connectorCount}
onChange={(e) => handleValueChange('connectorCount', e.target.value)}
onKeyDown={(e) => e.preventDefault()}
className="w-16 h-8 px-2 bg-[#104878] rounded text-center focus:outline-none focus:ring-2 focus:ring-[#0ff] text-white"
/>
<span className="ml-4 text-base font-roboto select-none text-black">全部连接</span>
</div>
<div className="flex items-center">
<input
type="number"
min="0"
max="18"
defaultValue={currentConfig.params.spliceCount}
onChange={(e) => handleValueChange('spliceCount', e.target.value)}
onKeyDown={(e) => e.preventDefault()}
className="w-16 h-8 px-2 bg-[#104878] rounded text-center focus:outline-none focus:ring-2 focus:ring-[#0ff] text-white"
/>
<span className="ml-4 text-base font-roboto select-none text-black">接头</span>
</div>
<div className="flex items-center">
<input
type="number"
min="1"
max="1"
defaultValue={currentConfig.params.refJumper}
onChange={(e) => handleValueChange('refJumper', e.target.value)}
onKeyDown={(e) => e.preventDefault()}
className="w-16 h-8 px-2 bg-[#104878] rounded text-center focus:outline-none focus:ring-2 focus:ring-[#0ff] text-white"
/>
<span className="ml-4 text-base font-roboto select-none text-black">跳线参照</span>
</div>
</div>
</div>
);
};
// 渲染内容
const renderContent = () => {
const { view } = navigation.current;
switch (view) {
case 'main':
return renderMainContent();
case 'setup':
return renderSetupContent();
case 'cfp-conunt':
return renderCFPCONUNTContent();
default:
return renderMainContent();
}
};
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<StatusBar />
{renderTitleBar()}
{renderContent()}
{renderFooter()}
</div>
);
}

View File

@@ -0,0 +1,747 @@
import React, { useState, useEffect } from 'react';
import StatusBar from '../lib/StatusBar';
import TitleBar from '../lib/TitleBar';
import useDisplayStore from '@/store/displayStore';
import useDeviceStore from '@/store/deviceStore';
import { v4 as uuidv4 } from 'uuid';
// 计算电阻余量的函数
function calculateResistanceMargin(data, limitdata, limitValue) {
try {
// 判断测试类型
const isPoETest = limitValue.includes('+PoE');
const isALLTest = limitValue.includes('+ALL');
let minMargin = Infinity;
// 回路电阻计算
const loopResistanceData = data?.performance?.data?.['Loop Resistance (Ω)'];
const loopResistanceLimit = limitdata?.['Loop Resistance (Ω)']?.['PAIRLimit (Ω)'];
if (loopResistanceData && loopResistanceLimit) {
const pairs = ['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
for (const pair of pairs) {
const actualValues = loopResistanceData[pair];
if (actualValues && Array.isArray(actualValues)) {
for (let i = 0; i < actualValues.length && i < loopResistanceLimit.length; i++) {
const actualValue = actualValues[i];
const limitVal = loopResistanceLimit[i];
if (actualValue !== undefined && actualValue !== null && limitVal !== undefined && limitVal !== null) {
const margin = limitVal - Math.abs(actualValue);
minMargin = Math.min(minMargin, margin);
}
}
}
}
}
// 线对UBL计算仅在+PoE或+ALL测试时
if (isPoETest || isALLTest) {
const ublData = data?.performance?.data?.['UBL (Ω)'];
const ublLimit = limitdata?.['UBL (Ω)']?.['PAIRLimit (Ω)'];
if (ublData && ublLimit) {
const pairs = ['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
for (const pair of pairs) {
const actualValues = ublData[pair];
if (actualValues && Array.isArray(actualValues)) {
for (let i = 0; i < actualValues.length && i < ublLimit.length; i++) {
const actualValue = actualValues[i];
const limitVal = ublLimit[i];
if (actualValue !== undefined && actualValue !== null && limitVal !== undefined && limitVal !== null) {
const margin = limitVal - Math.abs(actualValue);
minMargin = Math.min(minMargin, margin);
}
}
}
}
}
}
// P2PUBL计算仅在+PoE或+ALL测试时
if (isPoETest || isALLTest) {
const p2pUblData = data?.performance?.data?.['P2PUBL (Ω)'];
const p2pUblLimit = limitdata?.['P2PUBL (Ω)']?.['PAIRLimit (Ω)'];
if (p2pUblData && p2pUblLimit) {
const pairs = ['PAIR1236', 'PAIR1245', 'PAIR1278', 'PAIR3645', 'PAIR3678', 'PAIR4578'];
for (const pair of pairs) {
const actualValues = p2pUblData[pair];
if (actualValues && Array.isArray(actualValues)) {
for (let i = 0; i < actualValues.length && i < p2pUblLimit.length; i++) {
const actualValue = actualValues[i];
const limitVal = p2pUblLimit[i];
if (actualValue !== undefined && actualValue !== null && limitVal !== undefined && limitVal !== null) {
const margin = limitVal - Math.abs(actualValue);
minMargin = Math.min(minMargin, margin);
}
}
}
}
}
}
return minMargin === Infinity ? 0 : minMargin;
} catch (error) {
console.error('Error calculating resistance margin:', error);
return 0;
}
}
export default function Testing() {
const { view } = useDisplayStore.getState().navigation.current;
const { navigateTo,
getCurrentTestConfig,
getCurrentProject,
getCurrentOperator,
getCurrentCableId,
getCurrentCableId2,
setToastMessage,
} = useDisplayStore();
const currentConfig = getCurrentTestConfig();
const { connectionStatus,setTotalToastMessage } = useDeviceStore();
// 铜缆测试路径和状态
const copperMainPathKey = connectionStatus?.mainPaths ? Object.keys(connectionStatus.mainPaths)[0] : null;
const copperRemotePathKey = connectionStatus?.remotePaths ? Object.keys(connectionStatus.remotePaths)[0] : null;
const coppermainend = copperMainPathKey ? connectionStatus.mainPaths[copperMainPathKey]?.end : null;
const copperremoteend = copperRemotePathKey ? connectionStatus.remotePaths[copperRemotePathKey]?.end : null;
const copperRefStatus = useDisplayStore.getState()?.ref?.copper?.status || null;
// CFP测试路径和状态
const cableType = currentConfig.params.cableType;
const isMultiMode = cableType.includes('OM');
const cfpRefStatus = useDisplayStore.getState()?.ref?.cfp?.status || null;
const cfpRefConnectStatus = useDisplayStore.getState()?.ref?.cfp?.connectStatus || null;
const cfpInResult = isMultiMode ? connectionStatus?.CFPMainIn : connectionStatus?.CFPMainOut;
const cfpOutResult = isMultiMode ? connectionStatus?.CFPMainOut : connectionStatus?.CFPMainIn;
// 获取isMultiMode
const wavelength1 = isMultiMode ? '850' : '1310';
const wavelength2 = isMultiMode ? '1300' : '1550';
// OFP测试路径和状态
const ofpMainEnd = copperMainPathKey ? connectionStatus.mainPaths[copperMainPathKey]?.end : null;
const ofpRefStatus = useDisplayStore.getState()?.ref?.ofp?.status || null;
const ofpRefConnectStatus = useDisplayStore.getState()?.ref?.ofp?.connectStatus || null;
const [tempTestResult, setTempTestResult] = useState(null);
// 获取测试链路清洁状态
const moduleType = currentConfig?.moduleType;
// 检查路径中所有端口的清洁状态
const checkPortsCleanStatus = (paths) => {
if (!paths || paths.length === 0) return false;
// 检查每个端口的清洁状态
const cleanPorts = paths.filter(portId => {
const portElement = document.getElementById(portId);
return portElement?.getAttribute('lcclean') === 'true';
});
// 返回已清洁的端口数量
return cleanPorts.length;
};
let allPathsCleanStatus = {};
let isAllPathsClean = 0; // 默认为0表示全部未清洁
// 只在模块类型为 cfp 或 ofp 时执行检测
if (moduleType === 'cfp' || moduleType === 'ofp') {
if (moduleType === 'cfp') {
// CFP模块检测所有路径
const mainInPaths = connectionStatus?.mainPaths?.['main-cfp-in']?.path || [];
const mainOutPaths = connectionStatus?.mainPaths?.[isMultiMode ? 'main-cfp-mm-out' : 'main-cfp-sm-out']?.path || [];
const remoteInPaths = connectionStatus?.remotePaths?.['remote-cfp-in']?.path || [];
const remoteOutPaths = connectionStatus?.remotePaths?.[isMultiMode ? 'remote-cfp-mm-out' : 'remote-cfp-sm-out']?.path || [];
// 获取每个路径的清洁端口数量
const mainInClean = checkPortsCleanStatus(mainInPaths);
const mainOutClean = checkPortsCleanStatus(mainOutPaths);
const remoteInClean = checkPortsCleanStatus(remoteInPaths);
const remoteOutClean = checkPortsCleanStatus(remoteOutPaths);
// 计算总端口数和已清洁端口数
const totalPorts = mainInPaths.length + mainOutPaths.length +
remoteInPaths.length + remoteOutPaths.length;
const cleanedPorts = mainInClean + mainOutClean +
remoteInClean + remoteOutClean;
// 设置清洁状态
if (cleanedPorts === 0) isAllPathsClean = 0;
else if (cleanedPorts < totalPorts) isAllPathsClean = 1;
else if (cleanedPorts === totalPorts) isAllPathsClean = 2;
allPathsCleanStatus = {
mainInClean: mainInClean === mainInPaths.length,
mainOutClean: mainOutClean === mainOutPaths.length,
remoteInClean: remoteInClean === remoteInPaths.length,
remoteOutClean: remoteOutClean === remoteOutPaths.length
};
} else {
// OFP模块只检测主机输出端口
const mainOutPaths = connectionStatus?.mainPaths?.[isMultiMode ? 'main-ofp-mm-out' : 'main-ofp-sm-out']?.path || [];
const cleanedPorts = checkPortsCleanStatus(mainOutPaths);
// 设置清洁状态
if (cleanedPorts === 0) isAllPathsClean = 0;
else if (cleanedPorts < mainOutPaths.length) isAllPathsClean = 1;
else if (cleanedPorts === mainOutPaths.length) isAllPathsClean = 2;
allPathsCleanStatus = {
mainOutClean: cleanedPorts === mainOutPaths.length
};
}
}
useEffect(() => {
if (!tempTestResult) {
const loadTestResult = async () => {
try {
const currentProject = getCurrentProject();
const currentOperator = getCurrentOperator();
// 获取当前选中的电缆ID
const currentCableId = getCurrentCableId().name;
// 获取当前选中的电缆ID2
const currentCableId2 = getCurrentCableId2().name;
const CopperWiremapStatus = connectionStatus?.CopperWiremapStatus;
const CopperPerformanceStatus = connectionStatus?.CopperPerformanceStatus;
const ofpResult = connectionStatus?.OFPStatus;
const moduleType = currentConfig?.moduleType;
const cableType = currentConfig?.params?.cableType;
const wireOrder = currentConfig?.params?.wireOrder;
let testResult;
// 不同模块组装测试结果逻辑
if (moduleType === '8000') {
let CopperResultStatus = 'fail';
let CopperWiremapResultStatus = 'fail';
if (CopperWiremapStatus === 'pass' ) {
if (wireOrder === 'T568B' ||
wireOrder === 'Ethernet Two-Pair' ||
wireOrder === 'M12-D Two-Pair') {
CopperWiremapResultStatus = 'pass';
console.log(CopperWiremapResultStatus);
}
} else if (CopperWiremapStatus === 'pass-2pair') {
if (wireOrder === 'Ethernet Two-Pair' ||
wireOrder === 'M12-D Two-Pair') {
CopperWiremapResultStatus = 'pass';
} else if (wireOrder === 'T568B') {
CopperWiremapResultStatus = 'fail';
CopperResultStatus = 'fail';
}
}else if (CopperWiremapStatus === 'sopen') {
if (wireOrder === 'T568B') {
CopperWiremapResultStatus = 'fail';
CopperResultStatus = 'fail';
}
else if (cableType.includes('U/UTP')) {
CopperWiremapResultStatus = 'pass';
}
else if (cableType.includes('F/UTP')) {
CopperWiremapResultStatus = 'fail';
CopperResultStatus = 'fail';
}
}
if (CopperWiremapResultStatus === 'pass') {
try {
const response = await import(`@/store/COPPER/${CopperPerformanceStatus}.json`);
const data = response.default;
// 加载限制数据
const limitValue = currentConfig?.params?.limitValue;
try {
// 从limitValue中提取基础名称(移除+PoE和+ALL后缀)
const baseName = limitValue.split(' (+')[0];
// 使用基础名称加载带有(+ALL)后缀的文件
const limitResponse = await import(`@/store/COPPER/${baseName} (+ALL).json`);
const limitdata = limitResponse;
// 计算所有参数的余量
const paramTitles = ['插入损耗', '回波损耗', 'NEXT', 'PS NEXT', 'ACR-N', 'PS ACR-N',
'ACR-F', 'PS ACR-F', 'CDNEXT', 'CMRL', 'TCL', 'ELTCTL', '电阻'];
const dataGroupMap = {
'插入损耗': 'Insertion Loss (dB)',
'回波损耗': 'RL (dB)',
'NEXT': 'NEXT (dB)',
'PS NEXT': 'PS NEXT (dB)',
'ACR-N': 'ACR-N (dB)',
'PS ACR-N': 'PS ACR-N (dB)',
'ACR-F': 'ACR-F (dB)',
'PS ACR-F': 'PS ACR-F (dB)',
'CDNEXT': 'CDNEXT (dB)',
'CMRL': 'CMRL (dB)',
'TCL': 'TCL (dB)',
'ELTCTL': 'ELTCTL (dB)',
'电阻': 'Resistance (Ω)'
};
let hasNegativeMargin = false;
// 长度判断
if (limitdata.LENGTH && data.performance.LENGTH) {
const lengthPairs = ['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
for (const pair of lengthPairs) {
if (data.performance.LENGTH[pair] > limitdata.LENGTH) {
hasNegativeMargin = true;
break;
}
}
}
// 电阻判断
if (limitdata.LOOP && data.performance.OHM?.LOOP) {
const loopPairs = ['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
for (const pair of loopPairs) {
if (data.performance.OHM.LOOP[pair] > limitdata.LOOP) {
hasNegativeMargin = true;
console.log("电阻fail")
break;
}
}
}
// 检查是否是 +PoE 或 +ALL 测试
const hasPoE = limitValue?.includes('+PoE');
const hasALL = limitValue?.includes('+ALL');
// +PoE 或 +ALL 测试的额外检查
if ((hasPoE || hasALL) && data.performance.OHM) {
// 线对 UBL 余量检查
if (limitdata.PAIRUBL && data.performance.OHM.PAIRUBL) {
const pairUblPairs = ['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
for (const pair of pairUblPairs) {
if (Math.abs(data.performance.OHM.PAIRUBL[pair]) > limitdata.PAIRUBL) {
hasNegativeMargin = true;
console.log("UBL fail")
break;
}
}
}
// P2P UBL 余量检查
if (limitdata.P2PUBL && data.performance.OHM.P2PUBL) {
const p2pUblPairs = ['PAIR1236', 'PAIR1245', 'PAIR1278', 'PAIR3645', 'PAIR3678', 'PAIR4578'];
for (const pair of p2pUblPairs) {
if (Math.abs(data.performance.OHM.P2PUBL[pair]) > limitdata.P2PUBL) {
hasNegativeMargin = true;
console.log("P2P fail")
break;
}
}
}
}
for (const paramTitle of paramTitles) {
const dataGroup = dataGroupMap[paramTitle];
// 电阻参数的特殊处理
if (paramTitle === '电阻') {
const resistanceMargin = calculateResistanceMargin(data, limitdata, limitValue);
if (resistanceMargin < 0) {
hasNegativeMargin = true;
}
continue;
}
if (!dataGroup || !data?.performance?.data?.[dataGroup] || !limitdata?.[dataGroup]) continue;
const testData = data.performance.data[dataGroup];
const limitValues = limitdata[dataGroup]?.['PAIRLimit (dB)'] || [];
const pairs = ['NEXT (dB)', 'ACR-N (dB)'].includes(dataGroup) ?
['PAIR1236', 'PAIR1245', 'PAIR1278', 'PAIR3645', 'PAIR3678', 'PAIR4578'] :
['ACR-F (dB)', 'CDNEXT (dB)'].includes(dataGroup) ?
['PAIR1236', 'PAIR1245', 'PAIR1278', 'PAIR3612', 'PAIR3645', 'PAIR3678',
'PAIR4512', 'PAIR4535', 'PAIR4578', 'PAIR7812', 'PAIR7936', 'PAIR7845'] :
['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
for (let index = 0; index < limitValues.length && !hasNegativeMargin; index++) {
const limitValue = limitValues[index];
if (limitValue === undefined || limitValue === null) break;
for (const pair of pairs) {
const actualValue = testData[pair]?.[index];
if (actualValue !== undefined && actualValue !== null) {
const margin = paramTitle === '插入损耗' ?
Math.abs(limitValue) - Math.abs(actualValue) :
Math.abs(actualValue) - limitValue;
if (margin < 0) {
hasNegativeMargin = true;
break;
}
}
}
}
}
CopperResultStatus = hasNegativeMargin ? 'fail' : 'pass';
} catch (error) {
console.error('Error calculating margins:', error);
}
} catch (error) {
console.error('加载测试结果失败:', error);
}
}
testResult = {
id: uuidv4(),
name: currentProject?.cableIds[0]?.name || '',
testconfig: JSON.parse(JSON.stringify(currentConfig || {})),
operators: currentOperator?.name || '',
CopperWiremapStatus,
CopperWiremapResultStatus,
CopperPerformanceStatus,
CopperRef: copperRefStatus,
CopperResultStatus,
};
} else if (moduleType === 'cfp' ) {
// 极限值导入
const limitValue = currentConfig?.params?.limitValue;
const CFP_LIMIT = require('@/store/FIBER/CFP_LIMIT').default;
const limitData = CFP_LIMIT[limitValue];
if (limitData) {
// 使用limitData中的极限值进行后续处理
const {
adapterLoss,
firstLastAdapterLoss,
spliceLoss,
totalLoss1310nm,
totalLoss1550nm,
lossPerKm850nm,
lossPerKm13000nm,
lossPerKm1310nm,
lossPerKm1550nm,
maxSMLength,
manMMLength
} = limitData;
}
// 用户配置加载
const connectorCount = currentConfig?.params?.connectorCount;
const spliceCount = currentConfig?.params?.spliceCount;
const refJumper = currentConfig?.params?.refJumper;
let cfpMainInStatus = null;
let cfpMainOutStatus = null;
if (cfpInResult) {
try {
// 加载第一个波长的数据
const wave1Data = await import(`@/store/FIBER/${cfpInResult}/${wavelength1}-dump.json`);
// 加载第二个波长的数据
const wave2Data = await import(`@/store/FIBER/${cfpInResult}/${wavelength2}-dump.json`);
// 添加随机浮动因子损耗上下浮动1%
const getDistanceRandomFactor = () => 1; // 长度不浮动
const getLossRandomFactor = () => 1 + (Math.random() * 0.02 - 0.01); // 生成0.99到1.01之间的随机数
// 先计算两个波长的损耗值
const wave1Loss = wave1Data.Summary.totalLoss * getLossRandomFactor();
const wave2Loss = wave2Data.Summary.totalLoss * getLossRandomFactor();
// 如果是参考状态增加0.1
const finalWave1Loss = !cfpRefStatus ? wave1Loss : wave1Loss + 0.1;
const finalWave2Loss = !cfpRefStatus ? wave2Loss : wave2Loss + 0.1;
cfpMainInStatus = {
wavelength1: {
wave: wavelength1,
distance: (wave1Data.Summary.totalDistance * getDistanceRandomFactor()).toFixed(2),
loss: finalWave1Loss.toFixed(2)
},
wavelength2: {
wave: wavelength2,
distance: (wave2Data.Summary.totalDistance * getDistanceRandomFactor()).toFixed(2),
loss: finalWave2Loss.toFixed(2)
}
};
} catch (error) {
setTimeout(() => {
setTotalToastMessage("被测链路与参考跳纤类型不一致,请检查");
}, 1500); // 3000 毫秒 = 3 秒
return;
}
}
if (cfpOutResult) {
try {
// 加载第一个波长的数据
const wave1Data = await import(`@/store/FIBER/${cfpOutResult}/${wavelength1}-dump.json`);
// 加载第二个波长的数据
const wave2Data = await import(`@/store/FIBER/${cfpOutResult}/${wavelength2}-dump.json`);
// 添加随机浮动因子损耗上下浮动1%
const getDistanceRandomFactor = () => 1; // 长度不浮动
const getLossRandomFactor = () => 1 + (Math.random() * 0.02 - 0.01); // 生成0.99到1.01之间的随机数
// 先计算两个波长的损耗值
const wave1Loss = wave1Data.Summary.totalLoss * getLossRandomFactor();
const wave2Loss = wave2Data.Summary.totalLoss * getLossRandomFactor();
// 如果是参考状态增加0.1
const finalWave1Loss = !cfpRefStatus ? wave1Loss : wave1Loss + 0.1;
const finalWave2Loss = !cfpRefStatus ? wave2Loss : wave2Loss + 0.1;
cfpMainOutStatus = {
wavelength1: {
wave: wavelength1,
distance: (wave1Data.Summary.totalDistance * getDistanceRandomFactor()).toFixed(2),
loss: finalWave1Loss.toFixed(2)
},
wavelength2: {
wave: wavelength2,
distance: (wave2Data.Summary.totalDistance * getDistanceRandomFactor()).toFixed(2),
loss: finalWave2Loss.toFixed(2)
}
};
} catch (error) {
setTimeout(() => {
setToastMessage("未检测到光纤,请检查接口");
}, 1500); // 3000 毫秒 = 3 秒
return;
}
}
// 结果计算与判断
// 第一根光纤
const cfpMainIndistance = cfpMainInStatus?.wavelength1?.distance || 0;
const cfpMainIndistance2 = cfpMainInStatus?.wavelength2?.distance || 0;
const cfpMainInloss = cfpMainInStatus?.wavelength1?.loss || 0;
const cfpMainInloss2 = cfpMainInStatus?.wavelength2?.loss || 0;
// 长度判断
const maxLength = isMultiMode ? limitData.maxMMLength : limitData.maxSMLength;
const isLengthValidIn = cfpMainIndistance <= maxLength &&
cfpMainIndistance2 <= maxLength ;
// 损耗判断
const lossPerKm = isMultiMode ? (limitData.lossPerKm850nm):(limitData.lossPerKm1310nm);
const lossPerKm2 = isMultiMode ? (limitData.lossPerKm1300nm):(limitData.lossPerKm1550nm);
const totalLossLimit = limitData.adapterLoss !== null ?
connectorCount * limitData.adapterLoss +
spliceCount * limitData.spliceLoss +
(cfpMainIndistance/1000) * lossPerKm
: isMultiMode ?
(limitData.totalLoss850nm):(limitData.totalLoss1310nm);
const totalLossLimit2 = limitData.adapterLoss !== null ?
connectorCount * limitData.adapterLoss +
spliceCount * limitData.spliceLoss +
(cfpMainIndistance2/1000) * lossPerKm2
: isMultiMode ?
(limitData.totalLoss1300nm):(limitData.totalLoss1550nm);
const isLossValidIn = cfpMainInloss <= totalLossLimit &&
cfpMainInloss2 <= totalLossLimit2 ;
// 第二根光纤
const cfpMainOutdistance = cfpMainOutStatus?.wavelength1?.distance || 0;
const cfpMainOutdistance2 = cfpMainOutStatus?.wavelength2?.distance || 0;
const cfpMainOutloss = cfpMainOutStatus?.wavelength1?.loss || 0;
const cfpMainOutloss2 = cfpMainOutStatus?.wavelength2?.loss || 0;
// 长度判断
const maxLengthOut = isMultiMode ? limitData.maxMMLength : limitData.maxSMLength;
const isLengthValidOut = cfpMainOutdistance <= maxLength &&
cfpMainOutdistance2 <= maxLength ;
// 损耗判断
const lossPerKmOut = isMultiMode ? (limitData.lossPerKm850nm):(limitData.lossPerKm1310nm);
const lossPerKm2Out = isMultiMode ? (limitData.lossPerKm1300nm):(limitData.lossPerKm1550nm);
const totalLossLimitOut = limitData.adapterLoss !== null ?
connectorCount * limitData.adapterLoss +
spliceCount * limitData.spliceLoss +
(cfpMainIndistance/1000) * lossPerKmOut
: isMultiMode ?
(limitData.totalLoss850nm):(limitData.totalLoss1310nm);
const totalLossLimit2Out = limitData.adapterLoss !== null ?
connectorCount * limitData.adapterLoss +
spliceCount * limitData.spliceLoss +
(cfpMainOutdistance2/1000) * lossPerKm2Out
: isMultiMode ?
(limitData.totalLoss1300nm):(limitData.totalLoss1550nm);
const isLossValidOut = cfpMainOutloss <= totalLossLimitOut &&
cfpMainOutloss2 <= totalLossLimit2Out ;
// 结果判断
const CFPResultStatus = isLengthValidIn && isLossValidIn && isLengthValidOut && isLossValidOut ? 'pass' : 'fail';
// IN端口通过失败判断根据 isMultiMode 判断使用哪组参数)
const InPortStatus = isMultiMode
? (isLengthValidIn && isLossValidIn ? 'pass' : 'fail')
: (isLengthValidOut && isLossValidOut ? 'pass' : 'fail');
// OUT端口通过失败判断
const OutPortStatus = isMultiMode
? (isLengthValidOut && isLengthValidOut ? 'pass' : 'fail')
: (isLengthValidIn && isLossValidIn ? 'pass' : 'fail');
testResult = {
id: uuidv4(),
name: currentCableId || '',
name2: currentCableId2 || '',
testconfig: JSON.parse(JSON.stringify(currentConfig || {})),
operators: currentOperator?.name || '',
CFPRef:cfpRefStatus,
CFPRefConnect:cfpRefConnectStatus,
CFPMainIn:cfpMainInStatus,
isLengthValidIn,
isLossValidIn,
InPortStatus, // 添加IN端口状态
isLengthValidOut,
isLossValidOut,
OutPortStatus, // 添加OUT端口状态
CFPMainOut:cfpMainOutStatus,
CFPResultStatus,
PortCleanStatus:isAllPathsClean,
};
} else if (moduleType === 'ofp') {
const ofpConnectionStatus = connectionStatus;
let ofpResultStatus = 'fail';
if (ofpResult.includes('pass')) {
if (!ofpRefStatus) {
if (connectionStatus.OFPoutRefStatus.includes('sm-') || connectionStatus.OFPoutRefStatus.includes('mm-')) {
ofpResultStatus = 'fail';
} else {
ofpResultStatus = 'pass';
}
} else if (ofpRefStatus === 'start') {
if (connectionStatus.OFPoutRefStatus.includes('smc-') || connectionStatus.OFPoutRefStatus.includes('mmc-')) {
ofpResultStatus = 'pass';
}
} else if (ofpRefStatus === 'end') {
const hasSmOrMm = connectionStatus.OFPoutRefStatus.includes('sm-') || connectionStatus.OFPoutRefStatus.includes('mm-');
const hasSmcOrMmc = connectionStatus.OFPoutRefStatus.includes('smc-') || connectionStatus.OFPoutRefStatus.includes('mmc-');
const hasConnectedSmOrMm = connectionStatus.OFPConnectedToRefStatus?.includes('sm-') || connectionStatus.OFPConnectedToRefStatus?.includes('mm-');
const hasConnectedSmcOrMmc = connectionStatus.OFPConnectedToRefStatus?.includes('smc-') || connectionStatus.OFPConnectedToRefStatus?.includes('mmc-');
// 只有前导和末尾都是补偿线时才是pass其他情况都是fail
if (hasSmcOrMmc && hasConnectedSmcOrMmc) {
ofpResultStatus = 'pass';
} else {
ofpResultStatus = 'fail';
}
}
} else if (ofpResult === 'connector-fail-end') {
if (!ofpRefStatus) {
if (connectionStatus.OFPoutRefStatus.includes('sm-') || connectionStatus.OFPoutRefStatus.includes('mm-')) {
ofpResultStatus = 'fail';
} else {
ofpResultStatus = 'pass';
}
} else if (ofpRefStatus === 'start') {
if (connectionStatus.OFPoutRefStatus.includes('smc-') || connectionStatus.OFPoutRefStatus.includes('mmc-')) {
ofpResultStatus = 'pass';
}
} else if (ofpRefStatus === 'end') {
const hasSmOrMm = connectionStatus.OFPoutRefStatus.includes('sm-') || connectionStatus.OFPoutRefStatus.includes('mm-');
const hasSmcOrMmc = connectionStatus.OFPoutRefStatus.includes('smc-') || connectionStatus.OFPoutRefStatus.includes('mmc-');
const hasConnectedSmOrMm = connectionStatus.OFPConnectedToRefStatus?.includes('sm-') || connectionStatus.OFPConnectedToRefStatus?.includes('mm-');
const hasConnectedSmcOrMmc = connectionStatus.OFPConnectedToRefStatus?.includes('smc-') || connectionStatus.OFPConnectedToRefStatus?.includes('mmc-');
// 只有前导和末尾都是补偿线时才是pass其他情况都是fail
if (hasSmcOrMmc && hasConnectedSmcOrMmc) {
ofpResultStatus = 'pass';
} else {
ofpResultStatus = 'fail';
}
}
}else if (ofpResult === 'connector-fail-start') {
if (ofpRefStatus === 'start') {
const hasSmOrMm = connectionStatus.OFPoutRefStatus.includes('sm-') || connectionStatus.OFPoutRefStatus.includes('mm-');
const hasSmcOrMmc = connectionStatus.OFPoutRefStatus.includes('smc-') || connectionStatus.OFPoutRefStatus.includes('mmc-');
if(hasSmOrMm){
ofpResultStatus = 'fail';
}else if(hasSmcOrMmc){
ofpResultStatus = 'fail';
}
}
}
testResult = {
id: uuidv4(),
name: currentProject?.cableIds[0]?.name || '',
testconfig: JSON.parse(JSON.stringify(currentConfig || {})),
operators: currentOperator?.name || '',
ofpResult,
ofpRefStatus,
OFPRefConnect:ofpRefConnectStatus,
ofpConnectionStatus,
ofpResultStatus,
PortCleanStatus:isAllPathsClean,
};
}
setTempTestResult(testResult);
} catch (error) {
console.error('加载测试结果失败:', error);
}
};
loadTestResult();
}
}, [connectionStatus?.CopperWiremapStatus, connectionStatus?.CopperPerformanceStatus]);
useEffect(() => {
if (tempTestResult) {
const moduleType = tempTestResult.testconfig?.moduleType;
let delayTime = 1500; // 默认延迟1.5秒
if (moduleType === '8000') {
delayTime = 5000; // 8000模块延迟5秒
} else if (moduleType === 'cfp') {
delayTime = 1500; // cfp模块延迟1.5秒
} else if (moduleType === 'ofp') {
delayTime = 6000; // ofp模块延迟6秒
}
const timer = setTimeout(() => {
navigateTo('resultinfo', 'nosave', tempTestResult);
}, delayTime);
return () => clearTimeout(timer);
}
}, [tempTestResult]);
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<StatusBar />
<TitleBar
title="进程"
/>
<div className="h-[490px] bg-[#303040] relative">
{/* 背景图片 */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: 'url(/testing.gif)' }}
/>
{/* 测试配置limit值 */}
<div className="absolute w-full top-8 z-10">
<span className="flex items-center justify-center text-black text-xl font-bold">
{currentConfig?.params?.limitValue || '未设置'}
</span>
</div>
</div>
<div className="h-[60px] bg-[#303030] flex items-center justify-center px-4">
<button
className="w-[100px] h-[40px] bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold shadow-lg"
onClick={() => navigateTo('home', 'main')}
>
取消
</button>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,201 @@
import React, { useState, useEffect } from 'react';
import WireMapView from './WireMapView';
import PerformanceView from './PerformanceView';
import DiagnosticView from './DiagnosticView';
import useDisplayStore from '@/store/displayStore';
import { v4 as uuidv4 } from 'uuid';
export default function CopperResultMain({ testResult: initialTestResult }) {
const { navigation, navigateTo, getCurrentTestConfig } = useDisplayStore();
const [activeTab, setActiveTab] = useState(navigation.current.params.activeTab || '布线图');
const [testResult, setTestResult] = useState(initialTestResult);
const [resultData, setResultData] = useState(null);
const currentConfig = getCurrentTestConfig();
const cableType = currentConfig?.params?.cableType;
const wireOrder = currentConfig?.params?.wireOrder;
useEffect(() => {
const loadTestResult = async () => {
if (initialTestResult.CopperWiremapResultStatus === 'fail') {
setResultData({ result: 'fail' });
} else if (initialTestResult.CopperWiremapResultStatus === 'pass') {
try {
const response = await import(`@/store/COPPER/${initialTestResult.CopperPerformanceStatus}.json`);
const data = response.default;
// 加载限制数据
const limitValue = initialTestResult.testconfig?.params?.limitValue;
let limitdata;
try {
// 从limitValue中提取基础名称(移除+PoE和+ALL后缀)
const baseName = limitValue.split(' (+')[0];
// 使用基础名称加载带有(+ALL)后缀的文件
const limitResponse = await import(`@/store/COPPER/${baseName} (+ALL).json`);
limitdata = limitResponse;
// 计算所有参数的余量
const paramTitles = ['插入损耗', '回波损耗', 'NEXT', 'PS NEXT', 'ACR-N', 'PS ACR-N',
'ACR-F', 'PS ACR-F', 'CDNEXT', 'CMRL', 'TCL', 'ELTCTL'];
const dataGroupMap = {
'插入损耗': 'Insertion Loss (dB)',
'回波损耗': 'RL (dB)',
'NEXT': 'NEXT (dB)',
'PS NEXT': 'PS NEXT (dB)',
'ACR-N': 'ACR-N (dB)',
'PS ACR-N': 'PS ACR-N (dB)',
'ACR-F': 'ACR-F (dB)',
'PS ACR-F': 'PS ACR-F (dB)',
'CDNEXT': 'CDNEXT (dB)',
'CMRL': 'CMRL (dB)',
'TCL': 'TCL (dB)',
'ELTCTL': 'ELTCTL (dB)'
};
let hasNegativeMargin = false;
// 长度判断
if (limitdata.LENGTH && data.performance.LENGTH) {
const lengthPairs = ['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
for (const pair of lengthPairs) {
if (data.performance.LENGTH[pair] > limitdata.LENGTH) {
hasNegativeMargin = true;
break;
}
}
}
// 电阻判断
if (limitdata.LOOP && data.performance.OHM?.LOOP) {
const loopPairs = ['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
for (const pair of loopPairs) {
if (data.performance.OHM.LOOP[pair] > limitdata.LOOP) {
hasNegativeMargin = true;
break;
}
}
}
for (const paramTitle of paramTitles) {
const dataGroup = dataGroupMap[paramTitle];
if (!dataGroup || !data?.performance?.data?.[dataGroup] || !limitdata?.[dataGroup]) continue;
const testData = data.performance.data[dataGroup];
const limitValues = limitdata[dataGroup]?.['PAIRLimit (dB)'] || [];
const getPairsByWireOrder = () => {
if (wireOrder === 'Ethernet Two-Pair' || wireOrder === 'M12-D Two-Pair') {
return ['NEXT (dB)', 'ACR-N (dB)'].includes(dataGroup) ?
['PAIR1236'] :
['ACR-F (dB)', 'CDNEXT (dB)'].includes(dataGroup) ?
['PAIR1236', 'PAIR3612'] :
['PAIR12', 'PAIR36'];
}
// 默认返回所有线对
return ['NEXT (dB)', 'ACR-N (dB)'].includes(dataGroup) ?
['PAIR1236', 'PAIR1245', 'PAIR1278', 'PAIR3645', 'PAIR3678', 'PAIR4578'] :
['ACR-F (dB)', 'CDNEXT (dB)'].includes(dataGroup) ?
['PAIR1236', 'PAIR1245', 'PAIR1278', 'PAIR3612', 'PAIR3645', 'PAIR3678',
'PAIR4512', 'PAIR4535', 'PAIR4578', 'PAIR7812', 'PAIR7936', 'PAIR7845'] :
['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
};
const pairs = getPairsByWireOrder();
for (let index = 0; index < limitValues.length && !hasNegativeMargin; index++) {
const limitValue = limitValues[index];
if (limitValue === undefined || limitValue === null) break;
for (const pair of pairs) {
const actualValue = testData[pair]?.[index];
if (actualValue !== undefined && actualValue !== null) {
const margin = paramTitle === '插入损耗' ?
Math.abs(limitValue) - Math.abs(actualValue) :
Math.abs(actualValue) - limitValue;
if (margin < 0) {
hasNegativeMargin = true;
break;
}
}
}
}
}
data.result = hasNegativeMargin ? 'fail' : 'pass';
setResultData(data);
} catch (error) {
console.error('Error calculating margins:', error);
setResultData({ result: 'fail' });
}
} catch (error) {
console.error('加载测试结果失败:', error);
setResultData({ result: 'fail' });
}
}
};
loadTestResult();
}, [initialTestResult]);
useEffect(() => {
if (resultData) {
setTestResult({
...initialTestResult,
resultdata: resultData
});
}
}, [resultData]);
useEffect(() => {
navigateTo(navigation.current.name, navigation.current.view, { ...navigation.current.params, activeTab });
}, [activeTab]);
const tabs = testResult?.CopperWiremapResultStatus === 'pass' ? (testResult?.resultdata?.result === 'pass' ? ['布线图', '性能'] : ['布线图', '性能', '诊断']) : ['布线图'];
//console.log(testResult);
const renderContent = () => {
switch (activeTab) {
case '布线图':
return (
<div className="w-full h-full flex items-center justify-center">
<WireMapView testResult={testResult} />
</div>
);
case '性能':
return (
<div className="w-full h-full flex items-center justify-center">
<PerformanceView testResult={testResult} />
</div>
);
case '诊断':
return (
<div className="w-full h-full flex items-center justify-center">
<DiagnosticView testResult={testResult} />
</div>
);
default:
return null;
}
};
return (
<div className="w-full h-[490px] flex flex-col overflow-hidden">
<div className="h-[1/100] flex">
{tabs.map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`flex-1 h-full flex items-center justify-center font-bold text-lg
${activeTab === tab
? 'bg-gradient-to-b from-[#b0b0b0] via-[#e0e4e0] to-[#b0b0b0] text-black'
: 'bg-[#303030] text-[#fffe92]'}`}
>
{tab}
</button>
))}
</div>
<div className="flex-1 bg-white">
{renderContent()}
</div>
</div>
);
}

View File

@@ -0,0 +1,634 @@
import React, { useEffect, useState } from 'react';
import useDeviceStore from '@/store/deviceStore';
import * as echarts from 'echarts';
// 生成OTDR曲线数据
const generateOTDRData = (events, wavelength) => {
const points = [];
// 根据波长设置不同参数
let samplingInterval, baseAttenuation, noiseRange, reflectionDecay;
if (wavelength === '850') {
samplingInterval = 0.5; // 采样间隔更密
baseAttenuation = 3.0; // 多模850nm衰减更大
noiseRange = 0.15; // 噪声更大
reflectionDecay = 0.5; // 反射衰减更快
} else if (wavelength === '1300') {
samplingInterval = 0.6;
baseAttenuation = 1.0; // 多模1300nm衰减适中
noiseRange = 0.12;
reflectionDecay = 0.45;
} else if (wavelength === '1310') {
samplingInterval = 0.8;
baseAttenuation = 0.35; // 单模1310nm衰减较小
noiseRange = 0.1;
reflectionDecay = 0.4;
} else { // 1550nm
samplingInterval = 1.0; // 采样间隔更大
baseAttenuation = 0.2; // 单模1550nm衰减最小
noiseRange = 0.08; // 噪声更小
reflectionDecay = 0.35; // 反射衰减更慢
}
// 获取首个和最后一个事件的距离
const eventDistances = Object.values(events).map(event => event.distance);
const minDistance = Math.min(...eventDistances) - 20; // 首个事件前20米
const maxDistance = Math.max(...eventDistances) + 20; // 最后事件后20米
let currentDistance = minDistance;
let currentDB = 0;
// 生成基础曲线数据
while (currentDistance <= maxDistance) {
// 添加随机噪声
const noise = (Math.random() - 0.5) * noiseRange;
// 计算基础衰减
const attenuation = (currentDistance * baseAttenuation) / 1000;
points.push([currentDistance, currentDB - attenuation + noise]);
currentDistance += samplingInterval;
}
// 添加事件特征
Object.values(events).forEach(event => {
const eventIndex = Math.floor((event.distance - minDistance) / samplingInterval);
if (eventIndex >= 0 && eventIndex < points.length) {
if (event.type === 'Reflector' || event.type === 'Start' || event.type === 'End' ||
event.type === 'StartRef' || event.type === 'EndRef'|| event.type === 'EndNoRef') {
// 反射事件:添加尖峰
const peakHeight = event.reflLoss ? -event.reflLoss : 10;
// 前面1个点快速升高
if (eventIndex > 0) {
points[eventIndex - 1][1] = peakHeight / 3;
}
// 峰值点
points[eventIndex][1] = peakHeight;
// 后面20个点平滑回落使用指数衰减函数
for (let i = eventIndex + 1; i < Math.min(points.length, eventIndex + 20); i++) {
const distance = i - eventIndex;
points[i][1] = peakHeight * Math.exp(-distance * reflectionDecay);
}
} else if (event.type === 'Splice' || event.spliceLoss) {
// 非反射事件:添加阶跃式衰减
const loss = event.spliceLoss || 0.2;
for (let i = eventIndex; i < points.length; i++) {
points[i][1] -= loss;
}
}
}
});
return points;
};
const CurveChart = ({ testResult }) => {
const { connectionStatus } = useDeviceStore();
const [eventData, setEventData] = useState(null);
const [currentWavelength, setCurrentWavelength] = useState(null);
const [wavelengthData, setWavelengthData] = useState({});
// 判断是否为多模光纤
const isMultiMode = testResult?.testconfig?.params?.cableType.includes('OM');
// 加载事件数据
// 初始化echarts实例
useEffect(() => {
const chartDom = document.getElementById('otdrChart');
if (!chartDom) return;
const myChart = echarts.init(chartDom);
// 监听窗口大小变化
const handleResize = () => {
myChart.resize();
};
window.addEventListener('resize', handleResize);
// 设置初始图表配置
const option = {
// title: {
// text: 'OTDR曲线图',
// left: 'center'
// },
// tooltip: {
// trigger: 'axis',
// formatter: function(params) {
// return `距离: ${params[0].data[0].toFixed(2)}m<br/>dB: ${params[0].data[1].toFixed(2)}`;
// }
// },
grid: {
left: '3%',
right: '3%',
bottom: '5%',
top: '3%',
containLabel: true
},
xAxis: {
type: 'value',
// name: '距离(m)',
// nameLocation: 'middle',
// nameGap: 30,
min: function(value) {
return value.min - 20;
},
max: function(value) {
return value.max + 20;
},
axisLine: {
show: false
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: '#E0E0E0'
}
}
},
yAxis: {
type: 'value',
name: 'dB',
nameLocation: 'middle',
nameGap: 50,
min: -10,
max: 40,
axisLine: {
show: false
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: '#E0E0E0'
}
}
},
series: [{
type: 'line',
data: [],
showSymbol: false,
lineStyle: {
width: 1
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(58,77,233,0.8)'
},
{
offset: 1,
color: 'rgba(58,77,233,0.1)'
}
])
}
}]
};
myChart.setOption(option);
return () => {
window.removeEventListener('resize', handleResize);
myChart.dispose();
};
}, []);
// 更新图表数据
useEffect(() => {
const loadTraceData = async () => {
try {
const wavelength1 = isMultiMode ? '850' : '1310';
const wavelength2 = isMultiMode ? '1300' : '1550';
const [data1, data2] = await Promise.all([
import(`@/store/FIBER/${testResult.ofpResult}/${wavelength1}-dump.json`),
import(`@/store/FIBER/${testResult.ofpResult}/${wavelength2}-dump.json`)
]);
// 根据基准设置重新创建事件数据
const processWavelengthData = (data) => {
const mergedEvents = {};
// 1. 首先添加原始事件数据
Object.keys(data.event).forEach(eventKey => {
mergedEvents[eventKey] = {...data.event[eventKey]};
});
// 2. 根据基准设置调整事件类型
const lastEventKey = Object.keys(mergedEvents)[Object.keys(mergedEvents).length - 1];
if (!testResult.ofpRefStatus) {
// 无基准设置
// 判断首根光纤类型
if (connectionStatus.OFPoutRefStatus?.includes('sm-') || connectionStatus.OFPoutRefStatus?.includes('mm-')) {
// 短跳线
mergedEvents['event0'] = { type: "Start", distance: 0, reflLoss: -35.25 };
mergedEvents['event1'] = { type: "Hidden", distance: 2, reflLoss: -14.18, spliceLoss: null };
// 调整所有其他事件的位置增加2米
Object.keys(mergedEvents).forEach(key => {
if (key !== 'event0' && key !== 'event1') {
mergedEvents[key] = {
...mergedEvents[key],
distance: mergedEvents[key].distance + 2
};
}
});
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
spliceLoss: null
};
} else if (connectionStatus.OFPoutRefStatus?.includes('smc-') || connectionStatus.OFPoutRefStatus?.includes('mmc-')) {
// 补偿光纤
mergedEvents['event0'] = { type: "Start", distance: 0, reflLoss: -35.25 };
// 调整所有其他事件的位置增加161.15米
Object.keys(mergedEvents).forEach(key => {
if (key !== 'event0') {
mergedEvents[key] = {
...mergedEvents[key],
distance: mergedEvents[key].distance + 161.15
};
}
});
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
spliceLoss: null
};
}
// 判断最后连接的光纤类型
if (!connectionStatus.OFPConnectedToRefStatus) {
// 无连接,不操作
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('sm-') || connectionStatus.OFPConnectedToRefStatus?.includes('mm-')) {
// 短跳线
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('smc-') || connectionStatus.OFPConnectedToRefStatus?.includes('mmc-')) {
// 补偿光纤
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 161.15,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
}
} else if (testResult.ofpRefStatus === 'start') {
// 仅前导基准
// 判断首根光纤类型
if (connectionStatus.OFPoutRefStatus?.includes('sm-') || connectionStatus.OFPoutRefStatus?.includes('mm-')) {
// 短跳线
mergedEvents['event0'] = { type: "Start", distance: 0, reflLoss: -19.44 };
mergedEvents['event1'] = { type: "Hidden", distance: 2, reflLoss: -33.44, spliceLoss: null };
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null
};
} else if (connectionStatus.OFPoutRefStatus?.includes('smc-') || connectionStatus.OFPoutRefStatus?.includes('mmc-')) {
// 补偿光纤
mergedEvents['event0'] = { type: "StartRef", distance: -161.15, reflLoss: -33.79 };
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null
};
}
// 判断最后连接的光纤类型
if (!connectionStatus.OFPConnectedToRefStatus) {
// 无连接,不操作
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('sm-') || connectionStatus.OFPConnectedToRefStatus?.includes('mm-')) {
// 短跳线
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('smc-') || connectionStatus.OFPConnectedToRefStatus?.includes('mmc-')) {
// 补偿光纤
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: 0.1
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 161.15,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
}
} else if (testResult.ofpRefStatus === 'end') {
// 前导和末导基准
// 判断首根光纤类型
if (connectionStatus.OFPoutRefStatus?.includes('sm-') || connectionStatus.OFPoutRefStatus?.includes('mm-')) {
// 短跳线
mergedEvents['event0'] = { type: "Start", distance: 0, reflLoss: -19.44 };
mergedEvents['event1'] = { type: "Hidden", distance: 2, reflLoss: -33.44, spliceLoss: null };
// 调整所有其他事件的位置增加2米
Object.keys(mergedEvents).forEach(key => {
if (key !== 'event0' && key !== 'event1') {
mergedEvents[key] = {
...mergedEvents[key],
distance: mergedEvents[key].distance + 2
};
}
});
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null
};
} else if (connectionStatus.OFPoutRefStatus?.includes('smc-') || connectionStatus.OFPoutRefStatus?.includes('mmc-')) {
// 补偿光纤
mergedEvents['event0'] = { type: "StartRef", distance: -161.15, reflLoss: -33.79 };
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null
};
}
// 判断最后连接的光纤类型
if (!connectionStatus.OFPConnectedToRefStatus) {
// 无连接
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "EndNoRef",
spliceLoss: null
};
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('sm-') || connectionStatus.OFPConnectedToRefStatus?.includes('mm-')) {
// 短跳线
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('smc-') || connectionStatus.OFPConnectedToRefStatus?.includes('mmc-')) {
// 补偿光纤
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加末尾补偿事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "EndRef",
distance: lastEvent.distance + 161.15,
reflLoss: -28.54
};
}
}
// 更新summary信息
const eventKeys = Object.keys(mergedEvents).sort((a, b) => {
const numA = parseInt(a.replace('event', ''));
const numB = parseInt(b.replace('event', ''));
return numA - numB;
});
const summarylastEventKey = eventKeys[eventKeys.length - 1];
const lastEvent = mergedEvents[summarylastEventKey];
// 计算总损耗
let totalLoss = null;
const allSpliceLossNull = Object.values(mergedEvents).every(event =>
event.spliceLoss === null || event.spliceLoss === undefined
);
if (!allSpliceLossNull) {
totalLoss = Object.values(mergedEvents).reduce((sum, event) => {
const loss = event.spliceLoss;
if (loss === null || loss === undefined) return sum;
return sum + loss;
}, 0);
}
// 获取正确的distance值
let finalDistance = lastEvent.distance;
if (testResult.ofpRefStatus === 'end' &&
(connectionStatus.OFPConnectedToRefStatus?.includes('smc-') ||
connectionStatus.OFPConnectedToRefStatus?.includes('mmc-'))) {
// 如果是末导基准且连接了补偿光纤使用倒数第二个事件的distance
const secondLastEventKey = eventKeys[eventKeys.length - 2];
const secondLastEvent = mergedEvents[secondLastEventKey];
finalDistance = secondLastEvent.distance;
}
const updatedSummary = {
...data1.Summary,
totalDistance: finalDistance,
totalLoss: totalLoss
};
return {
events: mergedEvents,
summary: updatedSummary
};
};
const processedData1 = processWavelengthData(data1);
const processedData2 = processWavelengthData(data2);
setWavelengthData({
[wavelength1]: processedData1,
[wavelength2]: processedData2
});
setCurrentWavelength(wavelength1);
setEventData(processedData1);
// 获取当前波长的事件数据
const currentEvents = processedData1.events;
// 生成OTDR曲线数据不再传入 totalDistance 参数,让 generateOTDRData 自己计算范围
const otdrData = generateOTDRData(currentEvents);
// 更新echarts图表
const chartDom = document.getElementById('otdrChart');
if (!chartDom) return;
const myChart = echarts.getInstanceByDom(chartDom);
if (myChart) {
// 从事件数据中获取距离范围
const eventDistances = Object.values(currentEvents).map(event => event.distance);
const minDistance = Math.min(...eventDistances) - 20; // 首个事件前20米
const maxDistance = Math.max(...eventDistances) + 20; // 最后事件后20米
myChart.setOption({
xAxis: {
min: minDistance,
max: maxDistance
},
series: [{
data: otdrData
}]
});
}
} catch (error) {
console.error('Failed to load trace data:', error);
}
};
loadTraceData();
}, [testResult]);
return (
<div className="w-full h-full flex flex-col bg-white p-4">
<div className="flex-1" id="otdrChart" style={{ width: '100%', height: '100%' }}>
{/* 曲线图显示区 */}
</div>
<div className="h-[10%] flex items-center justify-center space-x-4">
<button
onClick={() => {
const wavelengths = Object.keys(wavelengthData);
const currentIndex = wavelengths.indexOf(currentWavelength);
const prevWavelength = wavelengths[(currentIndex - 1 + wavelengths.length) % wavelengths.length];
setCurrentWavelength(prevWavelength);
const prevData = wavelengthData[prevWavelength];
setEventData(prevData);
// 重新生成OTDR曲线数据
const otdrData = generateOTDRData(prevData.events, prevWavelength);
// 更新echarts图表
const chartDom = document.getElementById('otdrChart');
if (!chartDom) return;
const myChart = echarts.getInstanceByDom(chartDom);
if (myChart) {
// 从事件数据中获取距离范围
const eventDistances = Object.values(prevData.events).map(event => event.distance);
const minDistance = Math.min(...eventDistances) - 20;
const maxDistance = Math.max(...eventDistances) + 20;
myChart.setOption({
xAxis: {
min: minDistance,
max: maxDistance
},
series: [{
data: otdrData
}]
});
}
}}
className="p-2 rounded-full bg-gray-300 hover:bg-gray-400"
>
&#9664;
</button>
<span className="text-lg font-semibold">{currentWavelength}nm</span>
<button
onClick={() => {
const wavelengths = Object.keys(wavelengthData);
const currentIndex = wavelengths.indexOf(currentWavelength);
const nextWavelength = wavelengths[(currentIndex + 1) % wavelengths.length];
setCurrentWavelength(nextWavelength);
const nextData = wavelengthData[nextWavelength];
setEventData(nextData);
// 重新生成OTDR曲线数据
const otdrData = generateOTDRData(nextData.events, nextWavelength);
// 更新echarts图表
const chartDom = document.getElementById('otdrChart');
if (!chartDom) return;
const myChart = echarts.getInstanceByDom(chartDom);
if (myChart) {
// 从事件数据中获取距离范围
const eventDistances = Object.values(nextData.events).map(event => event.distance);
const minDistance = Math.min(...eventDistances) - 20;
const maxDistance = Math.max(...eventDistances) + 20;
myChart.setOption({
xAxis: {
min: minDistance,
max: maxDistance
},
series: [{
data: otdrData
}]
});
}
}}
className="p-2 rounded-full bg-gray-300 hover:bg-gray-400"
>
&#9654;
</button>
</div>
</div>
);
};
export default CurveChart;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import Image from 'next/image';
import useDisplayStore from '@/store/displayStore';
export default function DiagnosticView({ testResult }) {
const { navigateTo } = useDisplayStore();
const handleRowClick = (curtitle) => {
navigateTo('copperperformance', 'HDTD', { testResult, curtitle });
};
const renderRow = (title) => (
<div
className="w-full flex items-center justify-between p-3 mb-2 rounded-md bg-gradient-to-b from-[#dedede] via-[#b5b5b5] to-[#8b898b] shadow-md cursor-pointer"
onClick={() => handleRowClick(title)}
>
<span className="text-lg font-medium text-black">{title}</span>
</div>
);
return (
<div className="w-full h-[470px] bg-[#6b6d6b] flex flex-col p-4 overflow-y-auto" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
{renderRow('HDTDR')}
{renderRow('HDTDX')}
</div>
);
}

View File

@@ -0,0 +1,718 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import useDisplayStore from '@/store/displayStore';
import Image from 'next/image';
import { useRef } from 'react';
export default function EventMapView({ testResult }) {
const connectionStatus = testResult.ofpConnectionStatus;
const { view } = useDisplayStore.getState().navigation.current;
const currentCableId = useDisplayStore.getState().getCurrentCableId();
const [selectedEvent, setSelectedEvent] = useState(null);
const [eventData, setEventData] = useState({});
const eventRefs = useRef({});
const isMultiMode = testResult?.testconfig?.params?.cableType.includes('OM');
// 加载事件数据
useEffect(() => {
const loadTraceData = async () => {
try {
const wavelength1 = isMultiMode ? '850' : '1310';
const wavelength2 = isMultiMode ? '1300' : '1550';
const [data1, data2] = await Promise.all([
import(`@/store/FIBER/${testResult.ofpResult}/${wavelength1}-dump.json`),
import(`@/store/FIBER/${testResult.ofpResult}/${wavelength2}-dump.json`)
]);
// 根据基准设置重新创建事件数据
const mergedEvents = {};
let firstEvent = null;
// 1. 首先添加原始事件数据
Object.keys(data1.event).forEach(eventKey => {
mergedEvents[eventKey] = {...data1.event[eventKey]}; // 使用展开运算符创建新对象
});
// 2. 添加第二个波长的特殊事件
Object.keys(data2.event).forEach(eventKey => {
const event1 = data1.event[eventKey];
const event2 = data2.event[eventKey];
if (event2 && (!event1 || event2.type !== event1.type)) {
mergedEvents[eventKey] = {...event2}; // 使用展开运算符创建新对象
}
});
// 3. 根据当前基准设置完全重新组织事件结构
const lastEventKey = Object.keys(mergedEvents)[Object.keys(mergedEvents).length - 1];
const firstEventKey = Object.keys(mergedEvents)[0];
if (!testResult.ofpRefStatus) {
// 无基准设置
// 判断首根光纤类型
if (connectionStatus.OFPoutRefStatus?.includes('sm-') || connectionStatus.OFPoutRefStatus?.includes('mm-')) {
// 短跳线
mergedEvents['event0'] = { type: "Start", distance: 0, reflLoss: -35.25 };
mergedEvents['event1'] = { type: "Hidden", distance: 2, reflLoss: -14.18, spliceLoss: null };
// 调整所有其他事件的位置增加2米
Object.keys(mergedEvents).forEach(key => {
if (key !== 'event0' && key !== 'event1') {
mergedEvents[key] = {
...mergedEvents[key],
distance: mergedEvents[key].distance + 2
};
}
});
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
spliceLoss: null
};
} else if (connectionStatus.OFPoutRefStatus?.includes('smc-') || connectionStatus.OFPoutRefStatus?.includes('mmc-')) {
// 补偿光纤
mergedEvents['event0'] = { type: "Start", distance: 0, reflLoss: -35.25 };
// 调整所有其他事件的位置增加161.15米
Object.keys(mergedEvents).forEach(key => {
if (key !== 'event0') {
mergedEvents[key] = {
...mergedEvents[key],
distance: mergedEvents[key].distance + 161.15
};
}
});
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
spliceLoss: null
};
}
// 判断最后连接的光纤类型
if (!connectionStatus.OFPConnectedToRefStatus) {
// 无连接,不操作
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('sm-') || connectionStatus.OFPConnectedToRefStatus?.includes('mm-')) {
// 短跳线
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('smc-') || connectionStatus.OFPConnectedToRefStatus?.includes('mmc-')) {
// 补偿光纤
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 161.15,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
}
} else if (testResult.ofpRefStatus === 'start') {
// 仅前导基准
// 判断首根光纤类型
if (connectionStatus.OFPoutRefStatus?.includes('sm-') || connectionStatus.OFPoutRefStatus?.includes('mm-')) {
// 短跳线
mergedEvents['event0'] = { type: "Start", distance: 0, reflLoss: -19.44 };
mergedEvents['event1'] = { type: "Hidden", distance: 2, reflLoss: -33.44, spliceLoss: null };
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null
};
} else if (connectionStatus.OFPoutRefStatus?.includes('smc-') || connectionStatus.OFPoutRefStatus?.includes('mmc-')) {
// 补偿光纤
mergedEvents['event0'] = { type: "StartRef", distance: -161.15, reflLoss: -33.79 };
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null
};
}
// 判断最后连接的光纤类型
if (!connectionStatus.OFPConnectedToRefStatus) {
// 无连接,不操作
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('sm-') || connectionStatus.OFPConnectedToRefStatus?.includes('mm-')) {
// 短跳线
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('smc-') || connectionStatus.OFPConnectedToRefStatus?.includes('mmc-')) {
// 补偿光纤
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: 0.1
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 161.15,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
}
} else if (testResult.ofpRefStatus === 'end') {
// 前导和末导基准
// 判断首根光纤类型
if (connectionStatus.OFPoutRefStatus?.includes('sm-') || connectionStatus.OFPoutRefStatus?.includes('mm-')) {
// 短跳线
mergedEvents['event0'] = { type: "Start", distance: 0, reflLoss: -19.44 };
mergedEvents['event1'] = { type: "Hidden", distance: 2, reflLoss: -33.44, spliceLoss: null };
// 调整所有其他事件的位置增加2米
Object.keys(mergedEvents).forEach(key => {
if (key !== 'event0' && key !== 'event1') {
mergedEvents[key] = {
...mergedEvents[key],
distance: mergedEvents[key].distance + 2
};
}
});
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null
};
} else if (connectionStatus.OFPoutRefStatus?.includes('smc-') || connectionStatus.OFPoutRefStatus?.includes('mmc-')) {
// 补偿光纤
mergedEvents['event0'] = { type: "StartRef", distance: -161.15, reflLoss: -33.79 };
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null
};
}
// 判断最后连接的光纤类型
if (!connectionStatus.OFPConnectedToRefStatus) {
// 无连接
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "EndNoRef",
spliceLoss: null
};
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('sm-') || connectionStatus.OFPConnectedToRefStatus?.includes('mm-')) {
// 短跳线
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('smc-') || connectionStatus.OFPConnectedToRefStatus?.includes('mmc-')) {
// 补偿光纤
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加末尾补偿事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "EndRef",
distance: lastEvent.distance + 161.15,
reflLoss: -28.54
};
}
}
// 更新summary信息
const eventKeys = Object.keys(mergedEvents).sort((a, b) => {
const numA = parseInt(a.replace('event', ''));
const numB = parseInt(b.replace('event', ''));
return numA - numB;
});
const summarylastEventKey = eventKeys[eventKeys.length - 1];
const lastEvent = mergedEvents[summarylastEventKey];
// 计算总损耗
let totalLoss = null;
const allSpliceLossNull = Object.values(mergedEvents).every(event =>
event.spliceLoss === null || event.spliceLoss === undefined
);
if (!allSpliceLossNull) {
totalLoss = Object.values(mergedEvents).reduce((sum, event) => {
const loss = event.spliceLoss;
if (loss === null || loss === undefined) return sum;
return sum + loss;
}, 0).toFixed(2); // 总损耗保留两位小数
}
// 获取正确的distance值
let finalDistance = lastEvent.distance;
if (testResult.ofpRefStatus === 'end' &&
(connectionStatus.OFPConnectedToRefStatus?.includes('smc-') ||
connectionStatus.OFPConnectedToRefStatus?.includes('mmc-'))) {
// 如果是末导基准且连接了补偿光纤使用倒数第二个事件的distance
const secondLastEventKey = eventKeys[eventKeys.length - 2];
const secondLastEvent = mergedEvents[secondLastEventKey];
finalDistance = secondLastEvent.distance;
}
const updatedSummary = {
...data1.Summary,
totalDistance: Number(finalDistance.toFixed(2)), // 总距离保留两位小数
totalLoss: totalLoss !== null ? Number(totalLoss) : null // 如果有损耗值则转为数字
};
setEventData({
events: mergedEvents,
summary: updatedSummary
});
// 获取第一个事件
firstEvent = mergedEvents[Object.keys(mergedEvents)[0]];
// 设置第一个事件为选中状态
setSelectedEvent(firstEvent);
} catch (error) {
console.error('Error loading trace data:', error);
}
};
if (testResult?.ofpResult) {
loadTraceData();
}
}, [testResult, isMultiMode]);
const processedEvents = useMemo(() => {
if (!eventData?.events) return [];
return Object.entries(eventData.events)
.sort((a, b) => parseInt(a[0].replace('event', '')) - parseInt(b[0].replace('event', '')))
.reverse();
}, [eventData?.events]);
const getEventStatus = useCallback((event, nextEvent, prevEvent) => {
if (event.type === 'Reflector') {
if (event.spliceLoss > 0.75) return 'fail';
if (nextEvent?.type === 'StartRef') return 'leading';
if (prevEvent?.type === 'EndRef') return 'trailing';
return 'normal';
}
if (event.type === 'Splice') {
return event.spliceLoss > 0.35 ? 'fail' : 'normal';
}
return 'normal';
}, []);
const getConnectionLineHeight = useCallback((eventsLength) => {
if (eventsLength <= 3) return '130px';
if (eventsLength <= 4) return '100px';
if (eventsLength <= 5) return '80px';
if (eventsLength <= 6) return '65px';
return '45px';
}, []);
const renderEventComponent = useCallback((event, status) => {
switch(event.type) {
case 'End': return <StartPoint status='normal'/>;
case 'EndRef': return <EndRef />;
case 'EndNoRef': return <EndNoRef />;
case 'EndNoFiber': return <EndNoFiber />;
case 'Reflector': return <Reflector status={status} />;
case 'Hidden': return <Reflector status='hidden' />;
case 'Splice': return <SplicePoint status={status} />;
case 'Bend': return <BendEvent />;
case 'Start': return <StartPoint status='normal'/>;
case 'StartRef': return <StartRef />;
default: return null;
}
}, []);
// 反射器样式
const Reflector = ({ status = 'normal' }) => {
return (
<div className="flex justify-center items-center w-[8%] min-w-[15px] max-w-[20px] flex-shrink-0">
<div className={`w-[80%] h-0 pb-[120%] relative
${status === 'hidden' ? 'border-1 border-blue-500 bg-white' : ''}`}
>
{/* 中间横条 - 所有状态都显示 */}
<div className={`w-[133%] h-[20%] absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10
${status === 'hidden' ? 'border-1 border-blue-500 bg-white' :
status === 'fail' ? 'bg-gradient-to-r from-[#8b0000] to-[#cc0000]' :
status === 'leading' ? 'bg-gradient-to-r from-[#1a365d] to-[#2a4a7f]' :
status === 'trailing' ? 'bg-gradient-to-r from-[#1a365d] to-[#2a4a7f]' :
'bg-gradient-to-r from-[#1a365d] to-[#2a4a7f]'}`}
/>
{status !== 'hidden' && (
<>
{/* Top half */}
<div className={`absolute top-0 left-0 right-0 h-[50%]
${status === 'fail' ? 'bg-[#8b0000]' :
status === 'leading' ? 'bg-[#1a365d]' :
status === 'trailing' ? 'bg-[#949294]' :
'bg-[#1a365d]'}`}
/>
{/* Bottom half */}
<div className={`absolute bottom-0 left-0 right-0 h-[50%]
${status === 'fail' ? 'bg-[#cc0000]' :
status === 'leading' ? 'bg-[#949294]' :
status === 'trailing' ? 'bg-[#2a4a7f]' :
'bg-[#2a4a7f]'}`}
/>
{status === 'fail' && (
<div className="absolute w-[80%] h-[80%] top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10">
<div className="absolute w-full h-[3px] bg-white top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-[1.5px] rotate-45" />
<div className="absolute w-full h-[3px] bg-white top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-[1.5px] -rotate-45" />
</div>
)}
</>
)}
</div>
</div>
);
};
//起始点样式
const StartPoint = ({ status = 'normal' }) => {
return (
<div className="flex justify-center items-center w-[8%] min-w-[15px] max-w-[20px] flex-shrink-0">
<div className="w-[80%] h-0 pb-[60%] relative">
{/* Top half only */}
<div className={`absolute top-0 left-0 right-0 h-full
${status === 'unnormal' ? 'bg-[#949294]' : 'bg-[#1a365d]'}`}
/>
</div>
</div>
);
};
//前导样式
const StartRef = () => {
return (
<div>
<StartPoint status = 'normal'/>
</div>
);
};
//末导样式
const EndRef = () => {
return (
<div>
<StartPoint status = 'unnormal'/>
</div>
);
};
// 无末导事件样式
const EndNoRef = () => {
return (
// <div className=" w-full h-[3px] bg-[#cc0000] top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-[1.5px] -rotate-45" />
<div className={`w-4 h-4 rounded-full relative`}>
<div className="absolute w-full h-full top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<div className=" w-full h-[3px] bg-[#cc0000] translate-y-3 rounded-[1.5px] rotate-45" />
<div className=" w-full h-[3px] bg-[#cc0000] translate-y-9/3 rounded-[1.5px] -rotate-45" />
</div>
</div>
);
};
// 无光纤
const EndNoFiber = () => {
return (
// <div className=" w-full h-[3px] bg-[#cc0000] top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-[1.5px] -rotate-45" />
<div className={`w-4 h-4 rounded-full relative`}>
<div className="absolute w-full h-full top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<div className=" w-full h-[3px] bg-[#cc0000] translate-y-3 rounded-[1.5px] rotate-45" />
<div className=" w-full h-[3px] bg-[#cc0000] translate-y-9/3 rounded-[1.5px] -rotate-45" />
</div>
</div>
);
};
// 熔接点样式
const SplicePoint = ({ status = 'normal' }) => {
return (
<div className={`w-4 h-4 rounded-full relative
${status === 'fail' ? 'bg-gradient-to-r from-[#8b0000] to-[#cc0000]' :
'bg-gradient-to-r from-[#1a365d] to-[#2a4a7f]'}`}
>
{status === 'fail' && (
<div className="absolute w-full h-full top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<div className="absolute w-[80%] h-[3px] bg-white top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-[1.5px] rotate-45" />
<div className="absolute w-[80%] h-[3px] bg-white top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-[1.5px] -rotate-45" />
</div>
)}
</div>
);
};
// 弯曲事件样式
const BendEvent = () => {
return (
<div className="w-5 h-5 rounded-full border-[5px] border-[#cc0000] bg-transparent
-mr-0.5 flex-shrink-0"
style={{ clipPath: 'inset(0 50% 0 0)' }} />
);
};
return (
<div className="w-full h-full flex flex-row bg-[#f0f0f0]"> {/* Changed to flex-row and added background */}
{/* Left Area (1/3 width) */}
<div className="w-1/3 h-full flex flex-col">
<div className="w-full h-[100%] bg-white p-2 flex items-end justify-center"> {/* 改为items-end使内容底部对齐 */}
<div className="w-full h-[90%] bg-white p-2 flex flex-col items-center justify-end relative">
{processedEvents.map(([key, event], index, array) => {
const nextEvent = array[index + 1]?.[1];
const prevEvent = array[index - 1]?.[1];
const distance = nextEvent ?
(event.distance - nextEvent.distance).toFixed(1) :
(eventData.summary.totalDistance - event.distance).toFixed(1);
const status = getEventStatus(event, nextEvent, prevEvent);
return (
<div key={key} className="flex flex-col items-center">
<div
ref={el => eventRefs.current[key] = el}
onClick={() => setSelectedEvent(event)}
className="cursor-pointer"
>
{renderEventComponent(event, status)}
</div>
{index < array.length - 1 && (
<div className="flex items-center relative">
<div className="absolute left-[-65px] text-sm text-black whitespace-nowrap">
{distance} m
</div>
<div
className={`w-[2px] ${nextEvent?.type === 'StartRef' || event.type === 'EndRef' ? 'bg-gray-400' : 'bg-black'}`}
style={{ height: getConnectionLineHeight(array.length) }}
/>
</div>
)}
</div>
);
})}
<Image
src="/otdr-start.png"
alt="OTDR Start"
width={40}
height={40}
/>
</div>
</div>
</div>
{/* Right Area (2/3 width) */}
<div className="w-2/3 h-full flex flex-col"> {/* Added right column */}
{/* Right Top (20%) */}
<div className="h-[20%] bg-white p-3 flex justify-between items-start ">
<div>
<div className="flex items-center mb-1">
<span className="text-black font-bold">光纤长度:</span>
<span className="text-black ml-1">{eventData?.summary?.totalDistance || '0'} m</span>
</div>
<div className="flex items-center">
<span className="text-black font-bold">总体损耗:</span>
<span className="text-black ml-1">{eventData?.summary?.totalLoss || 'N/A'} dB</span>
</div>
</div>
<div className="text-orange-500 font-bold">端点1</div>
</div>
{/* Right Middle (60%) */}
<div className="h-[60%] bg-white p-2 relative"> {/* Adjusted padding */}
{/* 事件简要框 */}
{selectedEvent && (
<div className="absolute p-4 rounded-lg shadow-lg bg-gradient-to-b from-gray-200 to-gray-300 border-2"
style={{
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '80%',
maxWidth: '350px',
zIndex: 10,
borderColor: selectedEvent.type === 'StartRef' || selectedEvent.type === 'Start' ? '#1a9850' :
selectedEvent.type === 'EndRef' || selectedEvent.type === 'End' ? '#1a365d' :
selectedEvent.spliceLoss > 0.75 || selectedEvent.spliceLoss > 0.35 ? '#cc0000' : '#1a365d'
}}
>
{/* 小尾巴 - 指向左侧事件 */}
<div className="absolute w-0 h-0 border-solid"
style={{
left: '-20px',
top: '50%',
transform: 'translateY(-50%)',
borderWidth: '10px 20px 10px 0',
borderColor: 'transparent ' +
(selectedEvent.type === 'StartRef' || selectedEvent.type === 'Start' ? '#1a9850' :
selectedEvent.type === 'EndRef' || selectedEvent.type === 'End' ? '#1a365d' :
selectedEvent.spliceLoss > 0.75 || selectedEvent.spliceLoss > 0.35 ? '#cc0000' : '#1a365d') +
' transparent transparent'
}}
/>
{/* 标题 */}
<div className="text-center font-bold text-xl mb-4">
{selectedEvent.type === 'StartRef' ? 'OTDR端口' :
selectedEvent.type === 'EndRef' ? '末尾事件' :
selectedEvent.type === 'EndNoRef' ? '未找到末尾事件' :
selectedEvent.type === 'Start' ? 'OTDR端口' :
selectedEvent.type === 'End' ? '端点' :
selectedEvent.type === 'Reflector' ? '反射器' :
selectedEvent.type === 'Hidden' ? '隐藏事件' :
selectedEvent.type === 'Splice' ? '熔接点' :
selectedEvent.type === 'Bend' ? '弯曲事件' : '事件'}
</div>
{/* 事件信息 */}
<div className="mb-2">
<div className="text-center mb-2">{selectedEvent.distance.toFixed(2)} m</div>
{/* 根据事件类型显示不同信息 */}
{(selectedEvent.type === 'Reflector' ||
selectedEvent.type === 'Hidden' ||
selectedEvent.type === 'EndRef') && (
<div>
<div className="flex justify-between items-center mb-2">
<div className="flex items-center">
<img
src={selectedEvent.spliceLoss > 0.75 ? '/fail.png' : '/pass.png'}
alt="status"
className="w-4 h-4 mr-2"
/>
<span className="font-bold">损耗:</span>
</div>
<span>{selectedEvent.spliceLoss ? selectedEvent.spliceLoss.toFixed(2) : '0'} dB</span>
</div>
<div className="flex justify-between items-center mb-2">
<div className="flex items-center">
<div className="w-4 h-4 mr-2"></div>
<span className="font-bold">反射率:</span>
</div>
<span>{selectedEvent.reflLoss ? selectedEvent.reflLoss.toFixed(2) : '0'} dB</span>
</div>
</div>
)}
{(selectedEvent.type === 'Splice' || selectedEvent.type === 'Bend') && (
<div className="flex justify-between items-center mb-2">
<div className="flex items-center">
<img
src={selectedEvent.type === 'Splice' ?
(selectedEvent.spliceLoss > 0.35 ? '/fail.png' : '/pass.png') :
(selectedEvent.spliceLoss > 0.75 ? '/fail.png' : '/pass.png')}
alt="status"
className="w-4 h-4 mr-2"
/>
<span className="font-bold">损耗:</span>
</div>
<span>{selectedEvent.spliceLoss ? selectedEvent.spliceLoss.toFixed(2) : '0'} dB</span>
</div>
)}
{(selectedEvent.type === 'StartRef' ||
selectedEvent.type === 'Start' ||
selectedEvent.type === 'End') && (
<div className="flex justify-between items-center mb-2">
<div className="flex items-center">
<div className="w-4 h-4 mr-2"></div>
<span className="font-bold">反射率:</span>
</div>
<span>{selectedEvent.reflLoss ? selectedEvent.reflLoss.toFixed(2) : '0'} dB</span>
</div>
)}
{/* 连接质量指示器 - 仅对某些事件类型显示 */}
{(selectedEvent.type === 'Start' || selectedEvent.type === 'StartRef') && (
<div className="mt-4">
<div className="text-center mb-2">端口连接质量</div>
<div className="relative h-4 bg-gray-200 rounded-full overflow-hidden">
<div className="absolute top-0 left-0 h-full w-full flex">
<div className="h-full bg-red-600" style={{width: '33%'}}></div>
<div className="h-full bg-yellow-400" style={{width: '33%'}}></div>
<div className="h-full bg-green-500" style={{width: '34%'}}></div>
</div>
<div className="absolute top-0 right-4 h-full flex items-center">
<div className="w-3 h-3 bg-black transform rotate-45"></div>
</div>
<div className="absolute bottom-[-20px] right-4 text-xs">良好</div>
</div>
</div>
)}
</div>
</div>
)}
</div>
{/* Right Bottom (20%) */}
<div className="h-[20%] bg-white p-2 flex flex-col items-start justify-center"> {/* Adjusted padding and alignment */}
<div className="text-black text-sm mb-1">光纤类型: {testResult?.testconfig?.params?.cableType || null}</div> {/* Example data */}
<div className="text-black text-sm mb-1">测试极限值: {testResult?.testconfig?.params?.limitValue || null}</div> {/* Example data */}
{view === 'nosave' && (
<div className="text-black text-sm">下一个ID: {currentCableId?.name || 'OTDR-02'}</div> /* Example data */
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,448 @@
import React, { useState, useEffect } from 'react';
import useDisplayStore from '@/store/displayStore';
import useDeviceStore from '@/store/deviceStore';
import Image from 'next/image';
export default function EventTable({ testResult }) {
const { connectionStatus } = useDeviceStore();
const [eventData, setEventData] = useState(null);
const [currentWavelength, setCurrentWavelength] = useState(null);
const [wavelengthData, setWavelengthData] = useState({});
// 判断是否为多模光纤
const isMultiMode = testResult?.testconfig?.params?.cableType.includes('OM');
// 加载事件数据
useEffect(() => {
const loadTraceData = async () => {
try {
const wavelength1 = isMultiMode ? '850' : '1310';
const wavelength2 = isMultiMode ? '1300' : '1550';
const [data1, data2] = await Promise.all([
import(`@/store/FIBER/${testResult.ofpResult}/${wavelength1}-dump.json`),
import(`@/store/FIBER/${testResult.ofpResult}/${wavelength2}-dump.json`)
]);
// 根据基准设置重新创建事件数据
const processWavelengthData = (data) => {
const mergedEvents = {};
// 1. 首先添加原始事件数据
Object.keys(data.event).forEach(eventKey => {
mergedEvents[eventKey] = {...data.event[eventKey]};
});
// 2. 根据基准设置调整事件类型
const lastEventKey = Object.keys(mergedEvents)[Object.keys(mergedEvents).length - 1];
if (!testResult.ofpRefStatus) {
// 无基准设置
// 判断首根光纤类型
if (connectionStatus.OFPoutRefStatus?.includes('sm-') || connectionStatus.OFPoutRefStatus?.includes('mm-')) {
// 短跳线
mergedEvents['event0'] = { type: "Start", distance: 0, reflLoss: -35.25 };
mergedEvents['event1'] = { type: "Hidden", distance: 2, reflLoss: -14.18, spliceLoss: null };
// 调整所有其他事件的位置增加2米
Object.keys(mergedEvents).forEach(key => {
if (key !== 'event0' && key !== 'event1') {
mergedEvents[key] = {
...mergedEvents[key],
distance: mergedEvents[key].distance + 2
};
}
});
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
spliceLoss: null
};
} else if (connectionStatus.OFPoutRefStatus?.includes('smc-') || connectionStatus.OFPoutRefStatus?.includes('mmc-')) {
// 补偿光纤
mergedEvents['event0'] = { type: "Start", distance: 0, reflLoss: -35.25 };
// 调整所有其他事件的位置增加161.15米
Object.keys(mergedEvents).forEach(key => {
if (key !== 'event0') {
mergedEvents[key] = {
...mergedEvents[key],
distance: mergedEvents[key].distance + 161.15
};
}
});
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
spliceLoss: null
};
}
// 判断最后连接的光纤类型
if (!connectionStatus.OFPConnectedToRefStatus) {
// 无连接,不操作
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('sm-') || connectionStatus.OFPConnectedToRefStatus?.includes('mm-')) {
// 短跳线
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('smc-') || connectionStatus.OFPConnectedToRefStatus?.includes('mmc-')) {
// 补偿光纤
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 161.15,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
}
} else if (testResult.ofpRefStatus === 'start') {
// 仅前导基准
// 判断首根光纤类型
if (connectionStatus.OFPoutRefStatus?.includes('sm-') || connectionStatus.OFPoutRefStatus?.includes('mm-')) {
// 短跳线
mergedEvents['event0'] = { type: "Start", distance: 0, reflLoss: -19.44 };
mergedEvents['event1'] = { type: "Hidden", distance: 2, reflLoss: -33.44, spliceLoss: null };
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null
};
} else if (connectionStatus.OFPoutRefStatus?.includes('smc-') || connectionStatus.OFPoutRefStatus?.includes('mmc-')) {
// 补偿光纤
mergedEvents['event0'] = { type: "StartRef", distance: -161.15, reflLoss: -33.79 };
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null
};
}
// 判断最后连接的光纤类型
if (!connectionStatus.OFPConnectedToRefStatus) {
// 无连接,不操作
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('sm-') || connectionStatus.OFPConnectedToRefStatus?.includes('mm-')) {
// 短跳线
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('smc-') || connectionStatus.OFPConnectedToRefStatus?.includes('mmc-')) {
// 补偿光纤
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: 0.1
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 161.15,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
}
} else if (testResult.ofpRefStatus === 'end') {
// 前导和末导基准
// 判断首根光纤类型
if (connectionStatus.OFPoutRefStatus?.includes('sm-') || connectionStatus.OFPoutRefStatus?.includes('mm-')) {
// 短跳线
mergedEvents['event0'] = { type: "Start", distance: 0, reflLoss: -19.44 };
mergedEvents['event1'] = { type: "Hidden", distance: 2, reflLoss: -33.44, spliceLoss: null };
// 调整所有其他事件的位置增加2米
Object.keys(mergedEvents).forEach(key => {
if (key !== 'event0' && key !== 'event1') {
mergedEvents[key] = {
...mergedEvents[key],
distance: mergedEvents[key].distance + 2
};
}
});
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null
};
} else if (connectionStatus.OFPoutRefStatus?.includes('smc-') || connectionStatus.OFPoutRefStatus?.includes('mmc-')) {
// 补偿光纤
mergedEvents['event0'] = { type: "StartRef", distance: -161.15, reflLoss: -33.79 };
// 修改最后一个事件
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null
};
}
// 判断最后连接的光纤类型
if (!connectionStatus.OFPConnectedToRefStatus) {
// 无连接
const lastEvent = mergedEvents[lastEventKey];
mergedEvents[lastEventKey] = {
...lastEvent,
type: "EndNoRef",
spliceLoss: null
};
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('sm-') || connectionStatus.OFPConnectedToRefStatus?.includes('mm-')) {
// 短跳线
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加新的结束事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "End",
distance: lastEvent.distance + 2,
spliceLoss: null,
reflLoss: lastEvent.reflLoss,
attenuation: lastEvent.attenuation
};
} else if (connectionStatus.OFPConnectedToRefStatus?.includes('smc-') || connectionStatus.OFPConnectedToRefStatus?.includes('mmc-')) {
// 补偿光纤
const lastEvent = mergedEvents[lastEventKey];
// 将最后一个事件改为反射器
mergedEvents[lastEventKey] = {
...lastEvent,
type: "Reflector",
spliceLoss: null
};
// 添加末尾补偿事件
mergedEvents[`event${Object.keys(mergedEvents).length}`] = {
type: "EndRef",
distance: lastEvent.distance + 161.15,
reflLoss: -28.54
};
}
}
// 更新summary信息
const eventKeys = Object.keys(mergedEvents).sort((a, b) => {
const numA = parseInt(a.replace('event', ''));
const numB = parseInt(b.replace('event', ''));
return numA - numB;
});
const summarylastEventKey = eventKeys[eventKeys.length - 1];
const lastEvent = mergedEvents[summarylastEventKey];
// 计算总损耗
let totalLoss = null;
const allSpliceLossNull = Object.values(mergedEvents).every(event =>
event.spliceLoss === null || event.spliceLoss === undefined
);
if (!allSpliceLossNull) {
totalLoss = Object.values(mergedEvents).reduce((sum, event) => {
const loss = event.spliceLoss;
if (loss === null || loss === undefined) return sum;
return sum + loss;
}, 0);
}
// 获取正确的distance值
let finalDistance = lastEvent.distance;
if (testResult.ofpRefStatus === 'end' &&
(connectionStatus.OFPConnectedToRefStatus?.includes('smc-') ||
connectionStatus.OFPConnectedToRefStatus?.includes('mmc-'))) {
// 如果是末导基准且连接了补偿光纤使用倒数第二个事件的distance
const secondLastEventKey = eventKeys[eventKeys.length - 2];
const secondLastEvent = mergedEvents[secondLastEventKey];
finalDistance = secondLastEvent.distance;
}
const updatedSummary = {
...data1.Summary,
totalDistance: finalDistance,
totalLoss: totalLoss
};
return {
events: mergedEvents,
summary: updatedSummary
};
};
const processedData1 = processWavelengthData(data1);
const processedData2 = processWavelengthData(data2);
setWavelengthData({
[wavelength1]: processedData1,
[wavelength2]: processedData2
});
setCurrentWavelength(wavelength1);
setEventData(processedData1);
} catch (error) {
console.error('Error loading trace data:', error);
}
};
if (testResult?.ofpResult) {
loadTraceData();
}
}, [testResult, isMultiMode]);
// 获取事件类型显示名称
const getEventTypeName = (type) => {
switch(type) {
case 'StartRef': return 'OTDR端口';
case 'EndRef': return '末尾事件';
case 'EndNoRef': return '末尾事件';
case 'Start': return 'OTDR端口';
case 'End': return '端点';
case 'Reflector': return '反射器';
case 'Hidden': return '隐藏事件';
case 'Splice': return '熔接点';
case 'Bend': return '弯曲事件';
default: return '事件';
}
};
// 获取事件状态图标
const getEventStatusIcon = (event) => {
if (event.type === 'Start' || event.type === 'StartRef') {
return '/pass.png';
} else if (event.type === 'End' || event.type === 'EndRef') {
return '/pass.png';
} else if (event.type === 'EndNoRef') {
return '/fail.png';
} else if (event.type === 'Reflector' && event.spliceLoss > 0.75) {
return '/fail.png';
} else if (event.type === 'Splice' && event.spliceLoss > 0.35) {
return '/fail.png';
} else if (event.type === 'Bend') {
return '/fail.png';
} else {
return '/pass.png';
}
};
return (
<div className="w-full h-full flex flex-col bg-white p-4">
{/* 表头 */}
<div className="flex border-b border-gray-300 py-2 font-bold text-black">
<div className="w-24 text-center">(m)</div>
<div className="w-24 text-center">损耗</div>
<div className="w-24 text-center">反射</div>
<div className="flex-1 text-center">类型</div>
<div className="w-16 text-center"></div>
</div>
{/* 表格内容 */}
<div className="flex-1 overflow-y-auto">
{eventData && Object.entries(eventData.events)
.sort((a, b) => b[1].distance - a[1].distance)
.map(([key, event]) => (
<div key={key} className="flex items-center border-b border-gray-200 py-2">
<div className="w-24 text-center text-black">
{event.distance.toFixed(2)}
</div>
<div className="w-24 text-center text-black">
{event.spliceLoss ? event.spliceLoss.toFixed(2) : 'N/A'}
</div>
<div className="w-24 text-center text-black">
{event.reflLoss ? event.reflLoss.toFixed(2) : 'N/A'}
</div>
<div className="flex-1 text-center text-black">
{getEventTypeName(event.type)}
</div>
<div className="w-16 flex justify-center">
<div className="w-5 h-5 relative">
<Image
src={getEventStatusIcon(event)}
alt="status"
fill
className="object-contain"
/>
</div>
</div>
</div>
))}
</div>
{/* 底部控制区 */}
<div className="h-[10%] flex items-center justify-center space-x-4">
<button
onClick={() => {
const wavelengths = Object.keys(wavelengthData);
const currentIndex = wavelengths.indexOf(currentWavelength);
const prevWavelength = wavelengths[(currentIndex - 1 + wavelengths.length) % wavelengths.length];
setCurrentWavelength(prevWavelength);
setEventData(wavelengthData[prevWavelength]);
}}
className="p-2 rounded-full bg-gray-300 hover:bg-gray-400"
>
&#9664;
</button>
<span className="text-lg font-semibold">{currentWavelength}nm</span>
<button
onClick={() => {
const wavelengths = Object.keys(wavelengthData);
const currentIndex = wavelengths.indexOf(currentWavelength);
const nextWavelength = wavelengths[(currentIndex + 1) % wavelengths.length];
setCurrentWavelength(nextWavelength);
setEventData(wavelengthData[nextWavelength]);
}}
className="p-2 rounded-full bg-gray-300 hover:bg-gray-400"
>
&#9654;
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
export default function FooterBar() {
return (
<div className="h-[60px] bg-[#303030]">
{/* 这里后续会添加底部栏内容 */}
</div>
);
}

View File

@@ -0,0 +1,425 @@
import React, { useEffect, useState, useRef } from 'react';
import ReactECharts from 'echarts-for-react';
import * as echarts from 'echarts/core';
import {
GridComponent,
TooltipComponent,
LegendComponent,
DataZoomComponent,
MarkLineComponent,
ToolboxComponent
} from 'echarts/components';
import { LineChart } from 'echarts/charts';
import { UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
// 注册必要的组件
echarts.use([
GridComponent,
TooltipComponent,
LegendComponent,
LineChart,
CanvasRenderer,
UniversalTransition,
DataZoomComponent,
MarkLineComponent,
ToolboxComponent
]);
const FrequencyChart = ({
data,
limitdata,
limitValue,
HDTD,
curtitle,
wireOrder // 添加 wireOrder 参数
}) => {
const chartRef = useRef(null);
const [mouseInfo, setMouseInfo] = useState({
frequency: 0,
values: {
margin: 'N/A',
worstValue: 'N/A'
}
});
// 根据curtitle获取对应的数据组和配置
const getDataConfig = () => {
// 四对线的数据组
const fourPairsGroup = [
'Insertion Loss (dB)', 'RL (dB)', 'PS NEXT (dB)', 'PS ACR-N (dB)',
'PS ACR-F (dB)', 'TCL (dB)', 'CMRL (dB)', 'ELTCTL (dB)'
];
// 六对线的数据组
const sixPairsGroup = ['NEXT (dB)', 'ACR-N (dB)'];
// 十二对线的数据组
const twelvePairsGroup = ['ACR-F (dB)', 'CDNEXT (dB)'];
// 数据组映射
const dataGroupMap = {
'插入损耗': 'Insertion Loss (dB)',
'回波损耗': 'RL (dB)',
'NEXT': 'NEXT (dB)',
'PS NEXT': 'PS NEXT (dB)',
'ACR-N': 'ACR-N (dB)',
'PS ACR-N': 'PS ACR-N (dB)',
'ACR-F': 'ACR-F (dB)',
'PS ACR-F': 'PS ACR-F (dB)',
'CDNEXT': 'CDNEXT (dB)',
'CMRL': 'CMRL (dB)',
'TCL': 'TCL (dB)',
'ELTCTL': 'ELTCTL (dB)'
};
const dataGroup = dataGroupMap[curtitle];
const isSixPairs = sixPairsGroup.includes(dataGroup);
const isTwelvePairs = twelvePairsGroup.includes(dataGroup);
// 根据 wireOrder 选择线对
let pairs;
if (wireOrder === 'Ethernet Two-Pair' || wireOrder === 'M12-D Two-Pair') {
// Ethernet Two-Pair 模式下只显示 12 和 36 相关的线对
pairs = isTwelvePairs ?
['PAIR1236', 'PAIR3612'] :
isSixPairs ?
['PAIR1236'] :
['PAIR12', 'PAIR36'];
} else {
// T568B 模式下显示所有线对
pairs = isTwelvePairs ?
['PAIR1236', 'PAIR1245', 'PAIR1278', 'PAIR3612', 'PAIR3645', 'PAIR3678',
'PAIR4512', 'PAIR4535', 'PAIR4578', 'PAIR7812', 'PAIR7936', 'PAIR7845'] :
isSixPairs ?
['PAIR1236', 'PAIR1245', 'PAIR1278', 'PAIR3645', 'PAIR3678', 'PAIR4578'] :
['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
}
// 根据实际线对数量动态生成颜色
const colors = pairs.map((_, index) => {
const baseColors = ['#FFA500', '#0000FF', '#00FF00', '#8B4513', '#800080', '#FF69B4',
'#FF4500', '#4B0082', '#32CD32', '#FF1493', '#00CED1', '#FFD700'];
return baseColors[index % baseColors.length];
});
return { dataGroup, pairs, colors };
};
// 计算数据的Y轴范围
const calculateYRange = () => {
const { dataGroup, pairs } = getDataConfig();
if (!data?.[dataGroup]) return { min: 0, max: 60 };
// 收集所有数据值,包括实际数据和限制数据
let allValues = [];
// 添加实际数据值
pairs.forEach(pair => {
const dataValues = data[dataGroup]?.[pair] || [];
allValues.push(...dataValues);
});
// 添加极限值数据
if (limitdata?.[dataGroup]?.['PAIRLimit (dB)']) {
allValues.push(...limitdata[dataGroup]['PAIRLimit (dB)']);
}
if (allValues.length === 0) return { min: 0, max: 60 };
const maxValue = Math.max(...allValues.filter(v => v !== undefined && v !== null));
const minValue = Math.min(...allValues.filter(v => v !== undefined && v !== null));
// 计算合适的范围,确保包含所有数据点
const range = Math.max(Math.abs(maxValue), Math.abs(minValue));
// 向上取整到最接近的10的倍数并额外增加10%的空间
const roundedRange = Math.ceil(range * 1.1 / 10) * 10;
return { min: -25, max: roundedRange };
};
// 根据limitValue获取频率范围
const getFrequencyRange = (limitValue) => {
if (!limitValue) return { min: 1, max: 500 };
const limitValueLower = limitValue.toLowerCase();
if (limitValueLower.includes('cat 6a') || limitValueLower.includes('class ea')) {
return { min: 1, max: 500 };
} else if (limitValueLower.includes('cat 6') || limitValueLower.includes('class e')) {
return { min: 1, max: 350 };
} else if (limitValueLower.includes('cat 5e') || limitValueLower.includes('class d')) {
return { min: 1, max: 150 };
}
return { min: 1, max: 500 }; // 默认值
};
// 初始化范围
const [range, setRange] = useState(() => {
// 获取频率范围
const frequencies = data?.frequencies || [];
const freqRange = getFrequencyRange(limitValue);
return {
xMin: freqRange.min,
xMax: freqRange.max,
...calculateYRange()
};
});
// 准备Echarts的配置项
const getOption = () => {
if (!data?.frequencies) {
return {
title: {
text: '无数据',
left: 'center',
top: 'center'
}
};
}
const { dataGroup, pairs, colors } = getDataConfig();
if (!dataGroup) return {};
const frequencies = data.frequencies;
const yRange = calculateYRange();
const freqRange = getFrequencyRange(limitValue);
// 准备系列数据
const series = [];
const legendData = [];
// 添加实际数据系列
pairs.forEach((pair, index) => {
if (data[dataGroup]?.[pair]) {
// 创建数据点数组
const seriesData = frequencies.map((freq, i) => {
const value = data[dataGroup][pair][i];
return [freq, value];
}).filter(item => item[1] !== undefined && item[1] !== null);
// 添加数据系列
series.push({
name: pair,
type: 'line',
data: seriesData,
symbol: 'none',
lineStyle: {
width: 2,
color: colors[index]
},
emphasis: {
lineStyle: {
width: 3
}
}
});
legendData.push(pair);
}
});
// 添加极限值系列
if (limitdata?.[dataGroup]?.['PAIRLimit (dB)']) {
const limitSeriesData = frequencies.map((freq, i) => {
const value = limitdata[dataGroup]['PAIRLimit (dB)'][i];
return [freq, value];
}).filter(item => item[1] !== undefined && item[1] !== null);
series.push({
name: '极限值',
type: 'line',
data: limitSeriesData,
symbol: 'none',
lineStyle: {
width: 2,
color: '#FF0000',
type: 'solid'
},
emphasis: {
lineStyle: {
width: 3
}
}
});
legendData.push('极限值');
}
return {
// 删除title配置
tooltip: {
trigger: 'axis',
formatter: function (params) {
const freq = params[0].value[0].toFixed(1);
let result = `频率: ${freq} MHz<br/>`;
// 找到最差值和余量
let worstValue = -Infinity;
let minMargin = Infinity;
let limitValue = null;
// 查找极限值
const limitParam = params.find(p => p.seriesName === '极限值');
if (limitParam) {
limitValue = limitParam.value[1];
}
// 处理每个系列的数据
params.forEach(param => {
if (param.seriesName !== '极限值') {
const value = param.value[1];
result += `${param.seriesName}: ${value.toFixed(2)} dB<br/>`;
// 更新最差值
if (value !== undefined) {
worstValue = Math.max(worstValue, Math.abs(value));
}
// 计算余量
if (value !== undefined && limitValue !== undefined && limitValue !== null) {
const margin = curtitle === '插入损耗' ?
Math.abs(limitValue) - Math.abs(value) : // 插入损耗:极限值 - 参数值
Math.abs(value) - limitValue; // 其他参数:参数值 - 极限值
if (!isNaN(margin)) {
minMargin = Math.min(minMargin, margin);
}
}
}
});
// 添加最差值和余量信息
result += `<br/>最差值: ${worstValue !== -Infinity ? worstValue.toFixed(2) : 'N/A'} dB<br/>`;
result += `余量: ${minMargin !== Infinity ? minMargin.toFixed(2) : 'N/A'} dB`;
// 更新状态以在图表外部显示
setMouseInfo({
frequency: parseFloat(freq),
values: {
margin: minMargin !== Infinity ? minMargin.toFixed(1) : 'N/A',
worstValue: worstValue !== -Infinity ? worstValue.toFixed(1) : 'N/A'
}
});
return result;
},
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
legend: {
data: legendData,
type: 'scroll',
bottom: 0,
selected: legendData.reduce((acc, item) => {
acc[item] = true;
return acc;
}, {})
},
// 删除toolbox配置
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '3%', // 减小顶部空间,因为没有标题了
containLabel: true
},
xAxis: {
type: 'value',
name: '频率 (MHz)',
nameLocation: 'middle',
nameGap: 30,
min: range.xMin,
max: range.xMax,
axisLabel: {
formatter: '{value}'
}
},
yAxis: {
type: 'value',
name: 'dB',
nameLocation: 'middle',
nameGap: 40,
min: range.min,
max: range.max,
axisLabel: {
formatter: '{value}'
}
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
xAxisIndex: 0,
zoomOnMouseWheel: false, // 禁用鼠标滚轮缩放
moveOnMouseMove: true, // 允许鼠标拖动
preventDefaultMouseMove: false // 防止默认鼠标移动行为
},
{
type: 'inside',
start: 0,
end: 100,
yAxisIndex: 0,
orient: 'vertical',
zoomOnMouseWheel: false, // 禁用鼠标滚轮缩放
moveOnMouseMove: true, // 允许鼠标拖动
preventDefaultMouseMove: false // 防止默认鼠标移动行为
},
{
show: true,
type: 'slider',
bottom: 60,
start: 0,
end: 100,
xAxisIndex: 0
}
],
animation: false,
series: series
};
};
// 渲染图表区域
const renderChart = () => {
return (
<div className="relative w-full" style={{ height: '400px' }}> {/* 设置固定高度 */}
<div className="relative w-full h-full" >
<ReactECharts
ref={chartRef}
option={getOption()}
style={{ height: '100%', width: '100%' }}
/>
</div>
</div>
);
};
// 渲染参数信息区域
const renderParameters = () => {
return (
<div className="flex justify-between p-2 w-full">
<div className="space-y-2">
<div className="font-bold">{curtitle} : {mouseInfo.values.worstValue} dB </div>
<div className="font-bold">余量 : {mouseInfo.values.margin} dB</div>
</div>
</div>
);
};
return (
<div className="w-full flex flex-col"> {/* 移除h-full */}
{renderChart()}
{renderParameters()}
</div>
);
};
export default FrequencyChart;

View File

@@ -0,0 +1,227 @@
import React, { useEffect, useState, useRef } from 'react';
import ReactECharts from 'echarts-for-react';
import * as echarts from 'echarts/core';
import {
GridComponent,
TooltipComponent,
LegendComponent,
DataZoomComponent,
MarkLineComponent
} from 'echarts/components';
import { LineChart } from 'echarts/charts';
import { UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
// 注册必要的组件
echarts.use([
GridComponent,
TooltipComponent,
LegendComponent,
LineChart,
CanvasRenderer,
UniversalTransition,
DataZoomComponent,
MarkLineComponent
]);
const HDTDChart = ({
HDTD,
curtitle,
}) => {
const chartRef = useRef(null);
// 根据curtitle获取对应的数据组和配置
const getDataConfig = () => {
const isHDTDR = curtitle === 'HDTDR';
const pairs = isHDTDR ?
['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'] :
['PAIR1236', 'PAIR1245', 'PAIR1278', 'PAIR3645', 'PAIR3678', 'PAIR4578'];
const colors = isHDTDR ?
['#FFA500', '#0000FF', '#00FF00', '#8B4513'] :
['#FFA500', '#0000FF', '#00FF00', '#8B4513', '#800080', '#FF69B4'];
const yRange = isHDTDR ? { min: -100, max: 100 } : { min: 0, max: 100 };
return { pairs, colors, yRange };
};
// 准备Echarts的配置项
const getOption = () => {
if (!HDTD || !HDTD[curtitle]) {
return {
title: {
text: '无数据',
left: 'center',
top: 'center'
}
};
}
const { pairs, colors, yRange } = getDataConfig();
const data = HDTD[curtitle];
const lengthArray = data.LENGTH;
const maxLength = Math.max(...lengthArray);
// 准备系列数据
const series = [];
const legendData = [];
// 添加数据系列
pairs.forEach((pair, index) => {
const values = data[pair];
if (!values || values.length === 0) return;
// 创建数据点数组
const seriesData = values.map((value, i) => {
// 使用索引作为X轴位置或者如果有具体的位置数据可以使用它
const position = i / (values.length - 1) * maxLength;
return [position, value];
});
// 添加数据系列
series.push({
name: pair,
type: 'line',
data: seriesData,
symbol: 'none',
lineStyle: {
width: 2,
color: colors[index]
},
emphasis: {
lineStyle: {
width: 3
}
},
animation: false
});
legendData.push(pair);
});
return {
tooltip: {
trigger: 'axis',
formatter: function (params) {
// Add null checks to prevent errors
if (!params || !params.length || !params[0] || !params[0].value) {
return '无数据';
}
const position = params[0].value[0] ? params[0].value[0].toFixed(1) : '未知';
let result = `位置: ${position} m<br/>`;
params.forEach(param => {
if (param && param.value) {
const value = param.value[1];
result += `${param.seriesName}: ${value ? value.toFixed(2) : '未知'}<br/>`;
}
});
return result;
},
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
legend: {
data: legendData,
type: 'scroll',
bottom: 0,
selected: legendData.reduce((acc, item) => {
acc[item] = true;
return acc;
}, {})
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '3%',
containLabel: true
},
xAxis: {
type: 'value',
name: '位置 (m)',
nameLocation: 'middle',
nameGap: 30,
min: 0,
max: maxLength,
axisLabel: {
formatter: '{value}'
}
},
yAxis: {
type: 'value',
name: curtitle === 'HDTDR' ? '阻抗 (Ω)' : '幅度 (dB)',
nameLocation: 'middle',
nameGap: 40,
min: yRange.min,
max: yRange.max,
axisLabel: {
formatter: '{value}'
}
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
xAxisIndex: 0,
zoomOnMouseWheel: false,
moveOnMouseMove: true,
preventDefaultMouseMove: false
},
{
type: 'inside',
start: 0,
end: 100,
yAxisIndex: 0,
orient: 'vertical',
zoomOnMouseWheel: false,
moveOnMouseMove: true,
preventDefaultMouseMove: false
},
{
show: true,
type: 'slider',
bottom: 60,
start: 0,
end: 100,
xAxisIndex: 0
}
],
animation: false,
series: series
};
};
// 渲染图表区域
const renderChart = () => {
return (
<div className="relative w-full" style={{ height: '500px' }}>
<div className="relative w-full h-full">
<ReactECharts
ref={chartRef}
option={getOption()}
style={{ height: '100%', width: '100%' }}
/>
</div>
</div>
);
};
return (
<div className="w-full">
{renderChart()}
</div>
);
};
export default HDTDChart;

View File

@@ -0,0 +1,129 @@
import React, { useState } from 'react';
export default function Keyboard({ value, cursorPosition, onChange, onComplete }) {
const [isUpperCase, setIsUpperCase] = useState(false);
const [isSymbol, setIsSymbol] = useState(false);
const letterRows = [
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
['z', 'x', 'c', 'v', 'b', 'n', 'm']
];
const symbolRows = [
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
['!', '@', '#', '$', '%', '^', '&', '*', '(', ')'],
['-', '_', '+', '=', '[', ']', '{', '}', '|'],
['/', '\\', ':', ';', '"', '\'', ',', '.', '?']
];
const rows = isSymbol ? symbolRows : letterRows;
const handleKeyClick = (key) => {
const newValue = value.slice(0, cursorPosition) + (isUpperCase ? key.toUpperCase() : key) + value.slice(cursorPosition);
onChange(newValue, cursorPosition + 1);
};
const handleBackspace = () => {
if (cursorPosition > 0) {
const newValue = value.slice(0, cursorPosition - 1) + value.slice(cursorPosition);
onChange(newValue, cursorPosition - 1);
}
};
const handleSpace = () => {
const newValue = value.slice(0, cursorPosition) + ' ' + value.slice(cursorPosition);
onChange(newValue, cursorPosition + 1);
};
const toggleCase = () => {
setIsUpperCase(!isUpperCase);
};
const toggleSymbol = () => {
setIsSymbol(!isSymbol);
setIsUpperCase(false);
};
const shiftButtonClass = [
'w-11 h-10',
'bg-gradient-to-b',
isUpperCase ? 'from-[#f8c828] to-[#bc8c1c]' : 'from-[#656565] to-[#313431]',
'rounded-sm',
'flex items-center justify-center',
'text-white font-bold'
].join(' ');
const symbolButtonClass = [
'w-11 h-10',
'bg-gradient-to-b',
isSymbol ? 'from-[#f8c828] to-[#bc8c1c]' : 'from-[#656565] to-[#313431]',
'rounded-sm',
'flex items-center justify-center',
'text-white text-sm'
].join(' ');
return (
<div className='bottom-0 left-0 w-full h-[full] bg-[#303040] p-4 flex flex-col z-50'
// style={{
// position: 'absolute',
// bottom: '0px',
// width: '100%',
// zIndex: 50,
// }}
>
<div className="w-full h-full p-2 space-y-1">
{rows.map((row, rowIndex) => (
<div key={rowIndex} className="flex justify-center gap-1">
{rowIndex === 3 && (
<button
className={shiftButtonClass}
onClick={toggleCase}
>
</button>
)}
{row.map((key) => (
<button
key={key}
className="w-11 h-10 bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white"
onClick={() => handleKeyClick(key)}
>
{isUpperCase ? key.toUpperCase() : key}
</button>
))}
{rowIndex === 3 && (
<button
className="w-11 h-10 bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white font-bold"
onClick={handleBackspace}
>
</button>
)}
</div>
))}
<div className="flex justify-center gap-1">
<button
className={symbolButtonClass}
onClick={toggleSymbol}
>
{isSymbol ? 'ABC' : '?123'}
</button>
<button
className="flex-1 h-10 bg-gradient-to-b from-[#656565] to-[#313431] rounded-sm flex items-center justify-center text-white"
onClick={handleSpace}
>
空格
</button>
<button
className="w-16 h-10 bg-gradient-to-b from-[#f8c828] to-[#bc8c1c] rounded-sm flex items-center justify-center text-white font-bold"
onClick={onComplete}
>
确定
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,121 @@
import React from 'react';
import useDisplayStore from '@/store/displayStore';
export default function OLTSResultMain({ testResult }) {
const { getCurrentTestConfig } = useDisplayStore.getState();
const { view } = useDisplayStore.getState().navigation.current;
const currentConfig = getCurrentTestConfig();
const cableType = currentConfig.params.cableType;
const isMultiMode = cableType.includes('OM');
// 根据光纤类型和测试结果选择背景图片
const getBackgroundImage = () => {
const prefix = isMultiMode ? '/olts-mm-' : '/olts-sm-';
const { InPortStatus, OutPortStatus } = testResult;
// 两个端口都通过
if (InPortStatus === 'pass' && OutPortStatus === 'pass') {
return prefix + 'pass.png';
}
// 根据光纤类型判断返回的图片
if (isMultiMode) {
// 多模光纤 - 保持原有逻辑
if (InPortStatus === 'fail' && OutPortStatus === 'pass') {
return prefix + 'fail-outpass.png';
}
if (InPortStatus === 'pass' && OutPortStatus === 'fail') {
return prefix + 'fail-inpass.png';
}
} else {
// 单模光纤 - 交换图片逻辑
if (InPortStatus === 'fail' && OutPortStatus === 'pass') {
return prefix + 'fail-inpass.png';
}
if (InPortStatus === 'pass' && OutPortStatus === 'fail') {
return prefix + 'fail-outpass.png';
}
}
// 两个端口都失败
return prefix + 'fail.png';
};
return (
<div className="w-full h-[490px] flex flex-col overflow-hidden">
{/* 背景图片层 */}
<div className="w-full h-full relative">
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: `url(${getBackgroundImage()})` }}
/>
{/* 数据显示层 */}
<div className="absolute inset-0 flex items-center justify-center pl-66 pb-24">
<div className="flex flex-col gap-6 ">
{/* 上方数据显示框 */}
<div className="bg-gradient-to-b w-48 from-[#e6e3e6] to-[#7b797b] p-2 rounded-lg">
<div className="flex flex-col gap-0">
{/* 损耗显示 */}
{view !== 'nosave' ? (
<div className="text-black pb-3 font-bold whitespace-nowrap overflow-hidden text-ellipsis">{testResult.inputname}</div>
) : (
<div className="text-black pb-3 font-bold">未保存的结果</div>
)}
<div className="flex items-center">
{testResult.isLossValidOut ? (<img src="/pass.png" alt="Pass" className="w-6 h-6" />
) : (
<img src="/fail.png" alt="Fail" className="w-6 h-6" />
)}
<div className="text-black font-bold ml-2">损耗:</div>
<div className="text-black ml-auto">{testResult.CFPMainOut.wavelength1.loss}dB</div>
</div>
{/* 长度显示 */}
<div className="flex items-center">
{testResult.isLengthValidOut ? (<img src="/pass.png" alt="Pass" className="w-6 h-6" />
) : (
<img src="/fail.png" alt="Fail" className="w-6 h-6" />
)}
<div className="text-black font-bold ml-2">长度:</div>
<div className="text-black ml-auto">{testResult.CFPMainOut.wavelength1.distance}m</div>
</div>
</div>
</div>
{/* 下方数据显示框 */}
<div className="bg-gradient-to-b w-48 from-[#e6e3e6] to-[#7b797b] p-2 rounded-lg">
<div className="flex flex-col gap-0">
{/* 损耗显示 */}
{view !== 'nosave' ? (
<div className="text-black pb-3 font-bold whitespace-nowrap overflow-hidden text-ellipsis">{testResult.outname}</div>
) : (
<div className="text-black pb-3 font-bold">未保存的结果</div>
)}
<div className="flex items-center">
{testResult.isLossValidIn ? (<img src="/pass.png" alt="Pass" className="w-6 h-6" />
) : (
<img src="/fail.png" alt="Fail" className="w-6 h-6" />
)}
<div className="text-black font-bold ml-2">损耗:</div>
<div className="text-black ml-auto">{testResult.CFPMainIn.wavelength1.loss}dB</div>
</div>
{/* 长度显示 */}
<div className="flex items-center">
{testResult.isLengthValidIn ? (<img src="/pass.png" alt="Pass" className="w-6 h-6" />
) : (
<img src="/fail.png" alt="Fail" className="w-6 h-6" />
)}
<div className="text-black font-bold ml-2">长度:</div>
<div className="text-black ml-auto">{testResult.CFPMainIn.wavelength1.distance}m</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,59 @@
import React, { useState, useEffect } from 'react';
import EventMapView from './EventMapView';
import EventTable from './EventTable';
import CurveChart from './CurveChart';
export default function OTDRResultMain({ testResult }) {
const [activeTab, setActiveTab] = useState('EventMap');
const tabs = ['EventMap', '表', '曲线'];
const tempTestResult = testResult;
const renderContent = () => {
switch (activeTab) {
case 'EventMap':
return (
<div className="w-full h-full flex items-center justify-center">
<EventMapView testResult={tempTestResult} />
</div>
);
case '表':
return (
<div className="w-full h-full flex items-center justify-center">
<EventTable testResult={tempTestResult} />
</div>
);
case '曲线':
return (
<div className="w-full h-full flex items-center justify-center">
<CurveChart testResult={tempTestResult} />
</div>
);
default:
return null;
}
};
return (
<div className="w-full h-[490px] flex flex-col overflow-hidden">
<div className="h-[1/100] flex">
{tabs.map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`flex-1 h-full flex items-center justify-center font-bold text-lg
${activeTab === tab
? 'bg-gradient-to-b from-[#b0b0b0] via-[#e0e4e0] to-[#b0b0b0] text-black'
: 'bg-[#303030] text-[#fffe92]'}`}
>
{tab}
</button>
))}
</div>
<div className="flex-1 bg-white">
{renderContent()}
</div>
</div>
);
}

View File

@@ -0,0 +1,306 @@
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import useDisplayStore from '@/store/displayStore';
export default function PerformanceView({ testResult }) {
const { navigateTo } = useDisplayStore();
const showExtendedTests = testResult?.testconfig?.params?.limitValue?.includes('+ALL');
const [limitdata, setLimitdata] = useState(null);
const limitValue = testResult?.testconfig?.params?.limitValue;
// 计算参数余量
const calculateMargin = (paramTitle) => {
// 长度处理
if (paramTitle === '长度') {
const result = calculateLengthStatus();
return result.margin; // 只返回margin值
}
// 电阻参数的特殊处理
if (paramTitle === '电阻') {
return calculateResistanceMargin();
}
const dataGroupMap = {
'插入损耗': 'Insertion Loss (dB)',
'回波损耗': 'RL (dB)',
'NEXT': 'NEXT (dB)',
'PS NEXT': 'PS NEXT (dB)',
'ACR-N': 'ACR-N (dB)',
'PS ACR-N': 'PS ACR-N (dB)',
'ACR-F': 'ACR-F (dB)',
'PS ACR-F': 'PS ACR-F (dB)',
'CDNEXT': 'CDNEXT (dB)',
'CMRL': 'CMRL (dB)',
'TCL': 'TCL (dB)',
'ELTCTL': 'ELTCTL (dB)'
};
const dataGroup = dataGroupMap[paramTitle];
if (!dataGroup || !testResult?.resultdata?.performance?.data?.[dataGroup] || !limitdata?.[dataGroup]) {
return null;
}
let minMargin = Infinity;
const data = testResult.resultdata.performance.data[dataGroup];
const limitValues = limitdata[dataGroup]?.['PAIRLimit (dB)'] || [];
// 根据参数类型选择对应的线对组
const pairs = ['NEXT (dB)', 'ACR-N (dB)'].includes(dataGroup) ?
['PAIR1236', 'PAIR1245', 'PAIR1278', 'PAIR3645', 'PAIR3678', 'PAIR4578'] :
['ACR-F (dB)', 'CDNEXT (dB)'].includes(dataGroup) ?
['PAIR1236', 'PAIR1245', 'PAIR1278', 'PAIR3612', 'PAIR3645', 'PAIR3678',
'PAIR4512', 'PAIR4535', 'PAIR4578', 'PAIR7812', 'PAIR7936', 'PAIR7845'] :
['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
// 遍历所有索引直到遇到空值或null值
for (let index = 0; index < limitValues.length; index++) {
const limitValue = limitValues[index];
if (limitValue === undefined || limitValue === null) break;
for (const pair of pairs) {
const actualValue = data[pair]?.[index];
// 只在实际值存在且不为null时进行计算
if (actualValue !== undefined && actualValue !== null) {
const margin = paramTitle === '插入损耗' ?
Math.abs(limitValue) - Math.abs(actualValue) : // 插入损耗:极限值 - 参数值
Math.abs(actualValue) - limitValue; // 其他参数:参数值 - 极限值
minMargin = Math.min(minMargin, margin);
}
}
}
return minMargin !== Infinity ? minMargin.toFixed(1) : null;
};
// 计算长度余量和状态
const calculateLengthStatus = () => {
if (!testResult?.resultdata?.performance || !limitdata) {
return { margin: null, isPass: false };
}
let minMargin = Infinity;
let minLength = Infinity;
let isPass = true;
const pairs = ['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
// 计算长度余量
if (limitdata.LENGTH && testResult.resultdata.performance.LENGTH) {
const lengthLimit = limitdata.LENGTH;
const lengthData = testResult.resultdata.performance.LENGTH;
for (const pair of pairs) {
const actualValue = lengthData[pair];
if (actualValue !== undefined && actualValue !== null && lengthLimit !== undefined && lengthLimit !== null) {
const margin = lengthLimit - Math.abs(actualValue);
minMargin = Math.min(minMargin, margin);
minLength = Math.min(minLength, Math.abs(actualValue));
if (Math.abs(actualValue) > lengthLimit) {
isPass = false;
}
}
}
}
// 检查延时
if (limitdata.DELAY && testResult.resultdata.performance.DELAY) {
const delayLimit = limitdata.DELAY;
const delayData = testResult.resultdata.performance.DELAY;
for (const pair of pairs) {
const actualValue = delayData[pair];
if (actualValue !== undefined && actualValue !== null && delayLimit !== undefined && delayLimit !== null) {
if (Math.abs(actualValue) > delayLimit) {
isPass = false;
}
}
}
}
// 检查延时偏差
if (limitdata.DELAYSKEW && testResult.resultdata.performance.DELAYSKEW) {
const skewLimit = limitdata.DELAYSKEW;
const skewData = testResult.resultdata.performance.DELAYSKEW;
const skewPairs = ['PAIR1236', 'PAIR1245', 'PAIR1278', 'PAIR3645', 'PAIR3678', 'PAIR4578'];
for (const pair of skewPairs) {
const actualValue = skewData[pair];
if (actualValue !== undefined && actualValue !== null && skewLimit !== undefined && skewLimit !== null) {
if (Math.abs(actualValue) > skewLimit) {
isPass = false;
}
}
}
}
return {
margin: minLength !== Infinity ? minLength.toFixed(1) : null,
isPass
};
};
// 计算电阻余量
const calculateResistanceMargin = () => {
if (!testResult?.resultdata?.performance?.OHM || !limitdata) {
return null;
}
const hasPoE = testResult?.testconfig?.params?.limitValue?.includes('+PoE');
const hasALL = testResult?.testconfig?.params?.limitValue?.includes('+ALL');
let minMargin = Infinity;
// 回路电阻余量计算(所有情况都需要)
if (limitdata.LOOP && testResult.resultdata.performance.OHM.LOOP) {
const loopLimit = limitdata.LOOP;
const loopData = testResult.resultdata.performance.OHM.LOOP;
const loopPairs = ['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
for (const pair of loopPairs) {
const actualValue = loopData[pair];
if (actualValue !== undefined && actualValue !== null && loopLimit !== undefined && loopLimit !== null) {
const margin = loopLimit - Math.abs(actualValue);
minMargin = Math.min(minMargin, margin);
}
}
}
// +PoE或+ALL测试的额外检查
if (hasPoE || hasALL) {
// 线对UBL余量计算
if (limitdata.PAIRUBL && testResult.resultdata.performance.OHM.PAIRUBL) {
const pairUblLimit = limitdata.PAIRUBL;
const pairUblData = testResult.resultdata.performance.OHM.PAIRUBL;
const pairUblPairs = ['PAIR12', 'PAIR36', 'PAIR45', 'PAIR78'];
for (const pair of pairUblPairs) {
const actualValue = pairUblData[pair];
if (actualValue !== undefined && actualValue !== null && pairUblLimit !== undefined && pairUblLimit !== null) {
const margin = pairUblLimit - Math.abs(actualValue);
minMargin = Math.min(minMargin, margin);
}
}
}
// P2PUBL余量计算
if (limitdata.P2PUBL && testResult.resultdata.performance.OHM.P2PUBL) {
const p2pUblLimit = limitdata.P2PUBL;
const p2pUblData = testResult.resultdata.performance.OHM.P2PUBL;
const p2pUblPairs = ['PAIR1236', 'PAIR1245', 'PAIR1278', 'PAIR3645', 'PAIR3678', 'PAIR4578'];
for (const pair of p2pUblPairs) {
const actualValue = p2pUblData[pair];
if (actualValue !== undefined && actualValue !== null && p2pUblLimit !== undefined && p2pUblLimit !== null) {
const margin = p2pUblLimit - Math.abs(actualValue);
minMargin = Math.min(minMargin, margin);
}
}
}
}
return minMargin !== Infinity ? minMargin.toFixed(1) : null;
};
useEffect(() => {
// 异步加载限制数据
const loadLimitData = async () => {
if (limitValue) {
try {
// 从limitValue中提取基础名称(移除+PoE和+ALL后缀)
const baseName = limitValue.split(' (+')[0];
// 使用基础名称加载带有(+ALL)后缀的文件
const data = await import(`@/store/COPPER/${baseName} (+ALL).json`);
setLimitdata(data);
} catch (error) {
console.error('Error loading limit data:', error);
}
}
};
loadLimitData();
}, [limitValue]);
const getStatusIcon = (value, title) => {
if (!value) return null;
let isPass;
// 特殊处理长度参数
if (title === '长度') {
const lengthStatus = calculateLengthStatus();
isPass = lengthStatus.isPass;
} else {
// 其他参数保持原有逻辑
isPass = parseFloat(value) >= 0;
}
return (
<div className="w-4 h-4 relative ml-2">
<Image
src={isPass ? '/pass.png' : '/fail.png'}
alt={isPass ? '通过' : '失败'}
fill
className="object-contain"
/>
</div>
);
};
const handleRowClick = (curtitle) => {
if (curtitle === testResult?.testconfig?.params?.limitValue) {
return; // 第一行不需要跳转
}
if (curtitle === '长度') {
navigateTo('copperperformance', 'LENGTH', { testResult, curtitle });
}
else if (curtitle === '电阻') {
const hasPoE = testResult?.testconfig?.params?.limitValue?.includes('+PoE');
const hasALL = testResult?.testconfig?.params?.limitValue?.includes('+ALL');
if (hasPoE || hasALL) {
navigateTo('copperperformance', 'PoE', { testResult, curtitle });
} else {
navigateTo('copperperformance', 'OHM', { testResult, curtitle });
}
}
else if (curtitle === 'NEXT' || curtitle === 'PS NEXT' || curtitle === 'ACR-N' || curtitle === 'PS ACR-N' ||
curtitle === 'ACR-F' || curtitle === 'PS ACR-F' || curtitle === 'CDNEXT' || curtitle === 'CMRL' ||
curtitle === 'TCL' || curtitle === 'ELTCTL' || curtitle === '插入损耗' || curtitle === '回波损耗') {
navigateTo('copperperformance', 'DRAW', { testResult, curtitle });
}
};
const renderRow = (title, value, unit = '') => (
<div
className="w-full flex items-center justify-between p-3 mb-2 rounded-md bg-gradient-to-b from-[#dedede] via-[#b5b5b5] to-[#8b898b] shadow-md cursor-pointer"
onClick={() => handleRowClick(title)}
>
<span className="text-lg font-medium text-black">{title}</span>
<div className="flex items-center">
<span className="text-lg text-black">{title === '电阻' ? '' : (value ? `(${value}${unit})` : '')}</span>
{getStatusIcon(value, title)}
</div>
</div>
);
return (
<div className="w-full h-[470px] bg-[#6b6d6b] flex flex-col p-4 overflow-y-auto" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
{renderRow(testResult?.testconfig?.params?.limitValue)}
{renderRow('长度', calculateMargin('长度'), ' m')}
{renderRow('电阻', calculateMargin('电阻'), ' Ω')}
{renderRow('插入损耗', calculateMargin('插入损耗'), ' dB')}
{renderRow('回波损耗', calculateMargin('回波损耗'), ' dB')}
{renderRow('NEXT', calculateMargin('NEXT'), ' dB')}
{renderRow('PS NEXT', calculateMargin('PS NEXT'), ' dB')}
{renderRow('ACR-N', calculateMargin('ACR-N'), ' dB')}
{renderRow('PS ACR-N', calculateMargin('PS ACR-N'), ' dB')}
{renderRow('ACR-F', calculateMargin('ACR-F'), ' dB')}
{renderRow('PS ACR-F', calculateMargin('PS ACR-F'), ' dB')}
{showExtendedTests && (
<>
{renderRow('CDNEXT', calculateMargin('CDNEXT'), ' dB')}
{renderRow('CMRL', calculateMargin('CMRL'), ' dB')}
{renderRow('TCL', calculateMargin('TCL'), ' dB')}
{renderRow('ELTCTL', calculateMargin('ELTCTL'), ' dB')}
</>
)}
</div>
);
}

View File

@@ -0,0 +1,98 @@
import React from 'react';
import Image from 'next/image';
import useDisplayStore from '@/store/displayStore';
export default function ResultTitleBar({ title, backTo, view, onBack, testResult, result }) {
const { navigateTo, goBack, updateCurrentView, navigation } = useDisplayStore();
// 根据结果获取背景颜色
const getBgColor = () => {
// 如果存在CopperWiremapResultStatus使用铜缆测试逻辑
if (testResult?.CopperResultStatus) {
if (testResult.CopperResultStatus !== 'pass') return 'bg-[#ce1d31]';
return testResult.CopperResultStatus === 'pass' ? 'bg-[#00A65A]' : 'bg-[#ce1d31]';
}
// 如果存在OFPStatus使用光纤测试逻辑
if (testResult?.CFPResultStatus) {
return testResult.CFPResultStatus === 'pass' ? 'bg-[#00A65A]' : 'bg-[#ce1d31]';
}
// 如果存在OFPStatus使用光纤测试逻辑
if (testResult?.ofpResultStatus) {
return testResult.ofpResultStatus === 'pass' ? 'bg-[#00A65A]' : 'bg-[#ce1d31]';
}
// 默认蓝色
return 'bg-[#003366]';
};
// 获取结果文本
const getResultText = () => {
// 如果存在CopperWiremapResultStatus使用铜缆测试逻辑
if (testResult?.CopperResultStatus) {
if (testResult.CopperResultStatus !== 'pass') return '失败';
return testResult.CopperResultStatus === 'pass' ? '通过' : '失败';
}
// 如果存在CFPStatus使用光纤测试逻辑
if (testResult?.CFPResultStatus) {
return testResult.CFPResultStatus === 'pass' ? '通过' : '失败';
}
// 如果存在OFPStatus使用光纤测试逻辑
if (testResult?.ofpResultStatus) {
return testResult.ofpResultStatus === 'pass' ? '通过' : '失败';
}
return null;
};
const handleBack = () => {
if (onBack) {
onBack();
return;
}
if (backTo) {
const currentParams = navigation.current.params;
navigateTo(backTo, view, currentParams);
} else if (view) {
const currentParams = navigation.current.params;
updateCurrentView(view, currentParams);
} else {
goBack();
}
};
return (
<div className={`h-[60px] flex items-center justify-between relative ${getBgColor()}`}>
{/* 返回按钮 */}
{(backTo || view || onBack) && (
<button
onClick={handleBack}
className="w-[60px] h-full flex items-center justify-center"
>
<div className="w-12 h-12 relative">
<Image
src="/back.png"
alt="返回"
fill
className="object-contain"
priority
/>
</div>
</button>
)}
{/* 标题 - 修改为左对齐 */}
<div className="flex-1 flex items-center ml-4">
<h1 className="text-white font-bold text-l">
{title || testResult?.name}
</h1>
</div>
{/* 结果显示 */}
<div className="mr-4">
<h2 className="text-white font-bold text-2xl">
{getResultText()}
</h2>
</div>
</div>
);
}

View File

@@ -0,0 +1,87 @@
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import useDeviceStore from '@/store/deviceStore';
// 将音频对象移到组件外部,确保只创建一次
const connectSound = typeof Audio !== 'undefined' ? new Audio('/sounds/rmt_connect.wav') : null;
const disconnectSound = typeof Audio !== 'undefined' ? new Audio('/sounds/rmt_disconnect.wav') : null;
export default function StatusBar() {
const [time, setTime] = useState('');
const { connectionStatus } = useDeviceStore();
const ConnectStatus = connectionStatus?.CFPConnectStatus ?? connectionStatus?.['8000ConnectStatus'] ?? false;
//console.log(connectionStatus);
useEffect(() => {
// 只在客户端更新时间
const updateTime = () => {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
setTime(`${year}/${month}/${day} ${hours}:${minutes}:${seconds}`);
};
// 立即更新一次
updateTime();
// 每秒更新一次
const timer = setInterval(updateTime, 1000);
// 清理定时器
return () => clearInterval(timer);
}, []);
// 用于保存上一个状态
const [previousConnectStatus, setPreviousConnectStatus] = useState(ConnectStatus);
useEffect(() => {
// 当 ConnectStatus 变化时执行
if (ConnectStatus !== previousConnectStatus) {
if (ConnectStatus) {
connectSound?.play().catch(console.error);
} else {
disconnectSound?.play().catch(console.error);
}
// 更新上一个状态
setPreviousConnectStatus(ConnectStatus);
}
}, [ConnectStatus, previousConnectStatus]);
return (
<div className="h-[30px] bg-[#000000] flex justify-between items-center px-4">
<div className="flex items-center">
<div className="w-8 h-6 relative">
<Image
src="/battery.png"
alt="电池"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
{/* 根据 ConnectStatus 显示连接图标 */}
{ConnectStatus && (
<div className="w-10 h-6 relative ml-2">
<Image
src="/connect.png"
alt="连接状态"
sizes={"auto"}
fill
className="object-contain"
/>
</div>
)}
</div>
<div className="text-white text-sm">
{time}
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import React from 'react';
const StatusToast = ({ status }) => {
const getBackgroundColor = (status) => {
if (status === '已清洁') return 'rgba(0, 200, 0, 0.5)';
if (status === '未清洁') return 'rgba(255, 0, 0, 0.5)';
return 'rgba(0, 62, 106, 0.78)';
};
const getPosition = (status) => {
if (status === '已清洁' || status === '未清洁') {
return { bottom: '2%', right: '2%' };
}
return { bottom: '2%', right: '8%' };
};
const position = getPosition(status);
return (
<div
style={{
position: 'fixed',
...position,
padding: '1% 1%',
background: getBackgroundColor(status),
color: 'rgb(255, 255, 255)',
borderRadius: '5px',
fontWeight: 'bold',
zIndex: 5001,
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.2)',
}}
>
{status}
</div>
);
};
export default StatusToast;

View File

@@ -0,0 +1,11 @@
import React from 'react';
export default function SubTitleBar({ title }) {
return (
<div className="h-[60px] bg-gradient-to-b from-[#b0b0b0] via-[#e0e4e0] to-[#b0b0b0] flex items-center justify-center">
<h2 className="text-black font-bold text-lg">
{title}
</h2>
</div>
);
}

View File

@@ -0,0 +1,51 @@
import React from 'react';
import Image from 'next/image';
import useDisplayStore from '@/store/displayStore';
export default function TitleBar({ title, backTo, view, onBack }) {
const { navigateTo, goBack, updateCurrentView, navigation } = useDisplayStore();
const handleBack = () => {
if (onBack) {
onBack();
return;
}
if (backTo) {
const currentParams = navigation.current.params;
navigateTo(backTo, view, currentParams);
} else if (view) {
const currentParams = navigation.current.params;
updateCurrentView(view, currentParams);
} else {
goBack();
}
};
return (
<div className="h-[60px] bg-[#303030] flex items-center relative">
{(backTo || view || onBack) && (
<button
onClick={handleBack}
className="w-[60px] h-full flex items-center justify-center absolute left-2"
>
<div className="w-12 h-12 relative">
<Image
src="/back.png"
alt="返回"
fill
className="object-contain"
priority
/>
</div>
</button>
)}
<h1 className="text-[#f8c828] font-bold text-2xl absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2">
{title}
</h1>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import React from 'react';
import useDisplayStore from '@/store/displayStore';
const Toast = () => {
const { showToast, toastMessage } = useDisplayStore();
const setShowToast = useDisplayStore((state) => state.setShowToast);
const setToastMessage = useDisplayStore((state) => state.setToastMessage);
const handleConfirm = () => {
setToastMessage('');
};
return (
<div className="w-[480px] h-[640px] bg-[#002842d4] absolute z-9999">
<div className='pl-10 pt-60'>
<div className="bg-[#2B3C5B] rounded-lg p-6 w-[400px] min-h-[200px] flex flex-col">
<h3 className="text-white text-xl font-bold mb-4">提示</h3>
<div className="flex-1 flex items-center justify-center">
<div className="text-white text-lg">{toastMessage}</div>
</div>
<div className="flex justify-center mt-4">
<button
onClick={handleConfirm}
className="bg-[#354e7a] text-white px-6 py-2 rounded hover:bg-[#1E293B] transition-colors"
>
确定
</button>
</div>
</div>
</div>
</div>
);
};
export default Toast;

View File

@@ -0,0 +1,36 @@
import React from 'react';
import useDeviceStore from '@/store/deviceStore';
const TotalToast = () => {
const { showTotalToast, totalToastMessage } = useDeviceStore();
const setShowTotalToast = useDeviceStore((state) => state.setShowTotalToast);
const setTotalToastMessage = useDeviceStore((state) => state.setTotalToastMessage);
const handleConfirm = () => {
setTotalToastMessage('');
};
return (
<div className="fixed inset-0 flex items-center justify-center bg-[#002842d4] z-[9999]">
<div className="w-[30%] mx-auto">
<div className="bg-[#2B3C5B] rounded-lg p-8 flex flex-col">
<h3 className="text-white text-2xl font-bold mb-6 text-center">场景提示</h3>
<div className="flex-1 flex items-center justify-center mb-8">
<div className="text-white text-lg text-center">{totalToastMessage}</div>
</div>
<div className="flex justify-center">
<button
onClick={handleConfirm}
className="bg-[#354e7a] text-white px-8 py-3 rounded-lg hover:bg-[#1E293B] transition-colors duration-200"
>
确定
</button>
</div>
</div>
</div>
</div>
);
};
export default TotalToast;

View File

@@ -0,0 +1,100 @@
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import useDisplayStore from '@/store/displayStore';
export default function WireMapView({ testResult }) {
const { view } = useDisplayStore.getState().navigation.current;
const currentCableId = useDisplayStore.getState().getCurrentCableId();
const [WiremapSvg, setWiremapSvg] = useState(null);
const currentConfig = useDisplayStore.getState().getCurrentTestConfig();
const cableType = testResult.testconfig.params.cableType;
const wireOrder = testResult.testconfig.params.wireOrder;
useEffect(() => {
const loadSvg = async () => {
try {
let prefix = '';
let folderPath = 'T568B';
// 设置文件夹路径
if (wireOrder === 'T568B') {
folderPath = 'T568B';
} else if (wireOrder === 'Ethernet Two-Pair') {
folderPath = 'EthernetTwoPair';
} else if (wireOrder === 'M12-D Two-Pair') {
folderPath = 'M12DTwoPair';
}
// 设置文件前缀
if (cableType?.includes('U/UTP')) {
prefix = 'u-';
} else if (cableType?.includes('F/UTP')) {
prefix = 's-';
}
try {
// 尝试加载指定文件夹下的SVG
const svg = await import(`@/store/COPPER/${folderPath}/${prefix}${testResult?.CopperWiremapStatus}.svg`);
setWiremapSvg(svg.default);
} catch {
// 如果找不到对应的SVG加载默认的s-pass.svg
const defaultSvg = await import(`@/store/COPPER/${folderPath}/s-pass.svg`);
setWiremapSvg(defaultSvg.default);
}
} catch (error) {
console.error('Error loading SVG:', error);
}
};
loadSvg();
}, [testResult?.CopperWiremapStatus, cableType, wireOrder]);
return (
<div className="w-full h-full flex flex-col">
{/* 顶部区域 */}
<div className="flex justify-between items-center p-3 bg-white">
<div className="text-black font-bold">
{testResult?.testconfig?.params?.wireOrder || ''}
</div>
<div className="w-5 h-5 relative">
<Image
src={testResult?.CopperWiremapResultStatus === 'pass' ? '/pass.png' : '/fail.png'}
alt={testResult?.CopperWiremapResultStatus === 'pass' ? '通过' : '失败'}
fill
className="object-contain"
/>
</div>
</div>
{/* 分割线 */}
<div className="border-t border-gray-300"></div>
{/* 中间区域 */}
<div className="flex-1">
<div className="w-full h-full flex flex-col">
{WiremapSvg && (
<div className="w-full h-full">
<Image
src={WiremapSvg}
alt="Wiremap Status"
priority
/>
</div>
)}
</div>
</div>
{/* 底部区域 */}
<div className="flex justify-between items-center p-2 bg-white">
<button className="w-8 h-8 bg-gray-200 rounded-full flex items-center justify-center text-xl font-bold">?</button>
{view === 'nosave' && (
<div className="flex flex-col items-end">
<div className="text-black font-bold">下一个ID</div>
<div className="text-black text-sm">{currentCableId?.name || ''}</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,412 @@
import React from 'react';
export default function DataCenter({
onPortClick = () => {},
onPortHover = () => {},
selectedPort = null,
targetPort = null,
connections = {}
}) {
// 获取端口的连接状态样式
const getPortStyle = (portId) => {
if (selectedPort === portId) {
return 'bg-blue-300';
}
if (targetPort === portId) {
return 'bg-green-300';
}
if (connections && connections[portId]) {
return 'bg-[#00ff7f]';
}
return 'bg-gray-300';
};
return (
<div className="h-full w-full bg-black text-white flex">
{/* Rack taking 1/3 of the width */}
<div className="w-1/3 p-1 flex flex-col">
<div className="text-center mb-1">Rack CA</div>
<div className="h-[calc(100%-1.5rem)] rounded-md p-2 flex flex-col space-y-2">
{/* CA机柜内容 */}
{/* MPO配线架 */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1A</span>
<div className="flex-1 flex items-center justify-center space-x-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-12 h-10 mb-1 transition-colors flex items-center justify-center `}
jstype="testport-mpo"
mpoClean="false"
id={`CA-1A-${i + 1}`}
onClick={() => onPortClick(`CA-1A-${i + 1}`)}
onMouseEnter={() => onPortHover(`CA-1A-${i + 1}`)}
>
<div className={`w-12 h-10 bg-[url('/mpo.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>
{/* MPO配线架 */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1B</span>
<div className="flex-1 flex items-center justify-center space-x-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-12 h-10 mb-1 transition-colors flex items-center justify-center `}
jstype="testport-mpo"
mpoClean="false"
id={`CA-1B-${i + 1}`}
onClick={() => onPortClick(`CA-1B-${i + 1}`)}
onMouseEnter={() => onPortHover(`CA-1B-${i + 1}`)}
>
<div className={`w-12 h-10 bg-[url('/mpo.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>
{/* CA机柜内容 */}
</div>
</div>
{/* Rack taking 1/3 of the width */}
<div className="w-1/3 p-1 flex flex-col">
<div className="text-center mb-1">Rack CB</div>
<div className="h-[calc(100%-1.5rem)] rounded-md p-2 flex flex-col space-y-2">
{/* CB机柜内容 */}
{/* MPO配线架 */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1A</span>
<div className="flex-1 flex items-center justify-center space-x-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-12 h-10 mb-1 transition-colors flex items-center justify-center `}
jstype="testport-mpo"
mpoClean="false"
id={`CB-1A-${i + 1}`}
onClick={() => onPortClick(`CB-1A-${i + 1}`)}
onMouseEnter={() => onPortHover(`CB-1A-${i + 1}`)}
>
<div className={`w-12 h-10 bg-[url('/mpo.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>
{/* Network Switch */}
<div className="flex flex-col h-1/2 bg-gray-900 rounded-md p-2 relative border border-gray-700">
{/* Switch Top Area - Brand and Status LEDs */}
<div className="h-8 flex items-center justify-between px-2 border-b border-gray-700">
<div className="text-blue-500 font-bold text-sm">SWITCH</div>
<div className="flex space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-amber-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
</div>
</div>
{/* Switch Port Area */}
<div className="flex-1 flex flex-row">
{/* Left Side - RJ45 Ports (80%) */}
<div className="w-4/6 flex flex-col">
{/* Port Labels */}
<div className="flex justify-between px-4 text-xs text-gray-400">
<span>10/100/1000 Base-T</span>
<span>1-20</span>
</div>
{/* Top Row Ports (1-10) - RJ45 */}
<div className="flex-1 flex items-center justify-center">
<div className="grid grid-cols-10 gap-0.5 w-full">
{[...Array(10)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className="w-5 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
jstype="testport-mpo"
id={`SW-TOP-${i + 1}`}
>
<div className="w-4 h-4 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center"></div>
</div>
</div>
))}
</div>
</div>
{/* Bottom Row Ports (11-20) - RJ45 */}
<div className="flex-1 flex items-center justify-center">
<div className="grid grid-cols-10 gap-0.5 w-full">
{[...Array(10)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className="w-5 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
jstype="testport-mpo"
id={`SW-BOT-${i + 1}`}
>
<div className="w-4 h-4 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center"></div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Right Side - Fiber Ports (20%) */}
<div className="w-2/6 flex flex-col">
{/* Fiber Port Labels */}
<div className="flex justify-between px-1 text-xs text-gray-400">
<span>SFP+</span>
<span>21-24</span>
</div>
{/* Top Row Fiber Ports (21-22) */}
<div className="flex-1 flex items-center justify-center">
<div className="flex space-x-4"> {/* 使用 space-x-4 添加水平间距 */}
{[...Array(2)].map((_, i) => (
<div key={i} className="flex justify-center">
<div
className="w-10 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
>
<div className="flex">
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"></div>
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"></div>
</div>
</div>
</div>
))}
</div>
</div>
{/* Bottom Row Fiber Ports (23-24) */}
<div className="flex-1 flex items-center justify-center">
<div className="flex space-x-4"> {/* 使用 space-x-4 添加水平间距 */}
{[...Array(2)].map((_, i) => (
<div key={i} className="flex justify-center">
<div
className="w-10 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
>
<div className="flex">
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"
></div>
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"
></div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Switch Bottom Area - Console and Power */}
<div className="h-6 flex items-center justify-between px-2 border-t border-gray-700">
<div className="flex items-center space-x-2">
<div className="text-xs text-gray-400">Console</div>
<div className="w-4 h-2 bg-gray-600 rounded-sm"></div>
</div>
<div className="flex items-center space-x-2">
<div className="text-xs text-gray-400">Power</div>
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
</div>
</div>
</div>
{/* CB机柜内容 */}
</div>
</div>
{/* Rack taking 1/3 of the width */}
<div className="w-1/3 p-1 flex flex-col">
<div className="text-center mb-1">Rack CC</div>
<div className="h-[calc(100%-1.5rem)] rounded-md p-2 flex flex-col space-y-2">
{/* CC机柜内容 */}
{/* MPO配线架 */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1A</span>
<div className="flex-1 flex items-center justify-center space-x-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-12 h-10 mb-1 transition-colors flex items-center justify-center `}
jstype="testport-mpo"
mpoClean="false"
id={`CC-1A-${i + 1}`}
onClick={() => onPortClick(`CC-1A-${i + 1}`)}
onMouseEnter={() => onPortHover(`CC-1A-${i + 1}`)}
>
<div className={`w-12 h-10 bg-[url('/mpo.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>
{/* Network Switch */}
<div className="flex flex-col h-1/2 bg-gray-900 rounded-md p-2 relative border border-gray-700">
{/* Switch Top Area - Brand and Status LEDs */}
<div className="h-8 flex items-center justify-between px-2 border-b border-gray-700">
<div className="text-blue-500 font-bold text-sm">SWITCH</div>
<div className="flex space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-amber-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
</div>
</div>
{/* Switch Port Area */}
<div className="flex-1 flex flex-row">
{/* Left Side - RJ45 Ports (80%) */}
<div className="w-4/6 flex flex-col">
{/* Port Labels */}
<div className="flex justify-between px-4 text-xs text-gray-400">
<span>10/100/1000 Base-T</span>
<span>1-20</span>
</div>
{/* Top Row Ports (1-10) - RJ45 */}
<div className="flex-1 flex items-center justify-center">
<div className="grid grid-cols-10 gap-0.5 w-full">
{[...Array(10)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className="w-5 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
jstype="testport-mpo"
id={`SW-TOP-${i + 1}`}
>
<div className="w-4 h-4 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center"></div>
</div>
</div>
))}
</div>
</div>
{/* Bottom Row Ports (11-20) - RJ45 */}
<div className="flex-1 flex items-center justify-center">
<div className="grid grid-cols-10 gap-0.5 w-full">
{[...Array(10)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className="w-5 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
jstype="testport-mpo"
id={`SW-BOT-${i + 1}`}
>
<div className="w-4 h-4 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center"></div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Right Side - Fiber Ports (20%) */}
<div className="w-2/6 flex flex-col">
{/* Fiber Port Labels */}
<div className="flex justify-between px-1 text-xs text-gray-400">
<span>SFP+</span>
<span>21-24</span>
</div>
{/* Top Row Fiber Ports (21-22) */}
<div className="flex-1 flex items-center justify-center">
<div className="flex space-x-4"> {/* 使用 space-x-4 添加水平间距 */}
{[...Array(2)].map((_, i) => (
<div key={i} className="flex justify-center">
<div
className="w-10 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
>
<div className="flex">
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"></div>
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"></div>
</div>
</div>
</div>
))}
</div>
</div>
{/* Bottom Row Fiber Ports (23-24) */}
<div className="flex-1 flex items-center justify-center">
<div className="flex space-x-4"> {/* 使用 space-x-4 添加水平间距 */}
{[...Array(2)].map((_, i) => (
<div key={i} className="flex justify-center">
<div
className="w-10 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
>
<div className="flex">
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"
></div>
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"
></div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Switch Bottom Area - Console and Power */}
<div className="h-6 flex items-center justify-between px-2 border-t border-gray-700">
<div className="flex items-center space-x-2">
<div className="text-xs text-gray-400">Console</div>
<div className="w-4 h-2 bg-gray-600 rounded-sm"></div>
</div>
<div className="flex items-center space-x-2">
<div className="text-xs text-gray-400">Power</div>
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
</div>
</div>
</div>
{/* CC机柜内容 */}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,137 @@
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import useDeviceStore from '@/store/deviceStore';
import { API_URLS } from '@/config/api';
export default function DataCenterTask() {
const { faultScenarios } = useDeviceStore();
const [connections, setConnections] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchConnectionMap = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(API_URLS.CONNECTION.MAP_WITH_SCENE(faultScenarios));
if (!response.ok) {
throw new Error('获取连接配置失败');
}
const data = await response.json();
// 将API返回的数据转换为所需的格式
// 过滤掉包含特定关键字的端口
const formattedConnections = 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
}));
setConnections(formattedConnections);
} catch (err) {
setError(err.message);
console.error('获取连接配置错误:', err);
} finally {
setLoading(false);
}
};
fetchConnectionMap();
}, [faultScenarios?.currentScene]);
return (
// 主容器渐变背景、内边距、圆角、尺寸限制、Flex 布局、阴影、边框和动画
<div className="bg-gradient-to-br from-[#1E293B] to-[#0F172A] p-6 rounded-lg w-[full] h-[80vh] overflow-y-auto flex flex-col gap-6 shadow-2xl border border-[#334155] animate-fadeIn scrollbar-thin scrollbar-thumb-slate-600 scrollbar-track-slate-800">
{/* 任务标题 */}
<div className="border-b border-cyan-700 pb-3 mb-3">
<p className="text-sm font-semibold text-cyan-400 uppercase tracking-wider">新任务单</p>
<h1 className="text-2xl md:text-3xl font-bold text-gray-100 mt-1">任务要求数据中心MPO链路认证</h1>
<p className="text-xs text-gray-400">任务编号: SIM-TRN-DC-001 </p>
<p className="text-xs text-gray-400">地点: GlobalTech 数据中心 - A区机房</p>
</div>
{/* 情况说明 (背景) */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">情况说明</h2>
<p className="text-gray-300 leading-relaxed">
GlobalTech 数据中心刚刚完成了对其 A区机房的关键升级新的高密度光纤网络基础设施已部署完毕连接了三个核心机柜<span className="font-medium text-yellow-300">Rack CARack CB Rack CC</span> <span className="font-medium text-yellow-300">Rack CA</span> MPO线1A1B<span className="font-medium text-yellow-300">Rack CB</span> <span className="font-medium text-yellow-300">Rack CC</span> MPO线1A
</p>
<div className=" h-100 relative">
<Image src="/DataCenter.png" alt="datacenter" fill className="object-contain"/>
</div>
<p className="text-gray-300 leading-relaxed">
在这些高速链路正式投入运行前数据中心运维团队要求进行 <span className="font-semibold text-red-400">全面的MPO光纤链路认证测试</span> <span className="font-medium text-yellow-300">ISO</span>
</p>
</div>
{/* 任务目标 */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">任务目标</h2>
<ul className="list-disc list-inside text-gray-300 space-y-2 pl-4">
<li>
<strong className="text-gray-100">了解环境</strong> <span className="font-medium text-yellow-300">MPO</span> Rack CARack CB Rack CC 线
</li>
<li>
<strong className="text-gray-100">测试设备</strong> <span className="font-medium text-yellow-300"></span>ISO
</li>
<li>
<strong className="text-gray-100">执行认证测试</strong> MPO <span className="font-medium text-yellow-300">ISO</span>
</li>
<li>
<strong className="text-gray-100">验证光纤类型</strong> 使 <span className="font-medium text-yellow-300"></span>
</li>
<li>
<strong className="text-gray-100">分析测试数据</strong> <span className="font-semibold text-green-400"> (PASS)</span> <span className="font-semibold text-red-400"> (FAIL*)</span>
</li>
<li>
<strong className="text-gray-100">诊断故障点</strong> (<span className='text-red-400 font-bold'>FAIL*</span>)使
</li>
<li>
<strong className="text-gray-100">记录与报告</strong> 使<code className="bg-gray-700 px-1.5 py-0.5 rounded text-xs text-yellow-300">[-线]-[-线]</code> (<code className="bg-gray-700 px-1.5 py-0.5 rounded text-xs text-yellow-300">RackCA-1A-RackCB-1A</code>)
</li>
</ul>
</div>
{/* 目标链路概览 (可选:此处保持简洁) */}
<div className="space-y-2">
<h3 className="text-lg font-semibold text-cyan-300">目标链路概览</h3>
<p className="text-gray-300">你将测试数据中心三个核心机柜之间的多条MPO光纤链路系统操作界面将模拟每条链路的测试过程</p>
{/* 如何列出它们的示例,或者只保留上面的段落 */}
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm text-gray-400 pl-4">
{loading ? (
<p>加载中...</p>
) : error ? (
<p className="text-red-400">{error}</p>
) : (
connections.map(link => (
<p key={link.rack}>
<code className="text-yellow-400">{link.room}</code> &lt;--&gt;
<code className="text-yellow-400">{link.rack}</code>
</p>
))
)}
</div>
</div>
{/* 最终指示 */}
<div className="mt-4 border-t border-cyan-700 pt-4">
<p className="text-lg font-semibold text-gray-100">请进入模拟测试环境</p>
<p className="text-gray-300">配置好你的光纤测试设备按照ISO标准执行二级认证测试并详细记录你的发现数据中心的高可用性和性能取决于这些光纤链路的质量和你的专业测试能力</p>
<p className="mt-2 text-cyan-400 font-medium">祝你测试顺利</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,195 @@
import React from 'react';
export default function Industry({
onPortClick = () => {},
onPortHover = () => {},
selectedPort = null,
targetPort = null,
connections = {}
}) {
// 获取端口的连接状态样式
const getPortStyle = (portId) => {
if (selectedPort === portId) {
return 'bg-blue-300';
}
if (targetPort === portId) {
return 'bg-green-300';
}
if (connections && connections[portId]) {
return 'bg-[#00ff7f]';
}
return 'bg-gray-300';
};
return (
<div className="h-full w-full bg-black text-white flex">
{/* 工业设备区域 - 左侧45% */}
<div className="w-[45%] p-2">
<div className="grid grid-cols-2 gap-4 h-full">
{/* 设备1 */}
<div className="bg-gray-900 rounded-lg p-2 flex flex-col">
<div className="text-center mb-2">Device1</div>
<div className="flex items-center justify-center flex-1">
<div className="bg-gray-800 p-4 rounded-lg">
<div className="flex space-x-4">
<div
className={` rounded-full flex items-center justify-center transition-colors ${getPortStyle('Device1-1')}`}
jstype="testport-m12-d"
id="Device1-1"
onClick={() => onPortClick('Device1-1')}
onMouseEnter={() => onPortHover('Device1-1')}
>
<div className={`w-8 h-8 bg-[url('/m12-d.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<div
className={` rounded-full flex items-center justify-center transition-colors ${getPortStyle('Device1-2')}`}
jstype="testport-m12-d"
id="Device1-2"
onClick={() => onPortClick('Device1-2')}
onMouseEnter={() => onPortHover('Device1-2')}
>
<div className={`w-8 h-8 bg-[url('/m12-d.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
</div>
</div>
</div>
</div>
{/* 设备2 */}
<div className="bg-gray-900 rounded-lg p-2 flex flex-col">
<div className="text-center mb-2">Device2</div>
<div className="flex items-center justify-center flex-1">
<div className="bg-gray-800 p-4 rounded-lg">
<div className="flex space-x-4">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Device2-1')}`}
jstype="testport-m12-d"
id="Device2-1"
onClick={() => onPortClick('Device2-1')}
onMouseEnter={() => onPortHover('Device2-1')}
>
<div className={`w-8 h-8 bg-[url('/m12-d.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Device2-2')}`}
jstype="testport-m12-d"
id="Device2-2"
onClick={() => onPortClick('Device2-2')}
onMouseEnter={() => onPortHover('Device2-2')}
>
<div className={`w-8 h-8 bg-[url('/m12-d.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
</div>
</div>
</div>
</div>
{/* 设备3 */}
<div className="bg-gray-900 rounded-lg p-2 flex flex-col">
<div className="text-center mb-2">Device3</div>
<div className="flex items-center justify-center flex-1">
<div className="bg-gray-800 p-4 rounded-lg">
<div className="flex space-x-4">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Device3-1')}`}
jstype="testport-copper"
id="Device3-1"
onClick={() => onPortClick('Device3-1')}
onMouseEnter={() => onPortHover('Device3-1')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Device3-2')}`}
jstype="testport-copper"
id="Device3-2"
onClick={() => onPortClick('Device3-2')}
onMouseEnter={() => onPortHover('Device3-2')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
</div>
</div>
</div>
</div>
{/* 设备4 */}
<div className="bg-gray-900 rounded-lg p-2 flex flex-col">
<div className="text-center mb-2">Device4</div>
<div className="flex items-center justify-center flex-1">
<div className="bg-gray-800 p-4 rounded-lg">
<div className="flex space-x-4">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Device4-1')}`}
jstype="testport-copper"
id="Device4-1"
onClick={() => onPortClick('Device4-1')}
onMouseEnter={() => onPortHover('Device4-1')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Device4-2')}`}
jstype="testport-copper"
id="Device4-2"
onClick={() => onPortClick('Device4-2')}
onMouseEnter={() => onPortHover('Device4-2')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 工业控制柜 - 右侧55% */}
<div className="w-[55%] p-2">
<div className="text-center mb-2">Cabinet</div>
<div className="bg-gray-800 rounded-lg p-4 h-[calc(100%-2rem)]">
<div className="h-full flex flex-col space-y-4">
{/* 上层端口组 */}
<div className="flex-1 flex items-center justify-center">
<div className="grid grid-cols-4 gap-8">
{[...Array(4)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-12 h-12 mb-2 flex items-center justify-center transition-colors ${getPortStyle(`Cabinet-A${i + 1}`)}`}
jstype="testport-m12-d"
id={`Cabinet-A${i + 1}`}
onClick={() => onPortClick(`Cabinet-A${i + 1}`)}
onMouseEnter={() => onPortHover(`Cabinet-A${i + 1}`)}
>
<div className={`w-8 h-8 bg-[url('/m12-d.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">A{i + 1}</span>
</div>
))}
</div>
</div>
{/* 下层端口组 */}
<div className="flex-1 flex items-center justify-center">
<div className="grid grid-cols-4 gap-8">
{[...Array(4)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-12 h-12 mb-2 flex items-center justify-center transition-colors ${getPortStyle(`Cabinet-R${i + 1}`)}`}
jstype="testport-copper"
id={`Cabinet-R${i + 1}`}
onClick={() => onPortClick(`Cabinet-R${i + 1}`)}
onMouseEnter={() => onPortHover(`Cabinet-R${i + 1}`)}>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">R{i + 1}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,130 @@
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import useDeviceStore from '@/store/deviceStore';
import { API_URLS } from '@/config/api';
export default function IndustryTask() {
const { faultScenarios } = useDeviceStore();
const [connections, setConnections] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchConnectionMap = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(API_URLS.CONNECTION.MAP_WITH_SCENE(faultScenarios));
if (!response.ok) {
throw new Error('获取连接配置失败');
}
const data = await response.json();
const formattedConnections = 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
}));
setConnections(formattedConnections);
} catch (err) {
setError(err.message);
console.error('获取连接配置错误:', err);
} finally {
setLoading(false);
}
};
fetchConnectionMap();
}, [faultScenarios?.currentScene]);
return (
<div className="bg-gradient-to-br from-[#1E293B] to-[#0F172A] p-6 rounded-lg w-[full] h-[80vh] overflow-y-auto flex flex-col gap-6 shadow-2xl border border-[#334155] animate-fadeIn scrollbar-thin scrollbar-thumb-slate-600 scrollbar-track-slate-800">
{/* 任务标题 */}
<div className="border-b border-cyan-700 pb-3 mb-3">
<p className="text-sm font-semibold text-cyan-400 uppercase tracking-wider">新任务单</p>
<h1 className="text-2xl md:text-3xl font-bold text-gray-100 mt-1">任务要求工业网络链路认证</h1>
<p className="text-xs text-gray-400">任务编号: SIM-IND-NTW-007 </p>
<p className="text-xs text-gray-400">地点: 智能制造产业园 - 自动化生产线</p>
</div>
{/* 情况说明 */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">情况说明</h2>
<p className="text-gray-300 leading-relaxed">
智能制造产业园正在进行<span className="font-medium text-yellow-300">自动化生产线网络升级</span><span className="font-medium text-yellow-300">4Device X</span><span className="font-medium text-yellow-300">Cabinet</span>
</p>
<div className=" h-100 relative">
<Image src="/Industry.png" alt="industry" fill className="object-contain"/>
</div>
<p className="text-gray-300 leading-relaxed">
为确保生产线的稳定运行需要对所有工业网络链路进行<span className="font-semibold text-red-400">严格的物理层认证测试</span><span className="font-medium text-yellow-300">Cat 6 F/UTP</span>线
</p>
</div>
{/* 任务目标 */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">任务目标</h2>
<ul className="list-disc list-inside text-gray-300 space-y-2 pl-4">
<li>
<strong className="text-gray-100">环境评估</strong> <span className="font-medium text-yellow-300">4</span> Device X-Y Cabinet 线
</li>
<li>
<strong className="text-gray-100">测试准备</strong> 使<span className="font-medium text-yellow-300">DSX </span>
</li>
<li>
<strong className="text-gray-100">认证测试</strong> <span className="font-medium text-yellow-300">TIA 1005 </span><span className="font-medium text-yellow-300">(Crosstalk)</span><span className="font-medium text-yellow-300">(EMI)E2</span>
</li>
<li>
<strong className="text-gray-100">数据分析</strong> <span className="font-semibold text-green-400">(PASS)</span><span className="font-semibold text-red-400">(FAIL*)</span>
</li>
<li>
<strong className="text-gray-100">故障诊断</strong> 使<span className="font-medium text-yellow-300"></span>
</li>
<li>
<strong className="text-gray-100">报告记录</strong> <code className="bg-gray-700 px-1.5 py-0.5 rounded text-xs text-yellow-300">[]-[]</code> (<code className="bg-gray-700 px-1.5 py-0.5 rounded text-xs text-yellow-300">Device1-1-Cabinet-A1</code>)
</li>
</ul>
</div>
{/* 链路概览 */}
<div className="space-y-2">
<h3 className="text-lg font-semibold text-cyan-300">工业链路概览</h3>
<p className="text-gray-300">需要测试的工业网络链路包括各控制区域与中央控制室之间的连接每条链路都配备了工业级屏蔽接头</p>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm text-gray-400 pl-4">
{loading ? (
<p>加载中...</p>
) : error ? (
<p className="text-red-400">{error}</p>
) : (
connections.map(link => (
<p key={link.rack}>
<code className="text-yellow-400">{link.room}</code> &lt;--&gt;
<code className="text-yellow-400">{link.rack}</code>
</p>
))
)}
</div>
</div>
{/* 最终指示 */}
<div className="mt-4 border-t border-cyan-700 pt-4">
<p className="text-lg font-semibold text-gray-100">开始工业网络测试</p>
<p className="text-gray-300">请严格按照工业标准进行测试记住这些网络链路的可靠性直接关系到生产线的正常运转</p>
<p className="mt-2 text-cyan-400 font-medium">确保安全祝你测试顺利</p>
</div>
</div>
);
}

258
src/components/scene/MDF.js Normal file
View File

@@ -0,0 +1,258 @@
{/* Network Router */}
<div className="flex flex-col h-1/2 bg-gray-900 rounded-md p-2 relative border border-gray-700">
{/* Switch Top Area - Brand and Status LEDs */}
<div className="h-8 flex items-center justify-between px-2 border-b border-gray-700">
<div className="text-blue-500 font-bold text-sm">Router</div>
<div className="flex space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-amber-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
</div>
</div>
{/* Switch Port Area */}
<div className="flex-1 flex flex-row">
{/* Right Side - Fiber Ports (20%) */}
<div className="w-6/6 flex flex-col">
{/* Fiber Port Labels */}
<div className="flex justify-between px-1 text-xs text-gray-400">
<span>SFP+</span>
<span>1-12</span>
</div>
{/* Top Row Fiber Ports (1-6) */}
<div className="flex-1 flex items-center justify-center">
<div className="flex space-x-4"> {/* 使用 space-x-4 添加水平间距 */}
{[...Array(6)].map((_, i) => (
<div key={i} className="flex justify-center">
<div
className="w-10 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
>
<div className="flex">
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"></div>
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"></div>
</div>
</div>
</div>
))}
</div>
</div>
{/* Bottom Row Fiber Ports (6-12) */}
<div className="flex-1 flex items-center justify-center">
<div className="flex space-x-4"> {/* 使用 space-x-4 添加水平间距 */}
{[...Array(6)].map((_, i) => (
<div key={i} className="flex justify-center">
<div
className="w-10 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
>
<div className="flex">
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"
></div>
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"
></div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Switch Bottom Area - Console and Power */}
<div className="h-6 flex items-center justify-between px-2 border-t border-gray-700">
<div className="flex items-center space-x-2">
<div className="text-xs text-gray-400">Console</div>
<div className="w-4 h-2 bg-gray-600 rounded-sm"></div>
</div>
<div className="flex items-center space-x-2">
<div className="text-xs text-gray-400">Power</div>
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
</div>
</div>
</div>
{/* Network Switch */}
<div className="flex flex-col h-1/2 bg-gray-900 rounded-md p-2 relative border border-gray-700">
{/* Switch Top Area - Brand and Status LEDs */}
<div className="h-8 flex items-center justify-between px-2 border-b border-gray-700">
<div className="text-blue-500 font-bold text-sm">SWITCH</div>
<div className="flex space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-amber-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
</div>
</div>
{/* Switch Port Area */}
<div className="flex-1 flex flex-row">
{/* Left Side - RJ45 Ports (80%) */}
<div className="w-4/6 flex flex-col">
{/* Port Labels */}
<div className="flex justify-between px-4 text-xs text-gray-400">
<span>10/100/1000 Base-T</span>
<span>1-20</span>
</div>
{/* Top Row Ports (1-10) - RJ45 */}
<div className="flex-1 flex items-center justify-center">
<div className="grid grid-cols-10 gap-0.5 w-full">
{[...Array(10)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className="w-5 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
jstype="testport-mpo"
id={`SW-TOP-${i + 1}`}
>
<div className="w-4 h-4 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center"></div>
</div>
</div>
))}
</div>
</div>
{/* Bottom Row Ports (11-20) - RJ45 */}
<div className="flex-1 flex items-center justify-center">
<div className="grid grid-cols-10 gap-0.5 w-full">
{[...Array(10)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className="w-5 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
jstype="testport-mpo"
id={`SW-BOT-${i + 1}`}
>
<div className="w-4 h-4 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center"></div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Right Side - Fiber Ports (20%) */}
<div className="w-2/6 flex flex-col">
{/* Fiber Port Labels */}
<div className="flex justify-between px-1 text-xs text-gray-400">
<span>SFP+</span>
<span>21-24</span>
</div>
{/* Top Row Fiber Ports (21-22) */}
<div className="flex-1 flex items-center justify-center">
<div className="flex space-x-4"> {/* 使用 space-x-4 添加水平间距 */}
{[...Array(2)].map((_, i) => (
<div key={i} className="flex justify-center">
<div
className="w-10 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
>
<div className="flex">
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"></div>
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"></div>
</div>
</div>
</div>
))}
</div>
</div>
{/* Bottom Row Fiber Ports (23-24) */}
<div className="flex-1 flex items-center justify-center">
<div className="flex space-x-4"> {/* 使用 space-x-4 添加水平间距 */}
{[...Array(2)].map((_, i) => (
<div key={i} className="flex justify-center">
<div
className="w-10 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
>
<div className="flex">
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"
></div>
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"
></div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Switch Bottom Area - Console and Power */}
<div className="h-6 flex items-center justify-between px-2 border-t border-gray-700">
<div className="flex items-center space-x-2">
<div className="text-xs text-gray-400">Console</div>
<div className="w-4 h-2 bg-gray-600 rounded-sm"></div>
</div>
<div className="flex items-center space-x-2">
<div className="text-xs text-gray-400">Power</div>
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
</div>
</div>
</div>
{/* LC配线架 */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
{/* 修改配架名称 */}
<span className="text-xs mr-4">1B</span>
<div className="flex-1 flex items-center justify-center space-x-1">
{/* 修改端口数量 */}
{[...Array(12)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-8 h-8 mb-1 transition-colors flex items-center justify-center `}
jstype="testport-fiber" //修改jstype端点读取名称
id={`CB-1B-${i + 1}`} //修改id
onClick={() => onPortClick(`1A-${i + 1}`)}
onMouseEnter={() => onPortHover(`1A-${i + 1}`)}
>
<div className={`w-8 h-8 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>
{/* MPO配线架 */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1A</span>
<div className="flex-1 flex items-center justify-center space-x-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-12 h-10 mb-1 transition-colors flex items-center justify-center `}
jstype="testport-mpo"
id={`CA-1A-${i + 1}`}
onClick={() => onPortClick(`1A-${i + 1}`)}
onMouseEnter={() => onPortHover(`1A-${i + 1}`)}
>
<div className={`w-12 h-10 bg-[url('/mpo.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>

View File

@@ -0,0 +1,210 @@
import React from 'react';
export default function Office({
onPortClick = () => {},
onPortHover = () => {},
selectedPort = null,
targetPort = null,
connections = {}
}) {
// 获取端口的连接状态样式
const getPortStyle = (portId) => {
if (selectedPort === portId) {
return 'bg-blue-300';
}
if (targetPort === portId) {
return 'bg-green-300';
}
if (connections && connections[portId]) {
return 'bg-[#00ff7f]';
}
return 'bg-gray-300';
};
return (
<div className="h-full w-full bg-black text-white flex">
{/* Room panels - each taking 1/8 of width */}
<div className="w-1/8 p-1 flex flex-col">
<div className="text-center mb-1">Room1</div>
<div className="flex-1 bg-gray-900 flex items-center justify-center">
<div className="bg-gray-300 w-24 h-24 flex items-center justify-center">
<div className="flex space-x-4">
<div className="flex flex-col items-center">
<div
className={` rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room1-TO-1')}`}
jstype="testport-copper"
id="Room1-TO-1"
onClick={() => onPortClick('Room1-TO-1')}
onMouseEnter={() => onPortHover('Room1-TO-1')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs mt-1 text-black">TO-1</span>
</div>
<div className="flex flex-col items-center">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room1-TO-2')}`}
jstype="testport-copper"
id="Room1-TO-2"
onClick={() => onPortClick('Room1-TO-2')}
onMouseEnter={() => onPortHover('Room1-TO-2')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs mt-1 text-black">TO-2</span>
</div>
</div>
</div>
</div>
</div>
<div className="w-1/8 p-1 flex flex-col">
<div className="text-center mb-1">Room2</div>
<div className="flex-1 bg-gray-900 flex items-center justify-center">
<div className="bg-gray-300 w-24 h-24 flex items-center justify-center">
<div className="flex space-x-4">
<div className="flex flex-col items-center">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room2-TO-1')}`}
jstype="testport-copper"
id="Room2-TO-1"
onClick={() => onPortClick('Room2-TO-1')}
onMouseEnter={() => onPortHover('Room2-TO-1')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs mt-1 text-black">TO-1</span>
</div>
<div className="flex flex-col items-center">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room2-TO-2')}`}
jstype="testport-copper"
id="Room2-TO-2"
onClick={() => onPortClick('Room2-TO-2')}
onMouseEnter={() => onPortHover('Room2-TO-2')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs mt-1 text-black">TO-2</span>
</div>
</div>
</div>
</div>
</div>
<div className="w-1/8 p-1 flex flex-col">
<div className="text-center mb-1">Room3</div>
<div className="flex-1 bg-gray-900 flex items-center justify-center">
<div className="bg-gray-300 w-24 h-24 flex items-center justify-center">
<div className="flex space-x-4">
<div className="flex flex-col items-center">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room3-TO-1')}`}
jstype="testport-copper"
id="Room3-TO-1"
onClick={() => onPortClick('Room3-TO-1')}
onMouseEnter={() => onPortHover('Room3-TO-1')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs mt-1 text-black">TO-1</span>
</div>
<div className="flex flex-col items-center">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room3-TO-2')}`}
jstype="testport-copper"
id="Room3-TO-2"
onClick={() => onPortClick('Room3-TO-2')}
onMouseEnter={() => onPortHover('Room3-TO-2')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs mt-1 text-black">TO-2</span>
</div>
</div>
</div>
</div>
</div>
<div className="w-1/8 p-1 flex flex-col">
<div className="text-center mb-1">Room4</div>
<div className="relative flex-1 bg-gray-900 flex items-center justify-center">
<div
className={`absolute top-[55%] left-[66%] w-6 h-6 z-100`}
jstype="testport-cam"
id="Room4-CAM"
onClick={() => onPortClick('Room4-CAM')}
onMouseEnter={() => onPortHover('Room4-CAM')}
>
</div>
<div className={`w-full h-full bg-[url('/cam.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
</div>
{/* Rack taking 1/2 of the width */}
<div className="w-1/2 p-1 flex flex-col">
<div className="text-center mb-1">Rack1</div>
<div className="h-[calc(100%-1.5rem)] rounded-md p-2 flex flex-col space-y-2">
{/* Row 1A - taking 1/2 of rack height */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1A</span>
<div className="flex-1 flex items-center justify-center space-x-4">
{[...Array(8)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-16 h-16 mb-1 transition-colors flex items-center justify-center ${getPortStyle(`1A-${i + 1}`)}`}
jstype="testport-copper"
id={`1A-${i + 1}`}
onClick={() => onPortClick(`1A-${i + 1}`)}
onMouseEnter={() => onPortHover(`1A-${i + 1}`)}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>
{/* Row 1B - taking 1/2 of rack height */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1B</span>
<div className="flex-1 flex items-center justify-center space-x-4">
{[...Array(8)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-16 h-16 mb-1 transition-colors flex items-center justify-center ${getPortStyle(`1B-${i + 1}`)}`}
jstype="testport-fiber"
lcclean="false"
id={`1B-${i + 1}`}
onClick={() => onPortClick(`1B-${i + 1}`)}
onMouseEnter={() => onPortHover(`1B-${i + 1}`)}
>
<div className={`w-8 h-8 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,137 @@
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import useDeviceStore from '@/store/deviceStore';
import { API_URLS } from '@/config/api';
export default function OfficeTask() {
const { faultScenarios } = useDeviceStore();
const [connections, setConnections] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchConnectionMap = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(API_URLS.CONNECTION.MAP_WITH_SCENE(faultScenarios));
if (!response.ok) {
throw new Error('获取连接配置失败');
}
const data = await response.json();
// 将API返回的数据转换为所需的格式
// 过滤掉包含特定关键字的端口
const formattedConnections = 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
}));
setConnections(formattedConnections);
} catch (err) {
setError(err.message);
console.error('获取连接配置错误:', err);
} finally {
setLoading(false);
}
};
fetchConnectionMap();
}, [faultScenarios?.currentScene]);
return (
// 主容器渐变背景、内边距、圆角、尺寸限制、Flex 布局、阴影、边框和动画
<div className="bg-gradient-to-br from-[#1E293B] to-[#0F172A] p-6 rounded-lg w-[full] h-[80vh] overflow-y-auto flex flex-col gap-6 shadow-2xl border border-[#334155] animate-fadeIn scrollbar-thin scrollbar-thumb-slate-600 scrollbar-track-slate-800">
{/* 任务标题 */}
<div className="border-b border-cyan-700 pb-3 mb-3">
<p className="text-sm font-semibold text-cyan-400 uppercase tracking-wider">新任务单</p>
<h1 className="text-2xl md:text-3xl font-bold text-gray-100 mt-1">任务要求网络链路认证</h1>
<p className="text-xs text-gray-400">任务编号: SIM-TRN-NTW-005 </p>
<p className="text-xs text-gray-400">地点: Innovate Solutions 公司 - 8 层西翼</p>
</div>
{/* 情况说明 (背景) */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">情况说明</h2>
<p className="text-gray-300 leading-relaxed">
Innovate Solutions 公司刚刚完成了对其 8 层西翼办公区<span className="font-medium text-yellow-300">Room1 Room4</span> <span className="font-medium text-yellow-300"> 1 (Rack1)</span>
</p>
<div className=" h-100 relative">
<Image src="/Office.png" alt="office" fill className="object-contain"/>
</div>
<p className="text-gray-300 leading-relaxed">
在这些重要链路正式启用前集团工程部要求进行 <span className="font-semibold text-red-400">全面的物理层认证测试</span> <span className="font-medium text-yellow-300">TIA</span>
</p>
</div>
{/* 任务目标 */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">任务目标</h2>
<ul className="list-disc list-inside text-gray-300 space-y-2 pl-4">
<li>
<strong className="text-gray-100">了解环境</strong> <span className="font-medium text-yellow-300">7 </span> Room1-4 Rack1 1A线
</li>
<li>
<strong className="text-gray-100">部署测试设备</strong> 使 <span className="font-medium text-yellow-300">DSX 线</span>
</li>
<li>
<strong className="text-gray-100">执行认证测试</strong> <span className="font-medium text-yellow-300"></span> <span className="font-medium text-yellow-300">T568B</span> 线 <span className="font-medium text-yellow-300">Cat 6 (F/UTP)</span> 线
</li>
<li>
<strong className="text-gray-100">执行认证测试</strong> <span className="font-medium text-yellow-300"></span> <span className="font-medium text-yellow-300">T568B</span> 线 <span className="font-medium text-yellow-300">Cat 6 (F/UTP)</span> 线
</li>
<li>
<strong className="text-gray-100">分析测试数据</strong> <span className="font-semibold text-green-400"> (PASS)</span> <span className="font-semibold text-red-400"> (FAIL*)</span>
</li>
<li>
<strong className="text-gray-100">诊断故障点</strong> (<span className='text-red-400 font-bold'>FAIL*</span>)使 ( HDTDR/HDTDX)
</li>
<li>
<strong className="text-gray-100">记录与报告</strong> 使<code className="bg-gray-700 px-1.5 py-0.5 rounded text-xs text-yellow-300">[]-[]</code> (<code className="bg-gray-700 px-1.5 py-0.5 rounded text-xs text-yellow-300">Room1-TO-1-Rack1-1A-1</code>)
</li>
</ul>
</div>
{/* 目标链路概览 (可选:此处保持简洁) */}
<div className="space-y-2">
<h3 className="text-lg font-semibold text-cyan-300">目标链路概览</h3>
<p className="text-gray-300">你将测试办公室房间与设备机柜之间的 8 条独立连接系统操作界面将模拟每条链路</p>
{/* 如何列出它们的示例,或者只保留上面的段落 */}
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm text-gray-400 pl-4">
{loading ? (
<p>加载中...</p>
) : error ? (
<p className="text-red-400">{error}</p>
) : (
connections.map(link => (
<p key={link.rack}>
<code className="text-yellow-400">{link.room}</code> &lt;--&gt;
<code className="text-yellow-400">{link.rack}</code>
</p>
))
)}
</div>
</div>
{/* 最终指示 */}
<div className="mt-4 border-t border-cyan-700 pt-4">
<p className="text-lg font-semibold text-gray-100">请进入模拟测试环境</p>
<p className="text-gray-300">配置好你的设备认真执行测试并报告你的发现网络的可用性取决于你的专业能力</p>
<p className="mt-2 text-cyan-400 font-medium">祝你测试顺利</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,174 @@
import React from 'react';
export default function SkillCompetition({
onPortClick = () => {},
onPortHover = () => {},
selectedPort = null,
targetPort = null,
connections = {}
}) {
// 获取端口的连接状态样式
const getPortStyle = (portId) => {
if (selectedPort === portId) {
return 'bg-blue-300';
}
if (targetPort === portId) {
return 'bg-green-300';
}
if (connections && connections[portId]) {
return 'bg-[#00ff7f]';
}
return 'bg-gray-300';
};
return (
<div className="h-full w-full bg-black text-white flex">
{/* Room panels - each taking 1/8 of width */}
<div className="w-1/8 p-1 flex flex-col">
<div className="text-center mb-1">Room1</div>
<div className="flex-1 bg-gray-900 flex items-center justify-center">
<div className="bg-gray-300 w-24 h-24 flex items-center justify-center">
<div className="flex space-x-4">
<div
className={` rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room1-1')}`}
jstype="testport-copper"
id="Room1-1"
onClick={() => onPortClick('Room1-1')}
onMouseEnter={() => onPortHover('Room1-1')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room1-2')}`}
jstype="testport-copper"
id="Room1-2"
onClick={() => onPortClick('Room1-2')}
onMouseEnter={() => onPortHover('Room1-2')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
</div>
</div>
</div>
</div>
<div className="w-1/8 p-1 flex flex-col">
<div className="text-center mb-1">Room2</div>
<div className="flex-1 bg-gray-900 flex items-center justify-center">
<div className="bg-gray-300 w-24 h-24 flex items-center justify-center">
<div className="flex space-x-4">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room2-1')}`}
jstype="testport-copper"
id="Room2-1"
onClick={() => onPortClick('Room2-1')}
onMouseEnter={() => onPortHover('Room2-1')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room2-2')}`}
jstype="testport-copper"
id="Room2-2"
onClick={() => onPortClick('Room2-2')}
onMouseEnter={() => onPortHover('Room2-2')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
</div>
</div>
</div>
</div>
<div className="w-1/8 p-1 flex flex-col">
<div className="text-center mb-1">Room3</div>
<div className="flex-1 bg-gray-900 flex items-center justify-center">
<div className="bg-gray-300 w-24 h-24 flex items-center justify-center">
<div className="flex space-x-4">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room3-1')}`}
jstype="testport-copper"
id="Room3-1"
onClick={() => onPortClick('Room3-1')}
onMouseEnter={() => onPortHover('Room3-1')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room3-2')}`}
jstype="testport-copper"
id="Room3-2"
onClick={() => onPortClick('Room3-2')}
onMouseEnter={() => onPortHover('Room3-2')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
</div>
</div>
</div>
</div>
{/* Rack taking 1/2 of the width */}
<div className="w-5/8 p-1 flex flex-col">
<div className="text-center mb-1">Rack1</div>
<div className="h-[calc(100%-1.5rem)] rounded-md p-2 flex flex-col space-y-2">
{/* Row 1A - taking 1/2 of rack height */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1A</span>
<div className="flex-1 flex items-center justify-center space-x-4">
{[...Array(12)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-12 h-12 mb-1 transition-colors flex items-center justify-center ${getPortStyle(`1A-${i + 1}`)}`}
jstype="testport-copper"
id={`1A-${i + 1}`}
onClick={() => onPortClick(`1A-${i + 1}`)}
onMouseEnter={() => onPortHover(`1A-${i + 1}`)}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>
{/* Row 1B - taking 1/2 of rack height */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1B</span>
<div className="flex-1 flex items-center justify-center space-x-4">
{[...Array(12)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-12 h-12 mb-1 transition-colors flex items-center justify-center ${getPortStyle(`1B-${i + 1}`)}`}
jstype="testport-copper"
id={`1B-${i + 1}`}
onClick={() => onPortClick(`1B-${i + 1}`)}
onMouseEnter={() => onPortHover(`1B-${i + 1}`)}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,122 @@
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import useDeviceStore from '@/store/deviceStore';
export default function SkillCompetitionTask() {
const { faultScenarios } = useDeviceStore();
const [connections, setConnections] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchConnectionMap = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(`https://dsxapi.est-live.cn/api/connectionMap?scene=${faultScenarios}`);
if (!response.ok) {
throw new Error('获取连接配置失败');
}
const data = await response.json();
// 将API返回的数据转换为所需的格式
// 过滤掉包含特定关键字的端口
const formattedConnections = 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
}));
setConnections(formattedConnections);
} catch (err) {
setError(err.message);
console.error('获取连接配置错误:', err);
} finally {
setLoading(false);
}
};
fetchConnectionMap();
}, [faultScenarios?.currentScene]);
return (
// 主容器渐变背景、内边距、圆角、尺寸限制、Flex 布局、阴影、边框和动画
<div className="bg-gradient-to-br from-[#1E293B] to-[#0F172A] p-6 rounded-lg w-[full] h-[80vh] overflow-y-auto flex flex-col gap-6 shadow-2xl border border-[#334155] animate-fadeIn scrollbar-thin scrollbar-thumb-slate-600 scrollbar-track-slate-800">
{/* 任务标题 */}
<div className="border-b border-cyan-700 pb-3 mb-3">
<p className="text-sm font-semibold text-cyan-400 uppercase tracking-wider">新任务单</p>
<h1 className="text-2xl md:text-3xl font-bold text-gray-100 mt-1">任务要求网络链路认证</h1>
<p className="text-xs text-gray-400">任务编号: ZHENXING-NET-001 </p>
<p className="text-xs text-gray-400">地点: 真新镇办公园区楼宇 - 3 层西翼</p>
</div>1
{/* 情况说明 (背景) */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">情况说明</h2>
<p className="text-gray-300 leading-relaxed">
真新镇办公园区楼宇正在为其新设立的研发中心进行网络基础设施升级作为一家全球领先的技术创新企业公司对网络性能和可靠性有着极高的要求目前3层西翼办公区<span className="font-medium text-yellow-300">Room1 Room3</span>线线<span className="font-medium text-yellow-300"> 1 (Rack1)</span>
</p>
<div className=" h-100 relative">
<Image src="/SkillCompetition.png" alt="office" fill className="object-contain"/>
</div>
<p className="text-gray-300 leading-relaxed">
为确保网络基础设施满足标准公司要求按照<span className="font-semibold text-red-400">GB/T 50312-2016标准</span><span className="font-medium text-yellow-300">Cat 6</span>
</p>
</div>
{/* 任务目标 */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">任务目标</h2>
<ul className="list-disc list-inside text-gray-300 space-y-2 pl-4">
<li>
<strong className="text-gray-100">了解环境</strong> <span className="font-medium text-yellow-300">6 </span> Room1-3 Rack1 Panel 1A/1B
</li>
<li>
<strong className="text-gray-100">项目创建</strong> 使 <span className="font-medium text-yellow-300">ZhenXin-2025</span>
</li>
<li>
<strong className="text-gray-100">部署测试设备</strong> 使 <span className="font-medium text-yellow-300">DSX 线</span>GB/T 50312-2016
</li>
<li>
<strong className="text-gray-100">执行认证测试</strong> <span className="font-medium text-yellow-300"></span> <span className="font-medium text-yellow-300">GB/T 50312-2016</span> <span className="font-medium text-yellow-300">Cat 6 (F/UTP)</span> 线
</li>
<li>
<strong className="text-gray-100">执行认证测试</strong> <span className="font-medium text-yellow-300"></span> <span className="font-medium text-yellow-300">GB/T 50312-2016 </span> <span className="font-medium text-yellow-300">Cat 6 (F/UTP)</span> 线
</li>
<li>
<strong className="text-gray-100">分析测试数据</strong> <span className="font-semibold text-green-400"> (PASS)</span> <span className="font-semibold text-red-400"> (FAIL*)</span>
</li>
<li>
<strong className="text-gray-100">诊断故障点</strong> (<span className='text-red-400 font-bold'>FAIL*</span>)使
</li>
<li>
<strong className="text-gray-100">记录与报告</strong> 使<code className="bg-gray-700 px-1.5 py-0.5 rounded text-xs text-yellow-300">[]-[]</code> (<code className="bg-gray-700 px-1.5 py-0.5 rounded text-xs text-yellow-300">Room1-1-Rack1-1A-1</code>)
</li>
</ul>
</div>
{/* 目标链路概览 (可选:此处保持简洁) */}
{/* 最终指示 */}
<div className="mt-4 border-t border-cyan-700 pt-4">
<p className="text-lg font-semibold text-gray-100">请进入模拟测试环境</p>
<p className="text-gray-300">配置好你的设备认真执行测试并报告你的发现网络的可用性取决于你的专业能力</p>
<p className="mt-2 text-cyan-400 font-medium">祝你测试顺利</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,361 @@
import React, { useState, useEffect } from 'react';
import useDeviceStore from '@/store/deviceStore';
import Image from 'next/image';
export default function WorldSkill({
onPortClick = () => {},
onPortHover = () => {},
selectedPort = null,
targetPort = null,
connections = {}
}) {
// 获取端口的连接状态样式
const getPortStyle = (portId) => {
if (selectedPort === portId) {
return 'bg-blue-300';
}
if (targetPort === portId) {
return 'bg-green-300';
}
if (connections && connections[portId]) {
return 'bg-[#00ff7f]';
}
return 'bg-gray-300';
};
const {faultScenarios,seatUUID,seatNumber,updateWorldSkillScenarios} = useDeviceStore();
// 从localStorage加载清洁状态
const loadCleanState = () => {
if (!seatUUID) return;
const lccleanStates = JSON.parse(localStorage.getItem(`cleanState_${seatUUID}_lc`) || '{}');
// 恢复LC端口状态
document.querySelectorAll('[lcclean]').forEach(el => {
const elementId = el.id;
if (elementId && lccleanStates[elementId] !== undefined) {
el.setAttribute('lcclean', lccleanStates[elementId].toString());
}
});
};
// 监听seatUUID变化
useEffect(() => {
if (seatUUID) {
// 使用setTimeout确保DOM元素已加载
setTimeout(loadCleanState, 500);
}
}, [seatUUID]);
return (
<div>
{/* 办公区域 */}
<div className="h-full w-full bg-black text-white flex z-index-100">
{/* Room */}
<div className="w-[12%] p-1 flex flex-col">
{/* Room 1 - 上半部分 */}
<div className="flex-1 flex flex-col">
<div className="text-center mb-1">Room1</div>
<div className="flex-1 bg-gray-900 flex items-center justify-center">
<div className="bg-gray-300 w-24 h-24 flex items-center justify-center">
<div className="flex space-x-2">
<div className="flex flex-col items-center">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room1-TO-1')}`}
jstype="testport-copper"
id="Room1-TO-1"
onClick={() => onPortClick('Room1-TO-1')}
onMouseEnter={() => onPortHover('Room1-TO-1')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs mt-1 text-black">TO-1</span>
</div>
<div className="flex flex-col items-center">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room1-TO-2')}`}
jstype="testport-copper"
id="Room1-TO-2"
onClick={() => onPortClick('Room1-TO-2')}
onMouseEnter={() => onPortHover('Room1-TO-2')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs mt-1 text-black">TO-2</span>
</div>
</div>
</div>
</div>
</div>
{/* Room 3 - 下半部分 */}
<div className="flex-1 flex flex-col">
<div className="text-center mb-1">Room3</div>
<div className="flex-1 bg-gray-900 flex items-center justify-center">
<div className="bg-gray-300 w-24 h-24 flex items-center justify-center">
<div className="flex space-x-2">
<div className="flex flex-col items-center">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room3-TO-1')}`}
jstype="testport-copper"
id="Room3-TO-1"
onClick={() => onPortClick('Room3A-TO-1')}
onMouseEnter={() => onPortHover('Room3-TO-1')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs mt-1 text-black">TO-1</span>
</div>
<div className="flex flex-col items-center">
<div
className={`rounded-full flex items-center justify-center transition-colors ${getPortStyle('Room3-TO-2')}`}
jstype="testport-copper"
id="Room3-TO-2"
onClick={() => onPortClick('Room3-TO-2')}
onMouseEnter={() => onPortHover('Room3-TO-2')}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs mt-1 text-black">TO-2</span>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Room 4 */}
<div className="w-[12%] p-1 flex flex-col">
{/* Room 1A - 上半部分 */}
<div className="flex-1 flex flex-col">
<div className="text-center mb-1">Room4</div>
<div className="relative flex-1 bg-gray-900 flex items-center justify-center">
<div
className={`absolute top-[55%] left-[66%] w-6 h-6 z-100`}
jstype="testport-cam"
id="Room4-CAM"
onClick={() => onPortClick('Room4-CAM')}
onMouseEnter={() => onPortHover('Room4-CAM')}
>
</div>
<div className={`w-full h-full bg-[url('/cam.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
</div>
</div>
{/* 2F配线架 */}
<div className="w-[24%] p-1 flex flex-col">
<div className="text-center mb-1">2F-RackA</div>
<div className="h-[calc(100%-1.5rem)] rounded-md p-2 flex flex-col space-y-2">
{/* Row 1A - taking 1/2 of rack height */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1A</span>
<div className="flex-1 flex items-center justify-center space-x-6">
{[...Array(3)].map((_, groupIndex) => (
<div key={groupIndex} className="flex">
{[0, 1].map((portIndex) => {
const portId = `2F-RackA-1A-${groupIndex * 2 + portIndex + 1}`;
const portNumber = groupIndex * 2 + portIndex + 1;
return (
<div key={portIndex} className="flex flex-col items-center">
<div
className={`mb-1 transition-colors flex items-center justify-center`}
>
<div
className={`w-8 h-8 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center cursor-pointer`}
jstype="testport-fiber"
lcclean="false"
id={portId}
onClick={() => onPortClick(portId)}
onMouseEnter={() => onPortHover(portId)}
></div>
</div>
<span className="text-xs">{portNumber}</span>
</div>
);
})}
</div>
))}
</div>
</div>
</div>
{/* Row 1B - taking 1/2 of rack height */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1B</span>
<div className="flex-1 flex items-center justify-center space-x-6">
{[...Array(3)].map((_, groupIndex) => (
<div key={groupIndex} className="flex">
{[0, 1].map((portIndex) => {
const portId = `2F-RackA-1B-${groupIndex * 2 + portIndex + 1}`;
const portNumber = groupIndex * 2 + portIndex + 1;
return (
<div key={portIndex} className="flex flex-col items-center">
<div
className={`mb-1 transition-colors flex items-center justify-center`}
>
<div
className={`w-8 h-8 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center cursor-pointer`}
jstype="testport-fiber"
lcclean="false"
id={portId}
onClick={() => onPortClick(portId)}
onMouseEnter={() => onPortHover(portId)}
></div>
</div>
<span className="text-xs">{portNumber}</span>
</div>
);
})}
</div>
))}
</div>
</div>
</div>
</div>
</div>
{/* 1F DataCenter */}
<div className="w-[50%] p-1 flex flex-col">
<div className="text-center mb-1">1F-RackA</div>
<div className="h-[calc(100%-1.5rem)] rounded-md p-2 flex flex-col space-y-2">
{/* Row 1A - taking 1/2 of rack height */}
<div className="flex flex-col h-1/3 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1A</span>
<div className="flex-1 flex items-center justify-center space-x-4">
{[...Array(12)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-10 h-10 transition-colors flex items-center justify-center ${getPortStyle(`1F-RackA-1A-${i + 1}`)}`}
jstype="testport-copper"
id={`1F-RackA-1A-${i + 1}`}
onClick={() => onPortClick(`1F-RackA-1A-${i + 1}`)}
onMouseEnter={() => onPortHover(`1F-RackA-1A-${i + 1}`)}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>
{/* Row 1B - taking 1/2 of rack height */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1B</span>
<div className="flex-1 flex items-center justify-center space-x-6">
{[...Array(6)].map((_, groupIndex) => (
<div key={groupIndex} className="flex">
{[0, 1].map((portIndex) => {
const portId = `1F-RackA-1B-${groupIndex * 2 + portIndex + 1}`;
const portNumber = groupIndex * 2 + portIndex + 1;
return (
<div key={portIndex} className="flex flex-col items-center">
<div
className={`mb-1 transition-colors flex items-center justify-center`}
>
<div
className={`w-8 h-8 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center cursor-pointer`}
jstype="testport-fiber"
lcclean="false"
id={portId}
onClick={() => onPortClick(portId)}
onMouseEnter={() => onPortHover(portId)}
></div>
</div>
<span className="text-xs">{portNumber}</span>
</div>
);
})}
</div>
))}
</div>
</div>
</div>
{/* Row 1C - taking 1/2 of rack height */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1C</span>
<div className="flex-1 flex items-center justify-center space-x-6">
{[...Array(6)].map((_, groupIndex) => (
<div key={groupIndex} className="flex">
{[0, 1].map((portIndex) => {
const portId = `1F-RackA-1C-${groupIndex * 2 + portIndex + 1}`;
const portNumber = groupIndex * 2 + portIndex + 1;
return (
<div key={portIndex} className="flex flex-col items-center">
<div
className={`mb-1 transition-colors flex items-center justify-center`}
>
<div
className={`w-8 h-8 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center cursor-pointer`}
jstype="testport-fiber"
lcclean="false"
id={portId}
onClick={() => onPortClick(portId)}
onMouseEnter={() => onPortHover(portId)}
></div>
</div>
<span className="text-xs">{portNumber}</span>
</div>
);
})}
</div>
))}
</div>
</div>
</div>
</div>
</div>
{/* 竞技模式场景切换 */}
<div className="w-[2%] p-1 flex flex-col">
<button
className="bg-gradient-to-r from-blue-500 to-blue-700 hover:from-blue-600 hover:to-blue-800 text-white font-bold py-3 px-2 rounded-lg shadow-lg transform hover:scale-101 transition-all duration-200 text-sm flex flex-col items-center justify-center h-full min-h-[120px] border border-blue-400 hover:border-blue-300"
onClick={() => updateWorldSkillScenarios('WORKSHOP')}
>
<div className="text-xs leading-tight text-center">
前往<br/>车间
</div>
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,194 @@
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import useDeviceStore from '@/store/deviceStore';
import { API_URLS } from '@/config/api';
export default function WorldSkillTask() {
const { faultScenarios } = useDeviceStore();
const [connections, setConnections] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchConnectionMap = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(API_URLS.CONNECTION.MAP_WITH_SCENE(faultScenarios));
if (!response.ok) {
throw new Error('获取连接配置失败');
}
const data = await response.json();
const formattedConnections = Object.entries(data || {})
.filter(([portId]) => (
!portId.includes('main-permanent') &&
!portId.includes('main-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
}));
setConnections(formattedConnections);
} catch (err) {
setError(err.message);
console.error('获取连接配置错误:', err);
} finally {
setLoading(false);
}
};
fetchConnectionMap();
}, [faultScenarios?.currentScene]);
return (
<div className="bg-gradient-to-br from-[#1E293B] to-[#0F172A] p-6 rounded-lg w-[full] h-[95vh] overflow-y-auto flex flex-col gap-6 shadow-2xl border border-[#334155] animate-fadeIn scrollbar-thin scrollbar-thumb-slate-600 scrollbar-track-slate-800">
{/* 任务标题 */}
<div className="border-b border-cyan-700 pb-3 mb-3">
<p className="text-sm font-semibold text-cyan-400 uppercase tracking-wider">赛题</p>
<h1 className="text-2xl md:text-3xl font-bold text-gray-100 mt-1">工业园区网络基础设施验收测试</h1>
<p className="text-xs text-gray-400">任务编号: Skill-2025</p>
<p className="text-xs text-gray-400">赛项信息网络布线</p>
<p className="text-xs text-gray-400">比赛时间60分钟</p>
</div>
{/* 导言 */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">导言</h2>
<p className="text-gray-300 leading-relaxed">
鹈鹕镇新建的现代化工业园区已竣工现需对园区信息网络基础设施进行验收测试你作为网络测试工程师你的核心任务是完成<span className="font-medium text-yellow-300">办公区域网络链路的标准化测试</span><span className="font-medium text-yellow-300">4</span>
</p>
</div>
{/* 网络拓扑与系统图 */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">网络拓扑与系统图</h2>
<div className="h-100 relative">
<Image src="/Competition.png" alt="competition" fill className="object-contain"/>
</div>
<h2 className="text-xl font-semibold text-cyan-300">光纤连接图</h2>
<div className="h-100 relative">
<Image src="/SkillFiberConnectMap.png" alt="SkillFiberConnectMap" fill className="object-contain"/>
</div>
<div className="bg-gray-800 p-4 rounded-lg">
<h3 className="text-lg font-semibold text-cyan-400 mb-3">数据机房1F</h3>
<ul className="list-disc list-inside text-gray-300 space-y-2 pl-4">
<li><strong className="text-yellow-300">配线架1A</strong>Cat6 F/UTP线Room1Room3Room4</li>
<li><strong className="text-yellow-300">配线架1B</strong>OS22F线1A</li>
<li><strong className="text-yellow-300">配线架1C</strong>OS2</li>
</ul>
</div>
<div className="bg-gray-800 p-4 rounded-lg">
<h3 className="text-lg font-semibold text-cyan-400 mb-3">车间区域扩展测试目标</h3>
<div className="text-gray-300">
<p className="mb-2"><strong className="text-yellow-300">工业设备链路</strong></p>
<ul className="list-disc list-inside space-y-1 pl-4">
<li>2台工业机器人R-M-A 工业PLC控制柜PLC-Rack-1ACat6 F/UTP</li>
<li>工业PLC控制柜PLC-Rack-1B 生产数据机柜Data-Rack-1ACat6 F/UTP</li>
</ul>
</div>
</div>
</div>
{/* 通用说明 */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">通用说明</h2>
<ul className="list-decimal list-inside text-gray-300 space-y-2 pl-4">
<li>为了更规范的管理测试文件请创建项目并命名为 <code className="bg-gray-700 px-1.5 py-0.5 rounded text-s text-yellow-300">Skill-2025</code></li>
<li>要求保存每条测试数据在仪表内并命名为"主机连接的端口名称-远端连接的端口名称""主机连接的端口名称"
<ul className="list-disc ml-6 mt-2">
<li className="mb-2">例如两头连接测试仪
<ul className="list-[circle] ml-6 mt-1 space-y-1">
<li><code className="bg-gray-700 px-1.5 py-0.5 rounded text-s text-yellow-300">1F-RackA-1A-1-Room1-TO-1</code><code className="bg-gray-700 px-1.5 py-0.5 rounded text-s text-yellow-300">Room1-TO-2-1F-RackA-1A-12</code></li>
<li><code className="bg-gray-700 px-1.5 py-0.5 rounded text-s text-yellow-300">1F-RackA-1A-3-Room6-CAM</code></li>
</ul>
</li>
<li>例如单端连接测试仪
<ul className="list-[circle] ml-6 mt-1">
<li><code className="bg-gray-700 px-1.5 py-0.5 rounded text-s text-yellow-300">1F-RackA-1C-8</code></li>
</ul>
</li>
</ul>
</li>
<li>
测试过程需要符合测试规范要求以保证测试数据有效
<ul className="list-disc list-inside space-y-1 ml-4 mt-1">
<li>需要进行必要的基准设置参照设置补偿设置</li>
<li>光纤被测链路测试跳线测试仪接口需要进行端面清洁</li>
<li>测试过程中光源不允许断开</li>
<li>线缆类型和极限值正确</li>
<li>插座类型连接器数量熔接点数量根据场景要求进行正确设置</li>
</ul>
</li>
</ul>
</div>
{/* 基本任务 */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">基本任务</h2>
<ul className="list-decimal list-inside text-gray-300 space-y-2 pl-4">
<li>使用铜缆认证分析仪对<span className="font-medium text-yellow-300">全部房间的5条铜缆链路</span>1F-RackA-1A
<ul className="list-disc ml-6 mt-2 space-y-2">
<li>Room1Room3链路到配线架为永久链路</li>
<li>Room4网线连接头连接到摄像头到配线架为模块化插头链路并采用PoE供电技术</li>
<li>配线架点位标记已模糊不准确需要结合寻线定位并测试</li>
</ul>
</li>
<li>使用光纤损耗分析仪对1F-RackA-1B的<span className="font-medium text-yellow-300">1~6</span>2F-RackA-1A<span className="font-medium text-yellow-300">1~6</span></li>
<li>使用光时域反射分析仪对1F-RackA-1C的<span className="font-medium text-yellow-300">1~4</span>Server-Rack-1B<span className="font-medium text-yellow-300">1~4</span><span className="font-medium text-yellow-300">Server-Rack1F-RackAOTDR</span></li>
<li>要求办公楼区域被测链路需要按照<code className="bg-gray-700 px-1.5 py-0.5 rounded text-s text-yellow-300">ISO/IEC 11801</code>使Fibre LinkISO/IEC22OTDR使Fibre LinkISO/IEC</li>
<li>如果链路使用了PoE供电技术需要使用带<code className="bg-gray-700 px-1.5 py-0.5 rounded text-s text-yellow-300">+PoE</code>ISO/IEC</li>
<li>要求被测链路需要符合<code className="bg-gray-700 px-1.5 py-0.5 rounded text-s text-yellow-300">T568B</code>线</li>
<li>识别目标系统中存在的链路故障描述并报告至测试文档</li>
<li>提交测试文档至指定处</li>
</ul>
</div>
{/* 扩展任务 */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">扩展任务</h2>
<ul className="list-decimal list-inside text-gray-300 space-y-2 pl-4">
<li>使用铜缆认证分析仪结合M12跳线对<span className="font-medium text-yellow-300">工业机械手臂至PLC控制柜</span>2<span className="font-medium text-yellow-300">M12(D)</span></li>
<li>使用铜缆认证分析仪结合RJ45跳线对<span className="font-medium text-yellow-300">PLC控制柜至Data-Rack</span>2<span className="font-medium text-yellow-300">RJ45(线)</span></li>
<li>要求被测链路需要通过<code className="bg-gray-700 px-1.5 py-0.5 rounded text-s text-yellow-300">Profinet</code></li>
<li>要求被测链路需要符合标准线序</li>
<li>识别目标系统中存在的链路故障描述并报告至测试文档</li>
<li>提交测试文档至指定处</li>
</ul>
</div>
{/* 所需设备 */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">所需设备机具装置和材料</h2>
<p className="text-gray-300">所需的设备机具装置和材料由比赛场地提供详见材料清单</p>
<p className="text-gray-300">本模块不使用基础设施列表中未列出的材料和设备</p>
</div>
{/* 安全要求 */}
<div className="space-y-3">
<h2 className="text-xl font-semibold text-cyan-300">其他要求</h2>
<div className="bg-yellow-900/20 border border-yellow-500 p-4 rounded-lg">
<ul className="list-disc list-inside text-gray-300 space-y-2">
<li>选手必须遵守其他文件规定</li>
<li><strong className="text-yellow-300">必须遵守安全操作规范比赛全程符合健康安全与环保规定</strong></li>
</ul>
</div>
</div>
{/* 竞赛提示 */}
<div className="mt-4 border-t border-cyan-700 pt-4">
<p className="mt-2 text-cyan-400 font-medium">祝你取得优异成绩</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,310 @@
import React,{useState} from 'react';
import useDeviceStore from '@/store/deviceStore';
import Image from 'next/image';
export default function WorldSkillWORKSHOP({
onPortClick = () => {},
onPortHover = () => {},
selectedPort = null,
targetPort = null,
connections = {}
}) {
// 获取端口的连接状态样式
const getPortStyle = (portId) => {
if (selectedPort === portId) {
return 'bg-blue-300';
}
if (targetPort === portId) {
return 'bg-green-300';
}
if (connections && connections[portId]) {
return 'bg-[#00ff7f]';
}
return 'bg-gray-300';
};
const {faultScenarios,seatUUID,seatNumber,updateWorldSkillScenarios} = useDeviceStore();
return (
<div>
{/* 办公区域 */}
<div className="h-full w-full bg-black text-white flex z-index-100">
{/* 车间Button */}
<div className="w-[2%] p-1 flex flex-col">
<button
className="bg-gradient-to-r from-blue-500 to-blue-700 hover:from-blue-600 hover:to-blue-800 text-white font-bold py-3 px-2 rounded-lg shadow-lg transform hover:scale-105 transition-all duration-200 text-sm flex flex-col items-center justify-center h-full min-h-[120px] border border-blue-400 hover:border-blue-300"
onClick={() => updateWorldSkillScenarios('OFFICE')}
>
<div className="text-xs leading-tight text-center">
前往<br/>办公楼
</div>
</button>
</div>
{/* R-M-A */}
<div className="w-[12%] p-1 flex flex-col">
<div className="flex-1 flex flex-col">
<div className="text-center mb-1">R-M-A-1</div>
<div className="relative flex-1 bg-gray-900 flex items-center justify-center">
<div
className={`absolute top-[55%] left-[66%] w-6 h-6 z-100`}
jstype="testport-arm"
id="RMA-1"
onClick={() => onPortClick('RMA-1')}
onMouseEnter={() => onPortHover('RMA-1')}
>
</div>
<div className={`w-full h-full bg-[url('/MechanicalArm.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
</div>
</div>
{/* R-M-A */}
<div className="w-[12%] p-1 flex flex-col">
{/* R-M-A */}
<div className="flex-1 flex flex-col">
<div className="text-center mb-1">R-M-A-2</div>
<div className="relative flex-1 bg-gray-900 flex items-center justify-center">
<div
className={`absolute top-[55%] left-[66%] w-6 h-6 z-100`}
jstype="testport-arm"
id="RMA-2"
onClick={() => onPortClick('RMA-2')}
onMouseEnter={() => onPortHover('RMA-2')}
>
</div>
<div className={`w-full h-full bg-[url('/MechanicalArm.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
</div>
</div>
{/* 2F配线架 */}
<div className="w-[30%] p-1 flex flex-col">
<div className="text-center mb-1">PLC-Rack</div>
<div className="h-[calc(100%-1.5rem)] rounded-md p-2 flex flex-col space-y-2">
{/* Row 1A - taking 1/2 of rack height */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1A</span>
<div className="flex-1 flex items-center justify-center space-x-8">
{[...Array(2)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-10 h-10 transition-colors flex items-center justify-center`}
jstype="testport-m12-d"
id={`PLC-Rack-1A-${i + 1}`}
onClick={() => onPortClick(`PLC-Rack-1A-${i + 1}`)}
onMouseEnter={() => onPortHover(`PLC-Rack-1A-${i + 1}`)}
>
<div className={`w-8 h-8 bg-[url('/m12-d.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>
{/* Row 1B - taking 1/2 of rack height */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1B</span>
<div className="flex-1 flex items-center justify-center space-x-8">
{[...Array(2)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-10 h-10 transition-colors flex items-center justify-center ${getPortStyle(`PLC-Rack-1B-${i + 1}`)}`}
jstype="testport-copper"
id={`PLC-Rack-1B-${i + 1}`}
onClick={() => onPortClick(`PLC-Rack-1B-${i + 1}`)}
onMouseEnter={() => onPortHover(`PLC-Rack-1B-${i + 1}`)}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
{/* Data-Rack */}
<div className="w-[48%] p-1 flex flex-col">
<div className="text-center mb-1">Data-Rack</div>
<div className="h-[calc(100%-1.5rem)] rounded-md p-2 flex flex-col space-y-2">
{/* Row 1A - taking 1/2 of rack height */}
<div className="flex flex-col h-1/2 bg-gray-800 rounded-md p-2 relative">
<div className="absolute top-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute top-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 left-0 bg-white w-3 h-3 rounded-full"></div>
<div className="absolute bottom-0 right-0 bg-white w-3 h-3 rounded-full"></div>
<div className="flex-1 flex items-center">
<span className="text-xs mr-4">1A</span>
<div className="flex-1 flex items-center justify-center space-x-4">
{[...Array(8)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className={`w-10 h-10 transition-colors flex items-center justify-center ${getPortStyle(`1F-RackA-1A-${i + 1}`)}`}
jstype="testport-copper"
id={`1F-RackA-1A-${i + 1}`}
onClick={() => onPortClick(`1F-RackA-1A-${i + 1}`)}
onMouseEnter={() => onPortHover(`1F-RackA-1A-${i + 1}`)}
>
<div className={`w-8 h-8 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center`}></div>
</div>
<span className="text-xs">{i + 1}</span>
</div>
))}
</div>
</div>
</div>
{/* Network Switch */}
<div className="flex flex-col h-1/2 bg-gray-900 rounded-md p-2 relative border border-gray-700">
{/* Switch Top Area - Brand and Status LEDs */}
<div className="h-8 flex items-center justify-between px-2 border-b border-gray-700">
<div className="text-blue-500 font-bold text-sm">SWITCH</div>
<div className="flex space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-amber-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
</div>
</div>
{/* Switch Port Area */}
<div className="flex-1 flex flex-row">
{/* Left Side - RJ45 Ports (80%) */}
<div className="w-4/6 flex flex-col">
{/* Port Labels */}
<div className="flex justify-between px-4 text-xs text-gray-400">
<span>10/100/1000 Base-T</span>
<span>1-20</span>
</div>
{/* Top Row Ports (1-10) - RJ45 */}
<div className="flex-1 flex items-center justify-center">
<div className="grid grid-cols-10 gap-0.5 w-full">
{[...Array(10)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className="w-5 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
jstype=""
id={`SW-TOP-${i + 1}`}
>
<div className="w-4 h-4 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center"></div>
</div>
</div>
))}
</div>
</div>
{/* Bottom Row Ports (11-20) - RJ45 */}
<div className="flex-1 flex items-center justify-center">
<div className="grid grid-cols-10 gap-0.5 w-full">
{[...Array(10)].map((_, i) => (
<div key={i} className="flex flex-col items-center">
<div
className="w-5 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
jstype=""
id={`SW-BOT-${i + 1}`}
>
<div className="w-4 h-4 bg-[url('/rj45.png')] bg-contain bg-no-repeat bg-center"></div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Right Side - Fiber Ports (20%) */}
<div className="w-2/6 flex flex-col">
{/* Fiber Port Labels */}
<div className="flex justify-between px-1 text-xs text-gray-400">
<span>SFP+</span>
<span>21-24</span>
</div>
{/* Top Row Fiber Ports (21-22) */}
<div className="flex-1 flex items-center justify-center">
<div className="flex space-x-4"> {/* 使用 space-x-4 添加水平间距 */}
{[...Array(2)].map((_, i) => (
<div key={i} className="flex justify-center">
<div
className="w-10 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
>
<div className="flex">
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"></div>
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"></div>
</div>
</div>
</div>
))}
</div>
</div>
{/* Bottom Row Fiber Ports (23-24) */}
<div className="flex-1 flex items-center justify-center">
<div className="flex space-x-4"> {/* 使用 space-x-4 添加水平间距 */}
{[...Array(2)].map((_, i) => (
<div key={i} className="flex justify-center">
<div
className="w-10 h-5 border border-gray-600 rounded-sm flex items-center justify-center transition-colors"
>
<div className="flex">
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"
></div>
<div className="w-4 h-4 bg-[url('/lc.png')] bg-contain bg-no-repeat bg-center"
></div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Switch Bottom Area - Console and Power */}
<div className="h-6 flex items-center justify-between px-2 border-t border-gray-700">
<div className="flex items-center space-x-2">
<div className="text-xs text-gray-400">Console</div>
<div className="w-4 h-2 bg-gray-600 rounded-sm"></div>
</div>
<div className="flex items-center space-x-2">
<div className="text-xs text-gray-400">Power</div>
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

24
src/config/api.js Normal file
View File

@@ -0,0 +1,24 @@
// API 基础地址
// const BASE_URL = 'https://wxapi.est-live.cn';
// const BASE_URL = 'http://localhost:3007';
const BASE_URL = 'http://192.168.42.188:3007';
// const BASE_URL = '';
// API 路径配置
const API_URLS = {
// 竞赛相关
COMPETITION: {
DATA: `${BASE_URL}/api/competition/data`,
STATUS: `${BASE_URL}/api/competition/status`,
START: `${BASE_URL}/api/competition/start`,
END: `${BASE_URL}/api/competition/end`,
},
// 连接图相关
CONNECTION: {
MAP: `${BASE_URL}/api/connectionMap`,
MAP_WITH_SCENE: (scene) => `${BASE_URL}/api/connectionMap?scene=${scene}`,
INIT: `${BASE_URL}/api/initConnectionMap`,
},
};
export { BASE_URL, API_URLS };

14
src/pages/_app.js Normal file
View File

@@ -0,0 +1,14 @@
import "@/styles/globals.css";
import Head from 'next/head'
export default function App({ Component, pageProps }) {
return (
<>
<Head>
<title>信息网络布线仿真测试平台</title>
<meta name="description" content="信息网络布线仿真测试平台" />
</Head>
<Component {...pageProps} />
</>
)
}

13
src/pages/_document.js Normal file
View File

@@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body className="antialiased">
<Main />
<NextScript />
</body>
</Html>
);
}

719
src/pages/admin.js Normal file
View File

@@ -0,0 +1,719 @@
import React, { useState, useEffect } from 'react';
import Office from '@/components/scene/Office';
import Industry from '@/components/scene/Industry';
import WorldSkill from '@/components/scene/WorldSkill';
import CompetitionPage from './competition';
import { API_URLS } from '@/config/api';
export default function AdminPage() {
const [connections, setConnections] = useState({});
const [selectedPort, setSelectedPort] = useState(null);
const [targetPort, setTargetPort] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [currentScene, setCurrentScene] = useState('WorldSkill');
const [showModal, setShowModal] = useState(false);
const [showhistoryModal, setShowhistoryModal] = useState(false);
const [uuid, setUuid] = useState(null);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [confirmDialogMessage, setConfirmDialogMessage] = useState('');
const [confirmDialogCallback, setConfirmDialogCallback] = useState(null);
// 检查比赛状态
const checkCompetitionStatus = async () => {
try {
const response = await fetch(API_URLS.COMPETITION.STATUS);
if (!response.ok) throw new Error('获取比赛状态失败');
const data = await response.json();
if (data.isRunning) {
setShowModal(true);
setUuid(data.UUID);
}
} catch (err) {
console.error('获取比赛状态错误:', err);
}
};
// 初始化时检查比赛状态
useEffect(() => {
checkCompetitionStatus();
}, []);
// 从API获取连接配置
const fetchConnectionMap = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(API_URLS.CONNECTION.MAP_WITH_SCENE(currentScene));
if (!response.ok) {
throw new Error('获取连接配置失败');
}
const data = await response.json();
setConnections(data);
} catch (err) {
setError(err.message);
console.error('获取连接配置错误:', err);
} finally {
setLoading(false);
}
};
// 初始化时获取连接配置
useEffect(() => {
fetchConnectionMap();
}, [currentScene]);
// 检查端口是否允许配置
const isPortAllowed = (portId) => {
return !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');
};
// 处理端口点击
const handlePortClick = (portId) => {
if (!isPortAllowed(portId)) {
alert('该端口不允许配置');
return;
}
// 检查端口是否已经在连接配置中
const isPortConfigured = Object.entries(connections || {}).some(
([source, connection]) => source === portId || connection.connectedTo === portId
);
if (isPortConfigured) {
alert('该端口已经配置连接');
return;
}
if (!selectedPort) {
setSelectedPort(portId);
} else {
// 获取两个端口的jstype属性
const selectedPortElement = document.querySelector(`[id="${selectedPort}"]`);
const targetPortElement = document.querySelector(`[id="${portId}"]`);
if (!selectedPortElement || !targetPortElement) {
alert('无法获取端口类型信息');
return;
}
const selectedPortType = selectedPortElement.getAttribute('jstype');
const targetPortType = targetPortElement.getAttribute('jstype');
// 检查两个端口是否类型匹配
if (selectedPortType !== targetPortType) {
alert('只能连接相同类型的端口');
setSelectedPort(null);
setTargetPort(null);
return;
}
// 根据端口类型创建不同的连接配置
const newConnections = {
...connections,
[selectedPort]: selectedPortType === 'testport-fiber' ? {
connectedTo: portId,
type: 'fiber',
apitype:"olts",
fiberstatus: 'sm-pass'
} : {
connectedTo: portId,
type: 'copper',
apitype:"t568b",
wiremapstatus: 'pass',
performancestatus: 'pass'
}
};
setConnections(newConnections);
setSelectedPort(null);
setTargetPort(null);
}
};
// 处理端口悬停
const handlePortHover = (portId) => {
if (selectedPort) {
setTargetPort(portId);
}
};
// 保存连接配置到API
const handleSave = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(API_URLS.CONNECTION.MAP, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
[currentScene]: connections
})
});
if (!response.ok) {
throw new Error('保存连接配置失败');
}
alert('连接配置已保存!');
// 重新获取最新配置
fetchConnectionMap();
} catch (err) {
setError(err.message);
console.error('保存连接配置错误:', err);
alert('保存失败:' + err.message);
} finally {
setLoading(false);
}
};
// 导出配置为JSON文件
const handleExport = () => {
const dataStr = JSON.stringify(connections, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `connections_${currentScene}_${new Date().toISOString().slice(0, 10)}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
// 初始化连接配置
const handleInit = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(API_URLS.CONNECTION.INIT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (!response.ok) {
throw new Error('初始化连接配置失败');
}
alert('连接配置已恢复默认状态!');
// 重新获取最新配置
fetchConnectionMap();
} catch (err) {
setError(err.message);
console.error('初始化连接配置错误:', err);
alert('初始化失败:' + err.message);
} finally {
setLoading(false);
}
};
const [showKeyModal, setShowKeyModal] = useState(true);
const [key, setKey] = useState('');
const handleKeySubmit = () => {
if (key === 'admin1234') {
setShowKeyModal(false);
} else {
setConfirmDialogMessage('密钥错误!');
setConfirmDialogCallback(() => () => {
setShowConfirmDialog(false);
});
setShowConfirmDialog(true);
}
};
return (
<div className="w-full min-h-screen bg-[#0F172A] p-6">
{showKeyModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-9999">
<div className="bg-[#0F172A] p-8 rounded-lg border border-[#0ff]/20 w-[400px]">
<h2 className="text-[#0ff] text-xl mb-4">管理员页-请输入访问密码</h2>
<input
type="password"
value={key}
onChange={(e) => setKey(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleKeySubmit()}
className="w-full px-4 py-2 bg-[#1E293B] text-white rounded-lg mb-4 focus:outline-none focus:ring-2 focus:ring-[#0ff]/50 "
placeholder="请输入密钥..."
autoFocus
/>
<button
onClick={handleKeySubmit}
className="w-full bg-[#0ff]/20 text-[#0ff] py-2 rounded-lg hover:bg-[#0ff]/30 transition-colors"
>
确认
</button>
</div>
</div>
)}
{/* 确认对话框 */}
{showConfirmDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[10001]">
<div className="bg-[#0F172A] p-8 rounded-lg border border-[#0ff]/20 w-[400px]">
<h3 className="text-[#0ff] text-xl mb-4">{confirmDialogMessage}</h3>
<div className="flex space-x-4">
<button
onClick={() => {
if (confirmDialogCallback) confirmDialogCallback();
else setShowConfirmDialog(false);
}}
className="w-full bg-[#0ff]/20 text-[#0ff] py-2 rounded-lg hover:bg-[#0ff]/30 transition-colors"
>
确认
</button>
</div>
</div>
</div>
)}
{/* 全屏模态框 */}
{showModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="w-full h-full">
<div className='absolute top-1 right-6 z-50' >
<span className='text-white opacity-25'>{uuid}</span>
</div>
<div className='absolute top-6 right-6 z-50' >
<button
onClick={async () => {
// 添加二次确认
if (!window.confirm('确定要结束比赛吗?将导出比赛数据并关闭当前比赛。')) {
return;
}
try {
const response = await fetch(API_URLS.COMPETITION.END, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) throw new Error('结束比赛失败');
const result = await response.json();
// 创建并下载编码后的文件
const dataStr = JSON.stringify(result.data, null, 2);
// 使用编码和简单的字符替换来编码数据
const encodedData = encodeURIComponent(dataStr).split('').reverse().join('');
// 添加自定义头部标识,表明这是编码后的文件
const finalData = `EST_ENCODED_DATA:${encodedData}`;
const dataBlob = new Blob([finalData], { type: 'application/octet-stream' });
// console.log(dataBlob);
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `${result.uuid}_${new Date().toISOString().slice(0, 10)}.est`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
alert('比赛已结束!');
setShowModal(false);
} catch (err) {
console.error('结束比赛错误:', err);
alert('结束比赛失败:' + err.message);
}
}}
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
>
结束比赛
</button>
</div>
<CompetitionPage isComponent={true}/>
</div>
</div>
)}
{/* 历史结果模态框 */}
{showhistoryModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="w-full h-full">
<div className='absolute top-12 right-50 z-50' >
<button
onClick={async () => {
setShowhistoryModal(false)
}}
className="cursor-pointer bg-[#0ff]/20 hover:bg-[#0ff]/30 text-[#0ff] px-4 py-2 rounded transition-colors"
>
返回控制页面
</button>
</div>
<CompetitionPage isComponent={false}/>
</div>
</div>
)}
<div className="max-w-8xl mx-auto">
<div className="flex justify-between items-center mb-6">
<div className="flex items-center gap-4">
<h1 className="text-2xl font-bold text-[#00ff7f]">连接管理</h1>
<select
value={currentScene}
onChange={(e) => setCurrentScene(e.target.value)}
className="px-4 py-2 bg-[#1F2937] text-[#0ff] rounded-lg border border-[#0ff]/20"
>
<option value="Office">办公场景</option>
<option value="Industry">工业场景</option>
<option value="WorldSkill">竞赛模式</option>
</select>
</div>
<div className="flex gap-4">
{currentScene === 'WorldSkill' && (
<>
<button
onClick={async () => {
try {
const response = await fetch(API_URLS.COMPETITION.START, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) throw new Error('开始比赛失败');
alert('比赛已开始!');
// setShowModal(true); // 显示模态框
window.location.reload();
} catch (err) {
console.error('开始比赛错误:', err);
alert('开始比赛失败:' + err.message);
}
}}
className="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"
>
开始比赛
</button>
<button
onClick={async () => {
setShowhistoryModal(true)
}}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-600 transition-colors"
>
查看历史比赛数据
</button>
</>
)}
<button
onClick={handleInit}
disabled={loading}
className="px-4 py-2 bg-[#ff7f00] text-[#0F172A] rounded-lg hover:bg-[#ff7f00]/90 transition-colors disabled:opacity-50"
>
{loading ? '初始化中...' : '初始化配置'}
</button>
<button
onClick={fetchConnectionMap}
disabled={loading}
className="px-4 py-2 bg-[#0ff] text-[#0F172A] rounded-lg hover:bg-[#0ff]/90 transition-colors disabled:opacity-50"
>
{loading ? '加载中...' : '刷新配置'}
</button>
<button
onClick={handleSave}
disabled={loading}
className="px-4 py-2 bg-[#00ff7f] text-[#0F172A] rounded-lg hover:bg-[#00ff7f]/90 transition-colors disabled:opacity-50"
>
{loading ? '保存中...' : '保存配置'}
</button>
<button
onClick={handleExport}
disabled={loading}
className="px-4 py-2 bg-[#7f00ff] text-white rounded-lg hover:bg-[#7f00ff]/90 transition-colors disabled:opacity-50"
>
导出配置
</button>
<label
className="px-4 py-2 bg-[#7f00ff] text-white rounded-lg hover:bg-[#7f00ff]/90 transition-colors disabled:opacity-50 cursor-pointer"
>
导入配置
<input
type="file"
accept=".json"
className="hidden"
onChange={async (event) => {
const file = event.target.files[0];
if (!file || !file.name.endsWith('.json')) {
alert('请选择.json文件');
return;
}
try {
const text = await file.text();
let jsonData;
// 尝试直接解析为JSON
jsonData = JSON.parse(text);
setConnections(jsonData);
alert('配置已导入!');
} catch (error) {
console.error('读取文件失败:', error);
alert('读取文件失败');
}
}}
/>
</label>
</div>
</div>
{error && (
<div className="mb-4 p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-500">
{error}
</div>
)}
{/* 故障箱区域 */}
<div className="h-[30vh] bg-[#0F172A] shadow-lg p-2 mb-6 border border-[#0ff]/20 rounded-lg">
{currentScene === 'Office' ? (
<Office
onPortClick={handlePortClick}
onPortHover={handlePortHover}
selectedPort={selectedPort}
targetPort={targetPort}
connections={connections}
/>
) : currentScene === 'Industry' ? (
<Industry
onPortClick={handlePortClick}
onPortHover={handlePortHover}
selectedPort={selectedPort}
targetPort={targetPort}
connections={connections}
/>
) : (
<WorldSkill
onPortClick={handlePortClick}
onPortHover={handlePortHover}
selectedPort={selectedPort}
targetPort={targetPort}
connections={connections}
/>
)}
</div>
{/* 连接列表 */}
<div className="bg-[#0F172A] shadow-lg p-4 border border-[#0ff]/20 rounded-lg mb-6">
<h2 className="text-xl font-semibold text-[#00ff7f] mb-4">当前连接配置</h2>
<div className="grid grid-cols-2 gap-4">
{Object.entries(connections || {}).filter(([source]) => isPortAllowed(source)).map(([source, connection]) => (
<div
key={source}
className="flex items-center gap-4 p-3 bg-[#0ff]/5 rounded-lg hover:outline hover:outline-2 hover:outline-[#0ff] transition-all duration-200"
onMouseEnter={() => {
const sourceElement = document.getElementById(source);
const targetElement = document.getElementById(connection.connectedTo);
if (sourceElement) sourceElement.classList.add('highlight-element');
if (targetElement) targetElement.classList.add('highlight-element');
}}
onMouseLeave={() => {
const sourceElement = document.getElementById(source);
const targetElement = document.getElementById(connection.connectedTo);
if (sourceElement) sourceElement.classList.remove('highlight-element');
if (targetElement) targetElement.classList.remove('highlight-element');
}}
>
<div className="w-[15%] text-[#0ff]">{source}</div>
<div className="w-[5%] text-[#0ff]"></div>
<div className="w-[15%] text-[#0ff]">{connection.connectedTo}</div>
<div className="w-[10%]">
<span className={`text-xs ${connection.type === 'fiber' ? 'text-yellow-400' : 'text-blue-400'}`}>
[{connection.type === 'fiber' ? '光纤' : '铜缆'}]
</span>
</div>
<div className="flex-1 flex items-center gap-4">
{connection.type === 'copper' ? (
<>
{connection.apitype === 't568b' ? (
<>
<select
value={connection.wiremapstatus}
onChange={(e) => {
const newConnections = { ...connections };
newConnections[source] = {
...newConnections[source],
wiremapstatus: e.target.value
};
setConnections(newConnections);
}}
className="w-[25%] bg-[#0F172A] text-sm border border-[#0ff]/20 rounded px-2 py-1 text-[#0ff]"
>
<option value="pass" className="text-green-500">通过</option>
<option value="open">开路</option>
<option value="open2">开路2</option>
<option value="open3">开路3</option>
<option value="short">短路</option>
<option value="short2">短路2</option>
<option value="short3">短路3</option>
<option value="cross">跨接</option>
<option value="cross2">跨接2</option>
<option value="cross3">跨接3</option>
<option value="reversed">交叉</option>
<option value="reversed2">交叉2</option>
<option value="reversed3">交叉3</option>
<option value="miswire">乱序</option>
</select>
<select
value={connection.performancestatus}
onChange={(e) => {
const newConnections = { ...connections };
newConnections[source] = {
...newConnections[source],
performancestatus: e.target.value
};
setConnections(newConnections);
}}
className="w-[40%] bg-[#0F172A] text-sm border border-[#0ff]/20 rounded px-2 py-1 text-[#0ff]"
>
<option value="pass" className="text-green-500">通过</option>
<option value="pass-4m" className="text-green-500">通过(4)</option>
<option value="pass-30m" className="text-green-500">通过(30)</option>
<option value="return-loss-fail">回波损耗失败</option>
<option value="next-fail">近端串扰失败</option>
<option value="lengthfail">超长线</option>
<option value="ohmfail">电阻不合格</option>
<option value="mptl-nextfail">MPTL UBL失败</option>
</select>
</>
) : connection.apitype === 'workshop-m12' ? (
<>
<select
value={connection.wiremapstatus}
onChange={(e) => {
const newConnections = { ...connections };
newConnections[source] = {
...newConnections[source],
wiremapstatus: e.target.value
};
setConnections(newConnections);
}}
className="w-[25%] bg-[#0F172A] text-sm border border-[#0ff]/20 rounded px-2 py-1 text-[#0ff]"
>
<option value="pass-2pair" className="text-green-500">通过</option>
<option value="sopen" >屏蔽层开路</option>
</select>
<select
value={connection.performancestatus}
onChange={(e) => {
const newConnections = { ...connections };
newConnections[source] = {
...newConnections[source],
performancestatus: e.target.value
};
setConnections(newConnections);
}}
className="w-[40%] bg-[#0F172A] text-sm border border-[#0ff]/20 rounded px-2 py-1 text-[#0ff]"
>
<option value="workshop-m12-pass-30m" className="text-green-500">通过(30)</option>
</select>
</>
) : connection.apitype === 'workshop-2p' ? (
<>
<select
value={connection.wiremapstatus}
onChange={(e) => {
const newConnections = { ...connections };
newConnections[source] = {
...newConnections[source],
wiremapstatus: e.target.value
};
setConnections(newConnections);
}}
className="w-[25%] bg-[#0F172A] text-sm border border-[#0ff]/20 rounded px-2 py-1 text-[#0ff]"
>
<option value="pass-2pair" className="text-green-500">通过</option>
<option value="sopen" >屏蔽层开路</option>
</select>
<select
value={connection.performancestatus}
onChange={(e) => {
const newConnections = { ...connections };
newConnections[source] = {
...newConnections[source],
performancestatus: e.target.value
};
setConnections(newConnections);
}}
className="w-[40%] bg-[#0F172A] text-sm border border-[#0ff]/20 rounded px-2 py-1 text-[#0ff]"
>
<option value="workshop-2p-pass-80m" className="text-green-500">通过(80)</option>
<option value="workshop-2p-lengthfail">工业超长线</option>
</select>
</>
) : null}
</>
) : connection.type === 'fiber' ? (
<>
{connection.apitype === 'olts' ? (
<select
value={connection.fiberstatus}
onChange={(e) => {
const newConnections = { ...connections };
newConnections[source] = {
...newConnections[source],
fiberstatus: e.target.value
};
setConnections(newConnections);
}}
className="w-[70%] bg-[#0F172A] text-sm border border-[#0ff]/20 rounded px-2 py-1 text-[#0ff]"
>
<option value="sm-pass" className="text-green-500">单模通过</option>
<option value="sm-fail" className="text-red-500">单模失败</option>
</select>
) : connection.apitype === 'otdr' ? (
<select
value={connection.fiberstatus}
onChange={(e) => {
const newConnections = { ...connections };
newConnections[source] = {
...newConnections[source],
fiberstatus: e.target.value
};
setConnections(newConnections);
}}
className="w-[70%] bg-[#0F172A] text-sm border border-[#0ff]/20 rounded px-2 py-1 text-[#0ff]"
>
<option value="sm-pass" className="text-green-500">单模通过</option>
<option value="connector-fail-start">连接器故障(头部)</option>
<option value="connector-fail-end">连接器故障(尾部)</option>
<option value="splice-fail">熔接点故障</option>
<option value="bend">光纤弯曲</option>
</select>
) : null}
</>
) : null}
<button
onClick={() => {
const newConnections = { ...connections };
delete newConnections[source];
setConnections(newConnections);
}}
className="text-red-500 hover:text-red-400 px-2 py-1"
>
删除
</button>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}

1496
src/pages/competition.js Normal file

File diff suppressed because it is too large Load Diff

1367
src/pages/index.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><text x="180" y="305" text-anchor="start"></text><text x="630" y="305" text-anchor="end"></text><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><text x="180" y="355" text-anchor="start"></text><text x="630" y="355" text-anchor="end"></text><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><text x="180" y="435" text-anchor="start"></text><text x="630" y="435" text-anchor="end"></text><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><text x="180" y="485" text-anchor="start"></text><text x="630" y="485" text-anchor="end"></text><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><text x="180" y="305" text-anchor="start"></text><text x="630" y="305" text-anchor="end"></text><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><text x="180" y="355" text-anchor="start"></text><text x="630" y="355" text-anchor="end"></text><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><text x="180" y="435" text-anchor="start"></text><text x="630" y="435" text-anchor="end"></text><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><text x="180" y="485" text-anchor="start"></text><text x="630" y="485" text-anchor="end"></text><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><text x="180" y="555" text-anchor="start">0.01m</text><text x="630" y="555" text-anchor="end">34.6m</text><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><text x="180" y="305" text-anchor="start"></text><text x="630" y="305" text-anchor="end"></text><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><text x="180" y="355" text-anchor="start"></text><text x="630" y="355" text-anchor="end"></text><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><text x="180" y="435" text-anchor="start"></text><text x="630" y="435" text-anchor="end"></text><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><text x="180" y="485" text-anchor="start"></text><text x="630" y="485" text-anchor="end"></text><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><text x="180" y="305" text-anchor="start"></text><text x="630" y="305" text-anchor="end"></text><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><text x="180" y="355" text-anchor="start"></text><text x="630" y="355" text-anchor="end"></text><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><text x="180" y="435" text-anchor="start"></text><text x="630" y="435" text-anchor="end"></text><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><text x="180" y="485" text-anchor="start"></text><text x="630" y="485" text-anchor="end"></text><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><text x="180" y="305" text-anchor="start"></text><text x="630" y="305" text-anchor="end"></text><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><text x="180" y="355" text-anchor="start"></text><text x="630" y="355" text-anchor="end"></text><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><text x="180" y="435" text-anchor="start"></text><text x="630" y="435" text-anchor="end"></text><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><text x="180" y="485" text-anchor="start"></text><text x="630" y="485" text-anchor="end"></text><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

118
src/store/COPPER/HDTD.py Normal file
View File

@@ -0,0 +1,118 @@
import csv
import json
def extract_hdtx_data(csv_file_path):
with open(csv_file_path, mode='r', encoding='utf-8') as csv_file:
csv_reader = list(csv.reader(csv_file))
data = {
"HDTDR": {"LENGTH": []},
"HDTDX": {"LENGTH": []}
}
# 用于存储找到的起始行索引
hdtdr_start = -1
hdtdx_start = -1
# 第一步:扫描整个文件,定位关键行
for i, row in enumerate(csv_reader):
row_str = ",".join(row).strip()
# 查找HDTDR起始行
if "HDTDR (%)" in row_str and hdtdr_start == -1:
hdtdr_start = i
# 查找HDTDX起始行
if "HDTDX Analyzer" in row_str and hdtdx_start == -1:
hdtdx_start = i
# 第二步处理HDTDR数据
if hdtdr_start != -1:
# 跳过两行作为表头
header_row = hdtdr_start + 2
if header_row < len(csv_reader):
headers = [h.strip() for h in csv_reader[header_row]]
# 确定各列位置
length_col = headers.index("Length (m)") if "Length (m)" in headers else -1
pair_cols = {
"PAIR12": headers.index("1,2") if "1,2" in headers else -1,
"PAIR36": headers.index("3,6") if "3,6" in headers else -1,
"PAIR45": headers.index("4,5") if "4,5" in headers else -1,
"PAIR78": headers.index("7,8") if "7,8" in headers else -1
}
# 读取数据行
for row in csv_reader[header_row+1:]:
if row and row[0].replace('.', '', 1).isdigit():
# 读取长度
if length_col != -1 and length_col < len(row):
try:
length = float(row[length_col])
data["HDTDR"]["LENGTH"].append(length)
except (ValueError, IndexError):
pass
# 读取各线对数据
for pair_name, col_idx in pair_cols.items():
if col_idx != -1 and col_idx < len(row):
if pair_name not in data["HDTDR"]:
data["HDTDR"][pair_name] = []
try:
value = float(row[col_idx])
data["HDTDR"][pair_name].append(value)
except (ValueError, IndexError):
data["HDTDR"][pair_name].append(None)
# 第三步处理HDTDX数据
if hdtdx_start != -1:
# 跳过两行作为表头
header_row = hdtdx_start + 2
if header_row < len(csv_reader):
headers = [h.strip() for h in csv_reader[header_row]]
# 确定各列位置
length_col = headers.index("Length (m)") if "Length (m)" in headers else -1
pair_cols = {
"PAIR1236": headers.index("1,2-3,6") if "1,2-3,6" in headers else -1,
"PAIR1245": headers.index("1,2-4,5") if "1,2-4,5" in headers else -1,
"PAIR1278": headers.index("1,2-7,8") if "1,2-7,8" in headers else -1,
"PAIR3645": headers.index("3,6-4,5") if "3,6-4,5" in headers else -1,
"PAIR3678": headers.index("3,6-7,8") if "3,6-7,8" in headers else -1,
"PAIR4578": headers.index("4,5-7,8") if "4,5-7,8" in headers else -1
}
# 读取数据行
for row in csv_reader[header_row+1:]:
if row and row[0].replace('.', '', 1).isdigit():
# 读取长度
if length_col != -1 and length_col < len(row):
try:
length = float(row[length_col])
data["HDTDX"]["LENGTH"].append(length)
except (ValueError, IndexError):
pass
# 读取各线对数据
for pair_name, col_idx in pair_cols.items():
if col_idx != -1 and col_idx < len(row):
if pair_name not in data["HDTDX"]:
data["HDTDX"][pair_name] = []
try:
value = float(row[col_idx])
data["HDTDX"][pair_name].append(value)
except (ValueError, IndexError):
data["HDTDX"][pair_name].append(None)
return data
# 调用函数
csv_file_path = r'C:\Users\18930\Desktop\Work\网络仿真实训平台\资源平台\docusaurus\est-dsx\src\store\COPPER\data.csv'
json_data = extract_hdtx_data(csv_file_path)
# 保存 JSON
output_file_path = r'C:\Users\18930\Desktop\Work\网络仿真实训平台\资源平台\docusaurus\est-dsx\src\store\COPPER\output.json'
with open(output_file_path, mode='w', encoding='utf-8') as json_file:
json.dump(json_data, json_file, indent=4)
print(f"JSON 数据已保存到 {output_file_path}")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">3</text><text x="730" y="105" class="label">3</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">2</text><text x="730" y="175" class="label">2</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">4</text><text x="730" y="225" class="label">4</text><text x="180" y="305" text-anchor="start"/><text x="630" y="305" text-anchor="end"/><text x="180" y="355" text-anchor="start"/><text x="630" y="355" text-anchor="end"/><text x="180" y="435" text-anchor="start"/><text x="630" y="435" text-anchor="end"/><text x="180" y="485" text-anchor="start"/><text x="630" y="485" text-anchor="end"/><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">3</text><text x="730" y="105" class="label">3</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">2</text><text x="730" y="175" class="label">2</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">4</text><text x="730" y="225" class="label">4</text><text x="180" y="305" text-anchor="start"/><text x="630" y="305" text-anchor="end"/><text x="180" y="355" text-anchor="start"/><text x="630" y="355" text-anchor="end"/><text x="180" y="435" text-anchor="start"/><text x="630" y="435" text-anchor="end"/><text x="180" y="485" text-anchor="start"/><text x="630" y="485" text-anchor="end"/><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">3</text><text x="730" y="105" class="label">3</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">2</text><text x="730" y="175" class="label">2</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">4</text><text x="730" y="225" class="label">4</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><text x="180" y="555" text-anchor="start">0.01m</text><text x="630" y="555" text-anchor="end">34.6m</text><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">3</text><text x="730" y="105" class="label">3</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">2</text><text x="730" y="175" class="label">2</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">4</text><text x="730" y="225" class="label">4</text><text x="180" y="305" text-anchor="start"/><text x="630" y="305" text-anchor="end"/><text x="180" y="355" text-anchor="start"/><text x="630" y="355" text-anchor="end"/><text x="180" y="435" text-anchor="start"/><text x="630" y="435" text-anchor="end"/><text x="180" y="485" text-anchor="start"/><text x="630" y="485" text-anchor="end"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">3</text><text x="730" y="105" class="label">3</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">2</text><text x="730" y="175" class="label">2</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">4</text><text x="730" y="225" class="label">4</text><text x="180" y="305" text-anchor="start"/><text x="630" y="305" text-anchor="end"/><text x="180" y="355" text-anchor="start"/><text x="630" y="355" text-anchor="end"/><text x="180" y="435" text-anchor="start"/><text x="630" y="435" text-anchor="end"/><text x="180" y="485" text-anchor="start"/><text x="630" y="485" text-anchor="end"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">3</text><text x="730" y="105" class="label">3</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">2</text><text x="730" y="175" class="label">2</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">4</text><text x="730" y="225" class="label">4</text><text x="180" y="305" text-anchor="start"/><text x="630" y="305" text-anchor="end"/><text x="180" y="355" text-anchor="start"/><text x="630" y="355" text-anchor="end"/><text x="180" y="435" text-anchor="start"/><text x="630" y="435" text-anchor="end"/><text x="180" y="485" text-anchor="start"/><text x="630" y="485" text-anchor="end"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><path d="M 150,300 Q 400,300 650,300" class="wire solid" stroke="#0000ff"/><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><path d="M 150,350 Q 400,350 650,350" class="wire dashed" stroke="#0000ff"/><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><path d="M 150,430 Q 400,430 650,430" class="wire dashed" stroke="#996633"/><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><path d="M 150,480 Q 400,480 650,480" class="wire solid" stroke="#996633"/><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><text x="180" y="555" text-anchor="start">0m</text><text x="630" y="555" text-anchor="end">0m</text><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><path d="M 150,300 Q 400,300 650,300" class="wire solid" stroke="#0000ff"/><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><path d="M 150,350 Q 400,350 650,350" class="wire dashed" stroke="#0000ff"/><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><path d="M 150,430 Q 400,430 650,430" class="wire dashed" stroke="#996633"/><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><path d="M 150,480 Q 400,480 650,480" class="wire solid" stroke="#996633"/><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text><text x="150" y="290" text-anchor="middle">0.1m</text><circle cx="150" cy="300" r="8" fill="black"/><circle cx="150" cy="550" r="8" fill="black"/><line x1="150" y1="300" x2="150" y2="550" stroke="black" stroke-width="3"/><text x="150" y="290" text-anchor="middle">0.1m</text><circle cx="150" cy="550" r="8" fill="black"/><circle cx="150" cy="300" r="8" fill="black"/><line x1="150" y1="550" x2="150" y2="300" stroke="black" stroke-width="3"/></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,110 650,170" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,160 650,220" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,110 650,50" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,160 650,100" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><path d="M 150,300 Q 400,300 650,300" class="wire solid" stroke="#0000ff"/><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><path d="M 150,350 Q 400,350 650,350" class="wire dashed" stroke="#0000ff"/><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><path d="M 150,430 Q 400,430 650,430" class="wire dashed" stroke="#996633"/><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><path d="M 150,480 Q 400,480 650,480" class="wire solid" stroke="#996633"/><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,175 650,300" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,225 650,350" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><path d="M 150,300 Q 400,175 650,50" class="wire solid" stroke="#0000ff"/><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><path d="M 150,350 Q 400,225 650,100" class="wire dashed" stroke="#0000ff"/><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><path d="M 150,430 Q 400,430 650,430" class="wire dashed" stroke="#996633"/><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><path d="M 150,480 Q 400,480 650,480" class="wire solid" stroke="#996633"/><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,235 650,300" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,285 650,350" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><path d="M 150,300 Q 400,235 650,170" class="wire solid" stroke="#0000ff"/><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><path d="M 150,350 Q 400,285 650,220" class="wire dashed" stroke="#0000ff"/><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><path d="M 150,430 Q 400,430 650,430" class="wire dashed" stroke="#996633"/><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><path d="M 150,480 Q 400,480 650,480" class="wire solid" stroke="#996633"/><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,265 650,480" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,265 650,430" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,195 650,220" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,285 650,350" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><path d="M 150,300 Q 400,235 650,170" class="wire solid" stroke="#0000ff"/><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><path d="M 150,350 Q 400,325 650,300" class="wire dashed" stroke="#0000ff"/><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><path d="M 150,430 Q 400,265 650,100" class="wire dashed" stroke="#996633"/><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><path d="M 150,480 Q 400,265 650,50" class="wire solid" stroke="#996633"/><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><text x="180" y="55" text-anchor="start">0m</text><text x="630" y="55" text-anchor="end">21.4m</text><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><path d="M 150,300 Q 400,300 650,300" class="wire solid" stroke="#0000ff"/><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><path d="M 150,350 Q 400,350 650,350" class="wire dashed" stroke="#0000ff"/><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><path d="M 150,430 Q 400,430 650,430" class="wire dashed" stroke="#996633"/><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><path d="M 150,480 Q 400,480 650,480" class="wire solid" stroke="#996633"/><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><text x="180" y="305" text-anchor="start">0.2m</text><text x="630" y="305" text-anchor="end">22.4m</text><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><text x="180" y="355" text-anchor="start">0.2m</text><text x="630" y="355" text-anchor="end">22.4m</text><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><path d="M 150,430 Q 400,430 650,430" class="wire dashed" stroke="#996633"/><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><path d="M 150,480 Q 400,480 650,480" class="wire solid" stroke="#996633"/><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><text x="180" y="105" text-anchor="start">11.3m</text><text x="630" y="105" text-anchor="end">13.4m</text><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><text x="180" y="225" text-anchor="start">11.2m</text><text x="630" y="225" text-anchor="end">13.2m</text><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><text x="180" y="305" text-anchor="start">11.3m</text><text x="630" y="305" text-anchor="end">13.1m</text><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><text x="180" y="355" text-anchor="start">11.4m</text><text x="630" y="355" text-anchor="end">13.4m</text><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><text x="180" y="435" text-anchor="start">11.3m</text><text x="630" y="435" text-anchor="end">13.1m</text><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><text x="180" y="485" text-anchor="start">11.3m</text><text x="630" y="485" text-anchor="end">13.4m</text><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><text x="180" y="305" text-anchor="start">0m</text><text x="630" y="305" text-anchor="end">0m</text><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><text x="180" y="355" text-anchor="start">0m</text><text x="630" y="355" text-anchor="end">0m</text><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><text x="180" y="435" text-anchor="start">0m</text><text x="630" y="435" text-anchor="end">0m</text><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><text x="180" y="485" text-anchor="start">0m</text><text x="630" y="485" text-anchor="end">0m</text><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><path d="M 150,300 Q 400,300 650,300" class="wire solid" stroke="#0000ff"/><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><path d="M 150,350 Q 400,350 650,350" class="wire dashed" stroke="#0000ff"/><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><path d="M 150,430 Q 400,430 650,430" class="wire dashed" stroke="#996633"/><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><path d="M 150,480 Q 400,480 650,480" class="wire solid" stroke="#996633"/><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><style>
.wire { stroke-width: 10; fill: none; }
.label { font-family: Arial; font-size: 24px; font-weight: bold; }
.solid { stroke-dasharray: none; }
.dashed { stroke-dasharray: 30 15; }
</style><rect width="100%" height="100%" fill="#f9f9f9"/><line x1="100" y1="50" x2="150" y2="50" class="wire dashed" stroke="#ff9900"/><path d="M 150,50 Q 400,50 650,50" class="wire dashed" stroke="#ff9900"/><line x1="650" y1="50" x2="700" y2="50" class="wire dashed" stroke="#ff9900"/><text x="70" y="55" class="label" text-anchor="end">1</text><text x="730" y="55" class="label">1</text><line x1="100" y1="100" x2="150" y2="100" class="wire solid" stroke="#ff9900"/><path d="M 150,100 Q 400,100 650,100" class="wire solid" stroke="#ff9900"/><line x1="650" y1="100" x2="700" y2="100" class="wire solid" stroke="#ff9900"/><text x="70" y="105" class="label" text-anchor="end">2</text><text x="730" y="105" class="label">2</text><line x1="100" y1="170" x2="150" y2="170" class="wire dashed" stroke="#009900"/><path d="M 150,170 Q 400,170 650,170" class="wire dashed" stroke="#009900"/><line x1="650" y1="170" x2="700" y2="170" class="wire dashed" stroke="#009900"/><text x="70" y="175" class="label" text-anchor="end">3</text><text x="730" y="175" class="label">3</text><line x1="100" y1="220" x2="150" y2="220" class="wire solid" stroke="#009900"/><path d="M 150,220 Q 400,220 650,220" class="wire solid" stroke="#009900"/><line x1="650" y1="220" x2="700" y2="220" class="wire solid" stroke="#009900"/><text x="70" y="225" class="label" text-anchor="end">6</text><text x="730" y="225" class="label">6</text><line x1="100" y1="300" x2="150" y2="300" class="wire solid" stroke="#0000ff"/><path d="M 150,300 Q 400,325 650,350" class="wire solid" stroke="#0000ff"/><line x1="650" y1="300" x2="700" y2="300" class="wire solid" stroke="#0000ff"/><text x="70" y="305" class="label" text-anchor="end">4</text><text x="730" y="305" class="label">4</text><line x1="100" y1="350" x2="150" y2="350" class="wire dashed" stroke="#0000ff"/><path d="M 150,350 Q 400,325 650,300" class="wire dashed" stroke="#0000ff"/><line x1="650" y1="350" x2="700" y2="350" class="wire dashed" stroke="#0000ff"/><text x="70" y="355" class="label" text-anchor="end">5</text><text x="730" y="355" class="label">5</text><line x1="100" y1="430" x2="150" y2="430" class="wire dashed" stroke="#996633"/><path d="M 150,430 Q 400,430 650,430" class="wire dashed" stroke="#996633"/><line x1="650" y1="430" x2="700" y2="430" class="wire dashed" stroke="#996633"/><text x="70" y="435" class="label" text-anchor="end">7</text><text x="730" y="435" class="label">7</text><line x1="100" y1="480" x2="150" y2="480" class="wire solid" stroke="#996633"/><path d="M 150,480 Q 400,480 650,480" class="wire solid" stroke="#996633"/><line x1="650" y1="480" x2="700" y2="480" class="wire solid" stroke="#996633"/><text x="70" y="485" class="label" text-anchor="end">8</text><text x="730" y="485" class="label">8</text><line x1="100" y1="550" x2="150" y2="550" class="wire solid" stroke="#777777"/><path d="M 150,550 Q 400,550 650,550" class="wire solid" stroke="#777777"/><line x1="650" y1="550" x2="700" y2="550" class="wire solid" stroke="#777777"/><text x="70" y="555" class="label" text-anchor="end">S</text><text x="730" y="555" class="label">S</text></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

Some files were not shown because too many files have changed in this diff Show More