You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
495 lines
20 KiB
495 lines
20 KiB
2 months ago
|
<!DOCTYPE html>
|
||
|
<html lang="en">
|
||
|
<head>
|
||
|
<meta charset="UTF-8">
|
||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
<title>随机点名系统</title>
|
||
|
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/ionicons/4.5.6/css/ionicons.min.css'>
|
||
|
<link rel="stylesheet" href="styles.css">
|
||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
|
<link href="https://fonts.googleapis.com/css2?family=Long+Cang&display=swap" rel="stylesheet">
|
||
|
<link href="https://fonts.googleapis.com/css2?family=Zhi+Mang+Xing&display=swap" rel="stylesheet">
|
||
|
<link href="https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&display=swap" rel="stylesheet">
|
||
|
</head>
|
||
|
<body>
|
||
|
<h1>随机点名系统</h1>
|
||
|
<div id="message" style="display:none; position:fixed; top:20px; left:50%; transform:translateX(-50%); background-color:rgba(0, 0, 0, 0.5); color:white; padding:10px; border-radius:5px; white-space:nowrap;">
|
||
|
</div>
|
||
|
<div class="container">
|
||
|
<div class="file-upload">
|
||
|
|
||
|
<label class="file-label" for="fileInput" style="cursor: pointer;">
|
||
|
<img src="file-icon.svg" alt="选择文件" style="width: 40px; height: 40px;" />
|
||
|
<input type="file" id="fileInput" style="display: none;" onchange="updateFileName()" />
|
||
|
</label>
|
||
|
|
||
|
<span id="fileName">未选择文件</span>
|
||
|
|
||
|
<button id="uploadButton" onclick="uploadFile()"style="background: none; border: none; padding: 0; cursor: pointer;">
|
||
|
<img src="upload-icon.svg" alt="导入" style="width: 40px; height: 40px;" />
|
||
|
</button>
|
||
|
</div>
|
||
|
|
||
|
<div class="button-row">
|
||
|
<button id="drawButton" onclick="startDraw()" disabled>开始点名</button>
|
||
|
</div>
|
||
|
|
||
|
<div class="button-row">
|
||
|
<button id="onClass" onclick="onClass()" style="width:20% ;">到场</button>
|
||
|
</div>
|
||
|
|
||
|
<div class="button-row">
|
||
|
<button id="correctButton" onclick="repeatCorrectly()" disabled>准确重复问题</button>
|
||
|
<button id="incorrectButton" onclick="repeatIncorrectly()" disabled>重复不准确</button>
|
||
|
</div>
|
||
|
|
||
|
<div class="rating">
|
||
|
<input type="radio" name="rating" id="5" value="5" />
|
||
|
<label for="5" data-value="5"><i class="icon ion-md-star-outline"></i><span class="star-number hidden">5</span></label>
|
||
|
<input type="radio" name="rating" id="4" value="4" />
|
||
|
<label for="4" data-value="4"><i class="icon ion-md-star-outline"></i><span class="star-number hidden">4</span></label>
|
||
|
<input type="radio" name="rating" id="3" value="3" />
|
||
|
<label for="3" data-value="3"><i class="icon ion-md-star-outline"></i><span class="star-number hidden">3</span></label>
|
||
|
<input type="radio" name="rating" id="2" value="2" />
|
||
|
<label for="2" data-value="2"><i class="icon ion-md-star-outline"></i><span class="star-number hidden">2</span></label>
|
||
|
<input type="radio" name="rating" id="1" value="1" />
|
||
|
<label for="1" data-value="1"><i class="icon ion-md-star-outline"></i><span class="star-number hidden">1</span></label>
|
||
|
</div>
|
||
|
|
||
|
|
||
|
|
||
|
<!--<input type="text" id="manualPoints" placeholder="手动加减积分 (范围 0.5 到 3)" /> -->
|
||
|
|
||
|
<div class="button-row">
|
||
|
<!--<button id="manualButton" onclick="updatePoints()" disabled>更新积分</button> -->
|
||
|
<button id="resetButton" onclick="resetAllPoints()">重置所有积分</button>
|
||
|
</div>
|
||
|
|
||
|
<div class="view-student-row">
|
||
|
<button onclick="viewStudentList()">查看学生名单</button>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="scrollingNames"></div>
|
||
|
<div id="selectedStudent"></div>
|
||
|
<canvas id="particleCanvas"></canvas>
|
||
|
|
||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.1/xlsx.full.min.js"></script>
|
||
|
<script>
|
||
|
let students = JSON.parse(localStorage.getItem('students')) || [];
|
||
|
let selectedStudent = null;
|
||
|
let intervalId;
|
||
|
let lock = false;//自动加分锁
|
||
|
let lock1 = false;//手动加分锁
|
||
|
let onClassAdded = false;
|
||
|
let autoPointsAdded = false;
|
||
|
let manualPointsUsed = false;
|
||
|
let studentDrawn = false;
|
||
|
let starLock = false;
|
||
|
|
||
|
const stars = document.querySelectorAll('.rating label');
|
||
|
|
||
|
stars.forEach(star => {
|
||
|
star.addEventListener('mouseenter', () => {
|
||
|
const number = star.querySelector('.star-number');
|
||
|
number.classList.remove('hidden');
|
||
|
});
|
||
|
|
||
|
star.addEventListener('mouseleave', () => {
|
||
|
const number = star.querySelector('.star-number');
|
||
|
number.classList.add('hidden');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
const particleCanvas = document.getElementById('particleCanvas');
|
||
|
const particleCtx = particleCanvas.getContext('2d');
|
||
|
particleCanvas.width = window.innerWidth;
|
||
|
particleCanvas.height = window.innerHeight;
|
||
|
|
||
|
let particles = [];
|
||
|
|
||
|
function createParticlesFromText(a, b) {
|
||
|
// 清空画布
|
||
|
particleCtx.clearRect(0, 0, particleCanvas.width, particleCanvas.height);
|
||
|
|
||
|
// 设置字体
|
||
|
particleCtx.font = '150px "Long Cang"';
|
||
|
particleCtx.fillStyle = '#003366';
|
||
|
|
||
|
// 绘制第一行文字
|
||
|
const x1 = particleCanvas.width / 2 - particleCtx.measureText(selectedStudent.name).width / 2;
|
||
|
const y1 = particleCanvas.height / 2 - 50; // 向上移动50像素
|
||
|
particleCtx.fillText(selectedStudent.name, x1, y1);
|
||
|
|
||
|
// 绘制第二行文字
|
||
|
const x2 = particleCanvas.width / 2 - particleCtx.measureText(selectedStudent.id).width / 2;
|
||
|
const y2 = particleCanvas.height / 2 + 70; // 向下移动70像素
|
||
|
particleCtx.fillText(selectedStudent.id, x2, y2);
|
||
|
|
||
|
// 获取整个画布的像素数据
|
||
|
const imageData = particleCtx.getImageData(0, 0, particleCanvas.width, particleCanvas.height);
|
||
|
|
||
|
// 清空粒子数组
|
||
|
particles = [];
|
||
|
|
||
|
// 创建粒子
|
||
|
for (let y = 0; y < imageData.height; y += 2) {
|
||
|
for (let x = 0; x < imageData.width; x += 2) {
|
||
|
const index = (y * imageData.width + x) * 4;
|
||
|
const alpha = imageData.data[index + 3];
|
||
|
if (alpha > 128) { // 只选择不透明的像素
|
||
|
let particle = new Particle(Math.random() * particleCanvas.width, Math.random() * particleCanvas.height);
|
||
|
particle.targetX = x;
|
||
|
particle.targetY = y;
|
||
|
particles.push(particle);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 启动粒子动画
|
||
|
animateParticles();
|
||
|
}
|
||
|
|
||
|
function Particle(x, y) {
|
||
|
this.x = x;
|
||
|
this.y = y;
|
||
|
this.size = 1;
|
||
|
this.baseX = this.x;
|
||
|
this.baseY = this.y;
|
||
|
this.density = Math.random() * 30 + 1;
|
||
|
}
|
||
|
|
||
|
Particle.prototype.draw = function () {
|
||
|
particleCtx.fillStyle = 'black';
|
||
|
particleCtx.beginPath();
|
||
|
particleCtx.arc(this.x, this.y, this.size, 0, Math.PI * 2); // 绘制粒子
|
||
|
particleCtx.closePath();
|
||
|
particleCtx.fill();
|
||
|
};
|
||
|
|
||
|
Particle.prototype.update = function () {
|
||
|
let dx = this.targetX - this.x;
|
||
|
let dy = this.targetY - this.y;
|
||
|
let distance = Math.sqrt(dx * dx + dy * dy);
|
||
|
let speed = distance / 20; // 移动速度,离目标越远速度越快
|
||
|
this.x += dx / distance * speed;
|
||
|
this.y += dy / distance * speed;
|
||
|
|
||
|
// 减少抖动,当粒子非常接近目标位置时停止移动
|
||
|
if (distance < 1) {
|
||
|
this.x = this.targetX;
|
||
|
this.y = this.targetY;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function animateParticles() {
|
||
|
particleCtx.clearRect(0, 0, particleCanvas.width, particleCanvas.height); // 清除上一帧的粒子
|
||
|
|
||
|
particles.forEach(particle => {
|
||
|
particle.update(); // 更新每个粒子的状态
|
||
|
particle.draw(); // 绘制每个粒子
|
||
|
});
|
||
|
|
||
|
requestAnimationFrame(animateParticles); // 循环动画
|
||
|
}
|
||
|
|
||
|
function showMessage(text) {
|
||
|
const messageDiv = document.getElementById("message");
|
||
|
messageDiv.textContent = text;
|
||
|
messageDiv.style.display = "block"; // 显示消息
|
||
|
|
||
|
setTimeout(() => {
|
||
|
messageDiv.style.display = "none"; // 1秒后隐藏消息
|
||
|
}, 1000);
|
||
|
}
|
||
|
|
||
|
window.onload = function() {
|
||
|
const storedStudent = localStorage.getItem('selectedStudent');//获取selectedStudent
|
||
|
if (storedStudent) {
|
||
|
selectedStudent = JSON.parse(storedStudent);
|
||
|
document.getElementById('selectedStudent').innerText = `${selectedStudent.name} (学号: ${selectedStudent.id})`;
|
||
|
autoPointsAdded = localStorage.getItem('autoPointsAdded') === 'true';
|
||
|
manualPointsUsed = localStorage.getItem('manualPointsUsed') === 'true';
|
||
|
starLock = localStorage.getItem('starLock') === 'true';
|
||
|
onClassAdded = localStorage.getItem('onClassAdded') === 'true';
|
||
|
}
|
||
|
toggleButtons();//按键锁
|
||
|
};
|
||
|
|
||
|
function uploadFile() {
|
||
|
const input = document.getElementById('fileInput');
|
||
|
const file = input.files[0];
|
||
|
if (!file) {
|
||
|
showMessage("请选择一个文件!");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const reader = new FileReader();
|
||
|
reader.onload = function(e) {
|
||
|
const data = new Uint8Array(e.target.result);
|
||
|
const workbook = XLSX.read(data, { type: 'array' });
|
||
|
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
|
||
|
|
||
|
// 读取所有行(包括空行)
|
||
|
let allStudents = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
|
||
|
|
||
|
// 如果没有数据,则提示用户
|
||
|
if (allStudents.length < 2) {
|
||
|
showMessage("工作表中没有有效数据!");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// 去掉表头
|
||
|
allStudents = allStudents.slice(1);
|
||
|
|
||
|
// 过滤有效行,确保每行的有效性
|
||
|
students = allStudents
|
||
|
.filter(row => row[0] !== undefined && row[0] !== null && row[0] !== '') // 确保 ID 不为空
|
||
|
.map(row => ({
|
||
|
id: row[0],
|
||
|
name: row[1], // 假设第二列为名字
|
||
|
points: 0 // 默认分数为 0
|
||
|
}));
|
||
|
|
||
|
// 如果没有有效的学生数据,则提示
|
||
|
if (students.length === 0) {
|
||
|
showMessage("工作表中没有有效学生数据!");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
localStorage.setItem('students', JSON.stringify(students));
|
||
|
showMessage("学生名单导入成功!");
|
||
|
toggleButtons();
|
||
|
};
|
||
|
reader.readAsArrayBuffer(file);
|
||
|
}
|
||
|
|
||
|
function updateFileName() {
|
||
|
const input = document.getElementById('fileInput');
|
||
|
const fileName = document.getElementById('fileName');
|
||
|
if (input.files.length > 0) {
|
||
|
fileName.innerText = input.files[0].name; // 显示选择的文件名
|
||
|
} else {
|
||
|
fileName.innerText = '未选择文件'; // 显示未选择文件的提示
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function toggleButtons() {
|
||
|
const buttons = ['drawButton', 'correctButton', 'incorrectButton', 'manualButton'];
|
||
|
const enable = students.length > 0;
|
||
|
buttons.forEach(buttonId => {
|
||
|
document.getElementById(buttonId).disabled = !enable;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function viewStudentList() {
|
||
|
window.location.href = 'studentList.html';
|
||
|
}
|
||
|
|
||
|
function startDraw() {
|
||
|
if (students.length === 0) {
|
||
|
showMessage("请先导入学生名单!");
|
||
|
return;
|
||
|
}
|
||
|
// 隐藏页面元素
|
||
|
document.querySelector('.container').classList.add('hidden');
|
||
|
// 隐藏“开始点名”按钮
|
||
|
//document.getElementById('drawButton').style.display = 'none';
|
||
|
|
||
|
lock = false;
|
||
|
lock1 = false;
|
||
|
onClassAdded = false;
|
||
|
autoPointsAdded = false;
|
||
|
manualPointsUsed = false;
|
||
|
starLock = false;
|
||
|
|
||
|
|
||
|
const totalPoints = students.reduce((sum, student) => sum + (student.points > 10 ? 1 / student.points : 10 - student.points), 0);
|
||
|
const randomValue = Math.random() * totalPoints;
|
||
|
let cumulativePoints = 0;
|
||
|
|
||
|
for (const student of students) {
|
||
|
cumulativePoints += Math.max(1, 10 - student.points);
|
||
|
if (randomValue < cumulativePoints) {
|
||
|
selectedStudent = student;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
setTimeout(scrollStudents, 350);
|
||
|
}
|
||
|
|
||
|
function onClass() {
|
||
|
if(!onClassAdded && selectedStudent) {
|
||
|
selectedStudent.points += 1;
|
||
|
synPoints();
|
||
|
onClassAdded = true;
|
||
|
updateLocalStorage();
|
||
|
showMessage(`${selectedStudent.name}到场,加分 1。`);
|
||
|
} else if (!selectedStudent) {
|
||
|
showMessage("请先抽取一位学生");
|
||
|
} else {
|
||
|
showMessage("到场已加分");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
document.addEventListener('click', async function(event) {
|
||
|
const drawButton = document.getElementById('drawButton');
|
||
|
const scrollingNamesDiv = document.getElementById('scrollingNames');
|
||
|
const selectedStudentDiv = document.getElementById('selectedStudent');
|
||
|
|
||
|
if(!clickEnabled){
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!drawButton.contains(event.target) &&
|
||
|
!scrollingNamesDiv.contains(event.target) &&
|
||
|
!selectedStudentDiv.contains(event.target)) {
|
||
|
|
||
|
// 清空显示的被点到学生信息
|
||
|
scrollingNamesDiv.innerHTML = '';
|
||
|
|
||
|
particles = [];
|
||
|
particleCtx.clearRect(0, 0, particleCanvas.width, particleCanvas.height);
|
||
|
|
||
|
// 恢复页面元素
|
||
|
document.querySelector('.container').classList.remove('hidden');
|
||
|
selectedStudentDiv.style.display = 'block'; // 显示
|
||
|
}
|
||
|
});
|
||
|
|
||
|
function synPoints() {
|
||
|
// 更新 students 数组中的相应学生积分
|
||
|
const studentIndex = students.findIndex(s => s.id === selectedStudent.id);
|
||
|
if (studentIndex !== -1) {
|
||
|
students[studentIndex].points = selectedStudent.points; // 确保 students 数组中的积分也更新
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function repeatCorrectly() {
|
||
|
if (!lock && selectedStudent && !autoPointsAdded) {
|
||
|
selectedStudent.points += 0.5;
|
||
|
synPoints();
|
||
|
autoPointsAdded = true;
|
||
|
lock = true;
|
||
|
updateLocalStorage();
|
||
|
showMessage(`已为 ${selectedStudent.name}加分 0.5。`);
|
||
|
} else if (!selectedStudent) {
|
||
|
showMessage("请先抽取一位学生");
|
||
|
} else {
|
||
|
showMessage("已为该学生加过分");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
function repeatIncorrectly() {
|
||
|
if (!lock && selectedStudent && !autoPointsAdded) {
|
||
|
selectedStudent.points -= 1;
|
||
|
synPoints();
|
||
|
autoPointsAdded = true;
|
||
|
lock = true;
|
||
|
updateLocalStorage();
|
||
|
showMessage(`已为 ${selectedStudent.name} 扣分 1。`);
|
||
|
} else if (!selectedStudent) {
|
||
|
showMessage("请先抽取一位学生");
|
||
|
} else {
|
||
|
showMessage("已为该学生加过分");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let clickEnabled = true; // 用于跟踪点击是否被允许
|
||
|
|
||
|
function scrollStudents() {
|
||
|
document.getElementById('selectedStudent').innerText = '';
|
||
|
const scrollingNamesDiv = document.getElementById('scrollingNames');
|
||
|
scrollingNamesDiv.innerHTML = '';
|
||
|
let index = 0;
|
||
|
|
||
|
clickEnabled = false; // 禁用点击
|
||
|
|
||
|
return new Promise((resolve) => {
|
||
|
intervalId = setInterval(() => {
|
||
|
scrollingNamesDiv.innerText = students[index].name;
|
||
|
index = (index + 1) % students.length;
|
||
|
}, 100);
|
||
|
|
||
|
setTimeout(() => {
|
||
|
clearInterval(intervalId);
|
||
|
scrollingNamesDiv.innerHTML = '';
|
||
|
if (selectedStudent) {
|
||
|
document.getElementById('selectedStudent').innerText = `${selectedStudent.name} (学号: ${selectedStudent.id})`;
|
||
|
document.getElementById('selectedStudent').style.display = 'none'; // 隐藏
|
||
|
createParticlesFromText(`${selectedStudent.name} (学号:${selectedStudent.id})`);
|
||
|
updateLocalStorage();
|
||
|
}
|
||
|
resolve(); // 完成滚动时解析Promise
|
||
|
clickEnabled = true; // 恢复点击
|
||
|
}, 3000);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function updateLocalStorage() {
|
||
|
localStorage.setItem('selectedStudent', JSON.stringify(selectedStudent));
|
||
|
localStorage.setItem('students', JSON.stringify(students));
|
||
|
localStorage.setItem('autoPointsAdded', autoPointsAdded);
|
||
|
localStorage.setItem('manualPointsUsed', manualPointsUsed);
|
||
|
localStorage.setItem('starLock',starLock);
|
||
|
localStorage.setItem('onClassAdded',onClassAdded);
|
||
|
}
|
||
|
|
||
|
// 初始化星级点击事件监听器
|
||
|
document.querySelectorAll('.rating input').forEach(star => {
|
||
|
star.addEventListener('change', function() {
|
||
|
if (!starLock) { // 检查星星是否已经被点击过
|
||
|
updatePoints(this.value);
|
||
|
} else {
|
||
|
showMessage("每次点名只能点击一次星星进行加分。");
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// 根据选中的星级为学生加分
|
||
|
function updatePoints(points) {
|
||
|
points = parseInt(points); // 将点击的星星值转换为整数
|
||
|
if (selectedStudent) {
|
||
|
if (!starLock) { // 检查星星是否已经被点击过
|
||
|
selectedStudent.points += points; // 根据星星值加分
|
||
|
synPoints();
|
||
|
showMessage(`已为 ${selectedStudent.name} 加分 ${points}。`);
|
||
|
starLock = true; // 点击后锁定星星
|
||
|
updateLocalStorage();
|
||
|
} else {
|
||
|
showMessage("每次点名只能点击一次星星进行加分。");
|
||
|
}
|
||
|
} else {
|
||
|
showMessage("请先点名一个学生。");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 禁用所有星星选择
|
||
|
function disableStars() {
|
||
|
document.querySelectorAll('.rating input').forEach(star => {
|
||
|
star.disabled = true;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// 启用所有星星选择
|
||
|
function enableStars() {
|
||
|
document.querySelectorAll('.rating input').forEach(star => {
|
||
|
star.disabled = false;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
|
||
|
function resetAllPoints() {
|
||
|
students.forEach(student => student.points = 0);
|
||
|
localStorage.setItem('students', JSON.stringify(students));
|
||
|
localStorage.removeItem('selectedStudent');
|
||
|
selectedStudent = null;
|
||
|
document.getElementById('selectedStudent').innerText = '';
|
||
|
showMessage("所有积分已重置!");
|
||
|
}
|
||
|
</script>
|
||
|
</body>
|
||
|
</html>
|