This commit is contained in:
2025-10-22 03:33:11 +00:00
commit 0d300ff236
3020 changed files with 2363259 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
.container {
padding: 20px;
background: #fff;
border-radius: 8px;
min-height: 100%;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 24px;
}
.tab {
padding: 10px 20px;
border: none;
border-radius: 4px;
background-color: #f8f7fc;
color: #666;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
}
.tab:hover {
background-color: #f3f0f9;
}
.tab.active {
background-color: #6f42c1;
color: white;
}
.materialsList {
display: flex;
flex-direction: column;
gap: 20px;
}
.materialItem {
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 20px;
background-color: white;
transition: all 0.3s;
}
.materialItem:hover {
background-color: #f8f7fc;
border-color: #e6dcf5;
}
.materialItem h3 {
margin: 0 0 15px 0;
color: #6f42c1;
font-size: 18px;
}
.materialItem h4 {
margin: 10px 0;
color: #666;
font-size: 14px;
}
.downloadButtons {
display: flex;
flex-direction: row;
gap: 15px;
justify-content: flex-start;
}
.courseware, .teachingPlan ,.experimentData {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.downloadButtons button {
padding: 8px 16px;
border: 1px solid #6f42c1;
border-radius: 4px;
background-color: white;
color: #6f42c1;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
}
.downloadButtons button:hover {
background-color: #6f42c1;
color: white;
box-shadow: 0 0 0 2px rgba(111, 66, 193, 0.2);
}
/* 响应式设计 */
@media screen and (max-width: 768px) {
.container {
padding: 16px;
}
.courseware, .teachingPlan {
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.downloadButtons button {
width: 100%;
}
.materialItem h3 {
font-size: 16px;
}
}
@media screen and (max-width: 480px) {
.container {
padding: 12px;
}
.tabs {
flex-direction: column;
}
.tab {
width: 100%;
}
}

View File

@@ -0,0 +1,154 @@
import React, { useState } from 'react';
import styles from './Materials.module.css';
const Materials = () => {
const [activeTab, setActiveTab] = useState('theory');
const teachingPlanNames = {
1: '实验1熟悉线缆认证测试仪 教案',
2: '实验2铜缆认证测试课程 教案',
3: '实验3铜缆故障分析课程 教案',
4: '实验4光纤认证测试课程 教案',
5: '实验5光纤故障分析课程 教案',
6: '实验6报告软件的使用和结果统计 教案',
};
// 课件文件名映射,确保与 public/doc 下文件名一致
const coursewareTheoryNames = {
1: 'EST-100配套课件 理论第1章',
2: 'EST-100配套课件 理论第2章',
3: 'EST-100配套课件 理论第3章',
};
const coursewareLabNames = {
1: 'EST-100配套课件 实验1 熟悉认证测试仪',
2: 'EST-100配套课件 实验2 铜缆认证测试',
3: 'EST-100配套课件 实验3 铜缆故障分析',
4: 'EST-100配套课件 实验4 光纤认证测试',
5: 'EST-100配套课件 实验5 光纤故障分析',
6: 'EST-100配套课件 实验6 报告软件的使用与统计',
};
const theoryMaterials = [
{ id: 1, title: '第一章 走进综合布线测试' },
{ id: 2, title: '第二章 综合布线测试标准和参数' },
{ id: 3, title: '第三章 综合布线系统故障诊断' },
];
const labMaterials = [
{ id: 1, title: '实验1熟悉线缆认证测试仪' },
{ id: 2, title: '实验2铜缆认证测试' },
{ id: 3, title: '实验3铜缆故障分析' },
{ id: 4, title: '实验4光纤认证测试' },
{ id: 5, title: '实验5光纤故障分析' },
{ id: 6, title: '实验6报告软件的使用和结果统计' },
];
const handleDownload = (type, format, id, title) => {
const prefix = 'EST-100配套课件';
let fileName = '';
let extension = '';
if (type === '课件') {
if (activeTab === 'lab') {
fileName = coursewareLabNames[id];
} else {
fileName = coursewareTheoryNames[id];
}
extension = format.toLowerCase() === 'ppt' ? 'pptx' : 'pdf';
} else if (type === '教案') {
fileName = teachingPlanNames[id] || `实验${id}${title.replace(/^实验\d+/, '')} 教案`;
extension = format.toLowerCase() === 'word' ? 'docx' : 'pdf';
} else if (type === '数据') {
fileName = `EST实验${id}数据`;
extension = 'flw';
}
const fileUrl = encodeURI(`/doc/${fileName}.${extension}`);
const link = document.createElement('a');
link.href = fileUrl;
// 强制下载,避免 PDF 在新标签页直接打开被拦截
link.setAttribute('download', `${fileName}.${extension}`);
link.rel = 'noopener noreferrer';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<div className={styles.container}>
<div className={styles.tabs}>
<button
className={`${styles.tab} ${activeTab === 'theory' ? styles.active : ''}`}
onClick={() => setActiveTab('theory')}
>
理论课件
</button>
<button
className={`${styles.tab} ${activeTab === 'lab' ? styles.active : ''}`}
onClick={() => setActiveTab('lab')}
>
实验课件
</button>
</div>
<div className={styles.content}>
{activeTab === 'theory' ? (
<div className={styles.materialsList}>
{theoryMaterials.map(material => (
<div key={material.id} className={styles.materialItem}>
<h3>{material.title}</h3>
<div className={styles.downloadButtons}>
<button onClick={() => handleDownload('课件', 'PDF', material.id, material.title)}>
PDF版本
</button>
<button onClick={() => handleDownload('课件', 'PPT', material.id, material.title)}>
PPT版本
</button>
</div>
</div>
))}
</div>
) : (
<div className={styles.materialsList}>
{labMaterials.map(material => (
<div key={material.id} className={styles.materialItem}>
<h3>{material.title}</h3>
<div className={styles.downloadButtons}>
<div className={styles.courseware}>
<h4>课件下载</h4>
<button onClick={() => handleDownload('课件', 'PDF', material.id, material.title)}>
PDF版本
</button>
<button onClick={() => handleDownload('课件', 'PPT', material.id, material.title)}>
PPT版本
</button>
</div>
<div className={styles.teachingPlan}>
<h4>教案下载</h4>
<button onClick={() => handleDownload('教案', 'PDF', material.id, material.title)}>
PDF版本
</button>
<button onClick={() => handleDownload('教案', 'Word', material.id, material.title)}>
Word版本
</button>
</div>
{[3, 5, 6].includes(material.id) && (
<div className={styles.experimentData}>
<h4>实验数据</h4>
<button onClick={() => handleDownload('数据', '', material.id, material.title)}>
下载数据
</button>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default Materials;

View File

@@ -0,0 +1,221 @@
.container {
padding: 20px;
background: #fff;
border-radius: 8px;
min-height: 100%;
}
.header {
margin-bottom: 24px;
}
.orgInfo {
color: #666;
font-size: 14px;
margin-bottom: 8px;
}
.header h2 {
color: #6f42c1;
margin-bottom: 20px;
font-size: 24px;
}
.controls {
display: flex;
flex-direction: column;
gap: 16px;
}
.searchArea {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.filterArea {
display: flex;
gap: 16px;
align-items: center;
flex-wrap: wrap;
}
.searchField {
position: relative;
flex: 1;
max-width: 300px;
}
.searchInput {
width: 100%;
padding: 8px 32px 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
transition: all 0.3s;
}
.searchInput:focus {
outline: none;
border-color: #6f42c1;
box-shadow: 0 0 0 2px rgba(111, 66, 193, 0.2);
}
.select {
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
min-width: 150px;
background-color: white;
cursor: pointer;
transition: all 0.3s;
}
.select:focus {
outline: none;
border-color: #6f42c1;
box-shadow: 0 0 0 2px rgba(111, 66, 193, 0.2);
}
.checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
color: #666;
}
.checkbox input {
margin: 0;
cursor: pointer;
}
.exportButton {
padding: 8px 16px;
background-color: #6f42c1;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.exportButton:hover {
background-color: #5a32a3;
}
.clearButton {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #999;
cursor: pointer;
font-size: 16px;
padding: 4px;
line-height: 1;
}
.clearButton:hover {
color: #666;
}
.tableContainer {
position: relative;
overflow-x: auto;
}
.loading {
text-align: center;
padding: 40px;
color: #6f42c1;
font-size: 16px;
}
.table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
.table th {
background: #f8f7fc;
color: #6f42c1;
font-weight: 500;
text-align: left;
padding: 12px 16px;
cursor: pointer;
user-select: none;
white-space: nowrap;
border-bottom: 1px solid #f0f0f0;
}
.table th:hover {
background: #f3f0f9;
}
.table td {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
}
.table tbody tr:hover {
background-color: #f8f7fc;
}
.sortIcon {
margin-left: 4px;
color: #6f42c1;
}
/* 响应式设计 */
@media screen and (max-width: 768px) {
.container {
padding: 16px;
}
.controls {
gap: 12px;
}
.searchArea,
.filterArea {
flex-direction: column;
}
.searchField,
.select {
max-width: 100%;
}
.header h2 {
font-size: 20px;
margin-bottom: 16px;
}
.table th,
.table td {
padding: 8px;
font-size: 14px;
}
}
@media screen and (max-width: 480px) {
.container {
padding: 12px;
}
.header h2 {
font-size: 18px;
margin-bottom: 12px;
}
.exportButton {
width: 100%;
}
}

View File

@@ -0,0 +1,371 @@
'use client';
import { useState, useEffect } from 'react';
import styles from './ScoreQuery.module.css';
const getBaseUrl = () => {
if (typeof window !== 'undefined') {
const url = new URL(window.location.href);
return `${url.protocol}//${url.hostname}:3000/survey-answers`;
}
return '/';
};
const BASE_URL = getBaseUrl();
// 项目ID映射表
const PROJECT_MAP = {
'XEdP7K': '实验一随堂练',
'YaeKzc': '实验二随堂练',
'aGLHW7': '实验三随堂练',
'm1rrB7': '实验四随堂练',
'kYugoD': '实验五随堂练',
'MN3hY7': '实验六随堂练'
};
export default function ScoreQuery() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [username, setUsername] = useState('');
const [currentOrg, setCurrentOrg] = useState('');
const [selectedProject, setSelectedProject] = useState('all');
const [showHighestOnly, setShowHighestOnly] = useState(false);
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const orgParam = urlParams.get('org');
if (orgParam) {
const decodedOrg = decodeURIComponent(orgParam);
setCurrentOrg(decodedOrg);
}
}, []);
const fetchData = async () => {
setLoading(true);
try {
const params = new URLSearchParams();
if (currentOrg) params.append('org', currentOrg);
if (username) params.append('username', username);
const response = await fetch(`${BASE_URL}${params.toString() ? `?${params.toString()}` : ''}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`获取数据失败: ${response.status}`);
}
const result = await response.json();
if (result.success) {
setData(result.data || []);
} else {
throw new Error(result.error || '获取数据失败');
}
} catch (error) {
console.error('获取数据失败:', error);
alert('获取数据失败,请稍后重试');
setData([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (currentOrg) {
fetchData();
}
}, [currentOrg, username]);
const handleSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc';
}
setSortConfig({ key, direction });
};
const getProjectName = (projectId) => {
return PROJECT_MAP[projectId] || projectId;
};
// 处理数据筛选和最高分
const processData = (data) => {
let processedData = [...data];
// 项目筛选
if (selectedProject !== 'all') {
processedData = processedData.filter(item => item.projectId === selectedProject);
}
// 最高分筛选
if (showHighestOnly) {
const highestScores = {};
processedData.forEach(item => {
const key = `${item.username}-${item.projectId}`;
if (!highestScores[key] || item.score > highestScores[key].score) {
highestScores[key] = item;
}
});
processedData = Object.values(highestScores);
}
// 排序
if (sortConfig.key) {
processedData.sort((a, b) => {
let aValue = a[sortConfig.key];
let bValue = b[sortConfig.key];
if (sortConfig.key === 'submitTime') {
aValue = new Date(aValue);
bValue = new Date(bValue);
}
if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1;
if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
}
return processedData;
};
const handleExport = () => {
try {
const processedData = processData(data);
// 添加 BOM 以确保 Excel 正确识别 UTF-8 编码
const BOM = '\uFEFF';
// 构建 CSV 内容
const headers = ['考试项目', '用户名', '分数', '提交时间'];
const rows = processedData.map(item => [
getProjectName(item.projectId),
item.username,
item.score || '-',
item.submitTime ? new Date(item.submitTime).toLocaleString() : '-'
]);
// 将所有字段用双引号包裹,处理可能包含逗号的内容
const csvContent = BOM + [headers, ...rows]
.map(row =>
row.map(cell => `"${String(cell).replace(/"/g, '""')}"`)
.join(',')
)
.join('\n');
// 创建 Blob 对象
const blob = new Blob([csvContent], {
type: 'text/csv;charset=utf-8'
});
// 创建下载链接
const fileName = `成绩数据_${currentOrg}_${new Date().toLocaleDateString().replace(/\//g, '-')}.csv`;
// 尝试使用 iframe 方式下载
const downloadInIframe = () => {
try {
const url = window.URL.createObjectURL(blob);
// 创建隐藏的 iframe
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
// 在 iframe 中创建并触发下载链接
const iframeDoc = iframe.contentWindow.document;
const link = iframeDoc.createElement('a');
link.href = url;
link.download = fileName;
link.click();
// 清理
setTimeout(() => {
document.body.removeChild(iframe);
window.URL.revokeObjectURL(url);
}, 100);
return true;
} catch (e) {
console.warn('iframe下载方式失败尝试其他方式', e);
return false;
}
};
// 尝试使用 Blob URL 直接下载
const downloadWithBlobUrl = () => {
try {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.style.display = 'none';
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
setTimeout(() => {
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}, 100);
return true;
} catch (e) {
console.warn('Blob URL下载方式失败尝试其他方式', e);
return false;
}
};
// 尝试使用 Data URL 下载
const downloadWithDataUrl = () => {
try {
const reader = new FileReader();
reader.onload = () => {
const link = document.createElement('a');
link.style.display = 'none';
link.href = reader.result;
link.download = fileName;
document.body.appendChild(link);
link.click();
setTimeout(() => {
document.body.removeChild(link);
}, 100);
};
reader.readAsDataURL(blob);
return true;
} catch (e) {
console.warn('Data URL下载方式失败', e);
return false;
}
};
// 按优先级尝试不同的下载方式
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
// IE 浏览器
window.navigator.msSaveOrOpenBlob(blob, fileName);
} else if (!downloadInIframe() && !downloadWithBlobUrl() && !downloadWithDataUrl()) {
throw new Error('所有下载方式都失败了');
}
} catch (error) {
console.error('导出数据失败:', error);
alert('导出数据失败,请稍后重试');
}
};
const formatDate = (dateString) => {
if (!dateString) return '-';
return new Date(dateString).toLocaleString();
};
const processedData = processData(data);
return (
<div className={styles.container}>
<div className={styles.header}>
{currentOrg && (
<div className={styles.orgInfo}>当前组织{currentOrg}</div>
)}
<h2>成绩查询</h2>
<div className={styles.controls}>
<div className={styles.searchArea}>
<div className={styles.searchField}>
<input
type="text"
placeholder="搜索用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
className={styles.searchInput}
/>
<button
onClick={() => setUsername('')}
className={styles.clearButton}
>
×
</button>
</div>
</div>
<div className={styles.filterArea}>
<select
value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)}
className={styles.select}
>
<option value="all">所有考试项目</option>
{Object.entries(PROJECT_MAP).map(([id, name]) => (
<option key={id} value={id}>{name}</option>
))}
</select>
<label className={styles.checkbox}>
<input
type="checkbox"
checked={showHighestOnly}
onChange={(e) => setShowHighestOnly(e.target.checked)}
/>
仅显示最高分
</label>
<button
onClick={handleExport}
className={styles.exportButton}
>
导出数据
</button>
</div>
</div>
</div>
<div className={styles.tableContainer}>
{loading ? (
<div className={styles.loading}>加载中...</div>
) : (
<table className={styles.table}>
<thead>
<tr>
<th onClick={() => handleSort('projectId')}>
考试项目
{sortConfig.key === 'projectId' && (
<span className={styles.sortIcon}>
{sortConfig.direction === 'asc' ? '↑' : '↓'}
</span>
)}
</th>
<th onClick={() => handleSort('username')}>
用户名
{sortConfig.key === 'username' && (
<span className={styles.sortIcon}>
{sortConfig.direction === 'asc' ? '↑' : '↓'}
</span>
)}
</th>
<th onClick={() => handleSort('score')}>
分数
{sortConfig.key === 'score' && (
<span className={styles.sortIcon}>
{sortConfig.direction === 'asc' ? '↑' : '↓'}
</span>
)}
</th>
<th onClick={() => handleSort('submitTime')}>
提交时间
{sortConfig.key === 'submitTime' && (
<span className={styles.sortIcon}>
{sortConfig.direction === 'asc' ? '↑' : '↓'}
</span>
)}
</th>
</tr>
</thead>
<tbody>
{processedData.map((item) => (
<tr key={`${item.projectId}-${item.username}-${item.submitTime}`}>
<td>{getProjectName(item.projectId)}</td>
<td>{item.username}</td>
<td>{item.score ? `${item.score}` : '-'}</td>
<td>{formatDate(item.submitTime)}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,124 @@
.container {
display: flex;
min-height: 100vh;
background-color: #f8f7fc;
position: relative;
}
.sidebar {
width: 240px;
background-color: #fff;
box-shadow: 2px 0 8px rgba(111, 66, 193, 0.1);
padding: 20px 0;
transition: transform 0.3s ease;
}
.menuTitle {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 20px;
font-weight: bold;
color: #6f42c1;
padding: 16px 24px;
border-bottom: 1px solid #ede8f7;
margin-bottom: 8px;
}
.menuToggle {
display: none;
background: none;
border: none;
color: #6f42c1;
cursor: pointer;
padding: 8px;
position: fixed;
top: 10px;
left: 10px;
z-index: 1000;
border-radius: 4px;
background-color: white;
box-shadow: 0 2px 4px rgba(111, 66, 193, 0.1);
}
.menu {
display: flex;
flex-direction: column;
padding: 8px 0;
}
.menuItem {
display: flex;
align-items: center;
padding: 12px 24px;
font-size: 16px;
color: #595959;
background: none;
border: none;
text-align: left;
cursor: pointer;
transition: all 0.3s ease;
}
.menuItem:hover {
color: #6f42c1;
background-color: #f3f0f9;
}
.menuItem.active {
color: #6f42c1;
background-color: #f3f0f9;
border-right: 3px solid #6f42c1;
font-weight: 500;
}
.content {
flex: 1;
padding: 24px;
background-color: #fff;
margin: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(111, 66, 193, 0.08);
}
/* 响应式设计 */
@media screen and (max-width: 768px) {
.container {
flex-direction: column;
}
.menuToggle {
display: block;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
z-index: 999;
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
.content {
margin: 60px 16px 16px 16px;
padding: 16px;
}
}
/* 小屏幕手机 */
@media screen and (max-width: 480px) {
.content {
margin: 60px 8px 8px 8px;
padding: 12px;
}
.menuItem {
padding: 10px 16px;
font-size: 14px;
}
}

View File

@@ -0,0 +1,76 @@
import { useState } from 'react';
import ScoreQuery from '../../components/ScoreQuery';
import Materials from '../../components/Materials';
import styles from './Control.module.css';
export default function Control() {
const [selectedMenu, setSelectedMenu] = useState('score');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const menuItems = [
{
key: 'score',
label: '成绩查询',
},
{
key: 'materials',
label: '课件/教案',
}
];
const toggleSidebar = () => {
setIsSidebarOpen(!isSidebarOpen);
};
const handleMenuClick = (key) => {
setSelectedMenu(key);
setIsSidebarOpen(false); // 在移动端选择菜单项后关闭侧边栏
};
const renderContent = () => {
switch (selectedMenu) {
case 'score':
return <ScoreQuery />;
case 'materials':
return <Materials/>; // 这里需要替换为实际的课件/教案组件
default:
return null;
}
};
return (
<div className={styles.container}>
{/* 移动端菜单切换按钮 */}
<button
className={styles.menuToggle}
onClick={toggleSidebar}
aria-label="Toggle menu"
>
</button>
{/* 左侧菜单 */}
<div className={`${styles.sidebar} ${isSidebarOpen ? styles.open : ''}`}>
<div className={styles.menuTitle}>
控制台
</div>
<nav className={styles.menu}>
{menuItems.map(item => (
<button
key={item.key}
className={`${styles.menuItem} ${selectedMenu === item.key ? styles.active : ''}`}
onClick={() => handleMenuClick(item.key)}
>
{item.label}
</button>
))}
</nav>
</div>
{/* 右侧内容区 */}
<div className={styles.content}>
{renderContent()}
</div>
</div>
);
}

View File

@@ -0,0 +1,223 @@
import Head from 'next/head'
import { useEffect, useState } from 'react'
import styles from '../../styles/FlukeMoniPage.module.css'
export default function FlukeMoniPage({ token }) {
const hostname = typeof window !== 'undefined' ? window.location.hostname : '';
const PROTOCOL = typeof window !== 'undefined' ? window.location.protocol : '';
const BASE_URL = `${PROTOCOL}//${hostname}:3001`;
// const BASE_URL = `https://est-live.cn`
const openExperiment = (folder) => {
if (typeof window !== 'undefined') {
const iframe = document.getElementById('experimentIframe')
iframe.src = `/flukemoni/${folder}/HOMEPAGE.html`
const modalElement = document.getElementById('experimentModal')
const modal = new bootstrap.Modal(modalElement)
modal.show()
}
}
const openGuide = (path) => {
const guideWindow = window.open(`${BASE_URL}${path}`, '实验指导', 'width=500,height=800,right=0,top=0')
}
return (
<>
<Head>
<title>线缆测试与故障诊断实验</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
<div className="container mt-5">
<h1>线缆测试与故障诊断实验</h1>
<div className="row row-cols-1 row-cols-sm-2 row-cols-md-3 mt-4">
<div className="col-md-4 mb-4">
<div className="card">
<h4 className="card-header d-flex justify-content-between align-items-center">
实验一
<span
className={styles.guideLink}
onClick={() => openGuide('/docs/fluke/实训教程/实验一%20熟悉认证测试仪/🌐模拟器操作步骤')}
>
实验指导
</span>
</h4>
<div className="card-body">
<h5 className="card-title">认识线缆测试仪界面</h5>
<p className="card-text">熟悉福禄克线缆测试仪的界面</p>
<button className="btn btn-primary" onClick={() => openExperiment('Ex1')}>打开</button>
</div>
</div>
</div>
<div className="col-md-4 mb-4">
<div className="card">
<h4 className="card-header d-flex justify-content-between align-items-center">
实验二
<span
className={styles.guideLink}
onClick={() => openGuide('/docs/fluke/实训教程/实验二%20铜缆认证测试/🌐模拟器操作步骤')}
>
实验指导
</span>
</h4>
<div className="card-body">
<h5 className="card-title">铜缆认证测试</h5>
<p className="card-text">铜缆认证测试实验</p>
<button className="btn btn-primary" onClick={() => openExperiment('Ex2')}>打开</button>
</div>
</div>
</div>
<div className="col-md-4 mb-4">
<div className="card">
<h4 className="card-header d-flex justify-content-between align-items-center">
实验三
<span
className={styles.guideLink}
onClick={() => openGuide('/docs/fluke/实训教程/实验三%20铜缆故障分析/🌐模拟器操作步骤')}
>
实验指导
</span>
</h4>
<div className="card-body">
<h5 className="card-title">铜缆故障分析</h5>
<p className="card-text">铜缆链路问题诊断与分析</p>
<button className="btn btn-primary" onClick={() => openExperiment('Ex3')}>打开</button>
</div>
</div>
</div>
<div className="col-md-4 mb-4">
<div className="card">
<h4 className="card-header d-flex justify-content-between align-items-center">
实验四
<span
className={styles.guideLink}
onClick={() => openGuide('/docs/fluke/实训教程/实验四%20光纤认证测试/🌐模拟实验操作步骤/任务一%20选择光纤验收标准进行测试')}
>
实验指导
</span>
</h4>
<div className="card-body">
<h5 className="card-title">OLTS认证测试</h5>
<p className="card-text">光纤光损耗认证测试实验</p>
<button className="btn btn-primary" onClick={() => openExperiment('Ex4')}>打开</button>
</div>
</div>
</div>
<div className="col-md-4 mb-4">
<div className="card">
<h4 className="card-header d-flex justify-content-between align-items-center">
实验四
<span
className={styles.guideLink}
onClick={() => openGuide('/docs/fluke/实训教程/实验四%20光纤认证测试/🌐模拟实验操作步骤/任务一%20选择光纤验收标准进行测试')}
>
实验指导
</span>
</h4>
<div className="card-body">
<h5 className="card-title">OTDR认证测试</h5>
<p className="card-text">光纤光时域反射测试实验</p>
<button className="btn btn-primary" onClick={() => openExperiment('Ex4_2')}>打开</button>
</div>
</div>
</div>
<div className="col-md-4 mb-4">
<div className="card">
<h4 className="card-header d-flex justify-content-between align-items-center">
实验五
<span
className={styles.guideLink}
onClick={() => openGuide('/docs/fluke/实训教程/实验五%20光纤故障分析/🌐模拟器操作步骤')}
>
实验指导
</span>
</h4>
<div className="card-body">
<h5 className="card-title">光纤故障分析</h5>
<p className="card-text">光纤链路问题诊断与分析</p>
<button className="btn btn-primary" onClick={() => openExperiment('Ex5')}>打开</button>
</div>
</div>
</div>
</div>
</div>
<div id="experimentModal"
className="modal fade"
tabIndex="-1"
role="dialog"
aria-labelledby="experimentModalLabel"
aria-hidden="true"
data-bs-backdrop="static"
data-bs-keyboard="false"
>
<div className={`modal-dialog modal-dialog-centered ${styles.modalDialog}`}>
<div className={styles.modalContent}>
<div className={`modal-header ${styles.modalHeader}`}>
<div className="d-flex justify-content-between align-items-center w-100">
<h5 className="modal-title" id="experimentModalLabel">实验详情</h5>
<div>
<span
className={styles.guideLink}
onClick={() => {
const currentExperiment = document.getElementById('experimentIframe').src;
const folder = currentExperiment.split('/')[4];
let guidePath = '';
switch(folder) {
case 'Ex1':
guidePath = '/docs/fluke/实训教程/实验一%20熟悉认证测试仪/🌐模拟器操作步骤';
break;
case 'Ex2':
guidePath = '/docs/fluke/实训教程/实验二%20铜缆认证测试/🌐模拟器操作步骤';
break;
case 'Ex3':
guidePath = '/docs/fluke/实训教程/实验三%20铜缆故障分析/🌐模拟器操作步骤';
break;
case 'Ex4':
guidePath = '/docs/fluke/实训教程/实验四%20光纤认证测试/🌐模拟实验操作步骤/任务一%20选择光纤验收标准进行测试';
break;
case 'Ex4_2':
guidePath = '/docs/fluke/实训教程/实验四%20光纤认证测试/🌐模拟实验操作步骤/任务一%20选择光纤验收标准进行测试';
break;
case 'Ex5':
guidePath = '/docs/fluke/实训教程/实验五%20光纤故障分析/🌐模拟器操作步骤';
break;
}
openGuide(guidePath);
}}
>
实验指导
</span>
<button
type="button"
className={styles.closeButton}
data-bs-dismiss="modal"
aria-label="关闭"
>
×
</button>
</div>
</div>
</div>
<div className={styles.modalBody}>
<iframe id="experimentIframe" src="" frameBorder="0"></iframe>
</div>
</div>
</div>
</div>
</>
)
}

20
src/pages/_app.jsx Normal file
View File

@@ -0,0 +1,20 @@
import '../styles/globals.css'
import Script from 'next/script'
function MyApp({ Component, pageProps }) {
return (
<>
<Script
src="/scripts/jquery-3.7.1.min.js"
strategy="beforeInteractive"
/>
<Script
src="/scripts/bootstrap.bundle.min.js"
strategy="beforeInteractive"
/>
<Component {...pageProps} />
</>
)
}
export default MyApp

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

@@ -0,0 +1,15 @@
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html>
<Head>
<link rel="stylesheet" href="/styles/bootstrap.min.css" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}

15
src/pages/index.jsx Normal file
View File

@@ -0,0 +1,15 @@
export default function Home() {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
color: '#dc3545'
}}>
错误的加载
</div>
)
}

28
src/router/index.jsx Normal file
View File

@@ -0,0 +1,28 @@
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import FlukeMoniPage from '../pages/FlukeMoniPage';
import { useRouter } from 'next/router'
// 如果你需要路由守卫或其他路由相关逻辑,可以在这里实现
export function useRouteGuard() {
const router = useRouter()
// 添加你的路由逻辑
return router
}
// 定义路由配置(用于参考)
export const routes = {
home: '/',
flukeMoni: '/FlukeMoniPage',
}
const routesArray = [
{
path: '/FlukeMoniPage',
element: <FlukeMoniPage />
},
];
export default routesArray;

View File

@@ -0,0 +1,71 @@
/* 卡片样式 */
.card {
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card-header {
padding: 0.75rem 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f8f9fa;
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 7px 7px 0 0;
}
/* 实验指导链接样式 */
.guide-link {
color: #007bff;
font-size: 0.9rem;
cursor: pointer;
transition: color 0.2s;
}
.guide-link:hover {
color: #0056b3;
text-decoration: underline;
}
/* 模态框样式 */
.modal-content {
width: 550px !important;
margin: 0 auto;
}
.modal-dialog {
height: auto;
}
.modal-header {
border-bottom: 1px solid #dee2e6;
padding: 1rem;
}
.modal-body {
padding: 1rem;
}
/* 按钮样式 */
.btn-primary {
background-color: #007bff;
border-color: #007bff;
transition: all 0.2s;
}
.btn-primary:hover {
background-color: #0056b3;
border-color: #0056b3;
}
/* 标题和文本样式 */
.card-title {
margin-bottom: 0.5rem;
font-weight: 500;
}
.card-text {
color: #6c757d;
margin-bottom: 1rem;
}

View File

@@ -0,0 +1,140 @@
/* 卡片样式 */
.card {
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.cardHeader {
padding: 0.75rem 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f8f9fa;
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 7px 7px 0 0;
}
/* 实验指导链接样式 */
.guideLink {
color: #007bff;
font-size: 0.9rem;
cursor: pointer;
transition: color 0.2s;
}
.guideLink:hover {
color: #0056b3;
text-decoration: underline;
}
.modal{
min-width: 600px;
}
/* 模态框样式 */
.modalContent {
min-width: 600px;
width: 100%;
background-color: #fff;
border-radius: 0.3rem;
position: relative;
pointer-events: auto; /* 确保内容可以点击 */
}
.modalDialog {
max-width: 90vw !important; /* 使用 !important 覆盖 Bootstrap 默认值 */
width: 30vw !important;
margin: 1.75rem auto;
}
.modalHeader {
display: flex;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #dee2e6;
}
.closeButton {
padding: 0.5rem 0.75rem;
background: none;
border: none;
font-size: 1.5rem;
font-weight: 700;
line-height: 1;
color: #000;
text-shadow: 0 1px 0 #fff;
opacity: 0.5;
cursor: pointer;
margin-left: 1rem;
}
.closeButton:hover {
opacity: 0.75;
}
.modalBody {
padding: 1rem;
width: 600px;
height: 68vh; /* 设置一个合适的高度 */
overflow: hidden; /* 防止出现双滚动条 */
}
.modalBody iframe {
width: 100%;
height: 100%;
border: none;
}
/* 按钮样式 */
.btnPrimary {
background-color: #007bff;
border-color: #007bff;
transition: all 0.2s;
}
.btnPrimary:hover {
background-color: #0056b3;
border-color: #0056b3;
}
/* 标题和文本样式 */
.cardTitle {
margin-bottom: 0.5rem;
font-weight: 500;
}
.cardText {
color: #6c757d;
margin-bottom: 1rem;
}
/* 容器样式 */
.container {
padding: 2rem;
}
.experimentTitle {
margin-bottom: 2rem;
color: #333;
}
/* 加载遮罩层 */
.loadingOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
}
.loadingSpinner {
padding: 20px;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

View File

@@ -0,0 +1,74 @@
.card {
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.guideLink {
color: #007bff;
font-size: 0.9rem;
cursor: pointer;
transition: color 0.2s;
}
.guideLink:hover {
color: #0056b3;
text-decoration: underline;
}
.modalContent {
min-width: 430px;
width: 100%;
background-color: #fff;
border-radius: 0.3rem;
position: relative;
pointer-events: auto;
}
.modalDialog {
max-width: 90vw !important;
width: 30vw !important;
margin: 1.75rem auto;
}
.modalHeader {
display: flex;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #dee2e6;
}
.closeButton {
padding: 0.5rem 0.75rem;
background: none;
border: none;
font-size: 1.5rem;
font-weight: 700;
line-height: 1;
color: #000;
text-shadow: 0 1px 0 #fff;
opacity: 0.5;
cursor: pointer;
margin-left: 1rem;
}
.closeButton:hover {
opacity: 0.75;
}
.modalBody {
padding: 1rem;
width: 460px;
height: 75vh;
overflow: hidden;
}
.modalBody iframe {
width: 100%;
height: 100%;
border: none;
}
.btnBlock {
width: 100%;
}

42
src/styles/globals.css Normal file
View File

@@ -0,0 +1,42 @@
:root {
--background: #ffffff;
--foreground: #000000;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}