dev
This commit is contained in:
129
src/components/Materials/Materials.module.css
Normal file
129
src/components/Materials/Materials.module.css
Normal 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%;
|
||||
}
|
||||
}
|
||||
154
src/components/Materials/index.jsx
Normal file
154
src/components/Materials/index.jsx
Normal 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;
|
||||
221
src/components/ScoreQuery/ScoreQuery.module.css
Normal file
221
src/components/ScoreQuery/ScoreQuery.module.css
Normal 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%;
|
||||
}
|
||||
}
|
||||
371
src/components/ScoreQuery/index.jsx
Normal file
371
src/components/ScoreQuery/index.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
124
src/pages/Control/Control.module.css
Normal file
124
src/pages/Control/Control.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
76
src/pages/Control/index.jsx
Normal file
76
src/pages/Control/index.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
223
src/pages/FlukeMoniPage/index.jsx
Normal file
223
src/pages/FlukeMoniPage/index.jsx
Normal 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
20
src/pages/_app.jsx
Normal 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
15
src/pages/_document.js
Normal 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
15
src/pages/index.jsx
Normal 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
28
src/router/index.jsx
Normal 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;
|
||||
71
src/styles/FlukeMoniPage.css
Normal file
71
src/styles/FlukeMoniPage.css
Normal 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;
|
||||
}
|
||||
140
src/styles/FlukeMoniPage.module.css
Normal file
140
src/styles/FlukeMoniPage.module.css
Normal 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);
|
||||
}
|
||||
74
src/styles/NetAllyMoniPage.module.css
Normal file
74
src/styles/NetAllyMoniPage.module.css
Normal 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
42
src/styles/globals.css
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user