LiquidTempLab/LiquidTempLab.html
2025-03-04 17:54:18 +08:00

578 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>热水和冷牛奶温度变化折线图</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
}
.main-container {
display: flex;
flex-direction: row;
height: 100vh;
width: 100vw;
overflow: hidden;
}
.chart-section {
width: 66.67%;
height: 100%;
padding: 10px;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
}
.controls-section {
width: 33.33%;
height: 100%;
padding: 10px;
box-sizing: border-box;
overflow-y: auto;
background-color: #f9f9f9;
border-left: 1px solid #ddd;
}
.chart-container {
flex: 1;
width: 100%;
position: relative;
margin-bottom: 10px;
}
.title-container {
padding: 10px 0;
}
.axis-controls {
display: flex;
flex-direction: column;
margin: 10px 0;
padding: 10px;
background: #f5f5f5;
border-radius: 5px;
width: 100%;
box-sizing: border-box;
transition: max-height 0.3s ease-out;
overflow: hidden;
}
.axis-controls.collapsed {
max-height: 0;
padding: 0;
margin: 0;
}
.toggle-button {
background: #4CAF50;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
width: 100%;
text-align: left;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.toggle-button:hover {
background: #45a049;
}
.toggle-button .arrow {
transition: transform 0.3s ease;
}
.toggle-button.collapsed .arrow {
transform: rotate(-90deg);
}
.axis-control-group {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 10px;
width: 100%;
}
.axis-label {
font-weight: bold;
margin-bottom: 5px;
width: 100%;
text-align: left;
}
.axis-inputs {
display: flex;
align-items: center;
width: 100%;
}
input {
padding: 8px;
font-size: 14px;
width: 70px;
margin: 0 5px;
border: 1px solid #ccc;
border-radius: 4px;
}
.record-section {
width: 100%;
margin-top: 10px;
}
.input-controls {
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: 15px;
}
.input-group {
display: flex;
align-items: center;
margin: 5px 0;
width: 100%;
}
.input-group label {
flex: 1;
text-align: left;
}
.input-group input {
flex: 1;
max-width: 120px;
}
.buttons-container {
display: flex;
justify-content: space-between;
margin: 10px 0;
}
.record-list {
list-style-type: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 5px;
max-height: 40vh;
overflow-y: auto;
width: 100%;
}
.record-list li {
cursor: pointer;
padding: 8px 10px;
background: #f0f0f0;
border-radius: 5px;
transition: background 0.2s;
text-align: left;
font-size: 14px;
}
.record-list li:hover {
background: #ddd;
}
button {
padding: 8px 15px;
font-size: 14px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
flex: 1;
margin: 0 5px;
}
button:hover {
background: #45a049;
}
button.clear {
background: #f44336;
}
button.clear:hover {
background: #d32f2f;
}
.error-message {
color: #f44336;
font-size: 14px;
margin: 5px 0;
min-height: 20px;
}
.section-title {
margin: 10px 0;
text-align: left;
border-bottom: 1px solid #ddd;
padding-bottom: 5px;
}
/* 响应式设计 - 纵向屏幕 */
@media (max-width: 768px) {
.main-container {
flex-direction: column;
height: auto;
overflow: auto;
}
.chart-section {
width: 100%;
height: 60vh;
min-height: 300px;
}
.controls-section {
width: 100%;
height: auto;
border-left: none;
border-top: 1px solid #ddd;
}
.record-list {
max-height: 30vh;
}
.axis-inputs {
width: 100%;
justify-content: flex-start;
}
.axis-inputs span {
margin: 0 10px;
}
}
</style>
</head>
<body>
<div class="main-container">
<div class="chart-section">
<div class="title-container">
<h1>热水和冷牛奶温度变化折线图</h1>
</div>
<div class="chart-container">
<canvas id="temperatureChart"></canvas>
</div>
</div>
<div class="controls-section">
<h2 class="section-title">坐标轴设置</h2>
<button class="toggle-button collapsed" onclick="toggleAxisControls()">
坐标轴设置
<span class="arrow"></span>
</button>
<div class="axis-controls collapsed" id="axisControls">
<div class="axis-control-group">
<div class="axis-label">X轴范围 (分钟)</div>
<div class="axis-inputs">
<input type="number" id="xMin" placeholder="最小值" min="0" oninput="validateAxes()">
<span>-</span>
<input type="number" id="xMax" placeholder="最大值" min="0" oninput="validateAxes()">
</div>
</div>
<div class="axis-control-group">
<div class="axis-label">Y轴范围 (温度℃)</div>
<div class="axis-inputs">
<input type="number" id="yMin" placeholder="最小值" oninput="validateAxes()">
<span>-</span>
<input type="number" id="yMax" placeholder="最大值" oninput="validateAxes()">
</div>
</div>
</div>
<div class="error-message" id="axisError"></div>
<div class="record-section">
<h2 class="section-title">实验数据记录</h2>
<div class="input-controls">
<div class="input-group">
<label>时间(分钟):</label>
<input type="number" id="timeInput" value="0" min="0" step="1">
</div>
<div class="input-group">
<label>热水温度(℃):</label>
<input type="number" id="hotWaterInput" step="0.1">
</div>
<div class="input-group">
<label>冷牛奶温度(℃):</label>
<input type="number" id="coldMilkInput" step="0.1">
</div>
</div>
<div class="error-message" id="inputError"></div>
<div class="buttons-container">
<button onclick="addRecord()">添加数据</button>
<button class="clear" onclick="clearRecords()">清空数据</button>
</div>
<h3 class="section-title">已记录数据</h3>
<ul id="recordList" class="record-list"></ul>
</div>
</div>
</div>
<script>
const ctx = document.getElementById('temperatureChart').getContext('2d');
let chart;
let timeValues = [], hotWaterValues = [], coldMilkValues = [];
// 验证坐标轴值
function validateAxes() {
const xMin = document.getElementById('xMin').value;
const xMax = document.getElementById('xMax').value;
const yMin = document.getElementById('yMin').value;
const yMax = document.getElementById('yMax').value;
const axisError = document.getElementById('axisError');
axisError.textContent = '';
if (xMin && xMax && Number(xMin) >= Number(xMax)) {
axisError.textContent = 'X轴最小值必须小于最大值';
return false;
}
if (yMin && yMax && Number(yMin) >= Number(yMax)) {
axisError.textContent = 'Y轴最小值必须小于最大值';
return false;
}
if (xMin && Number(xMin) < 0) {
axisError.textContent = 'X轴最小值不能小于0';
document.getElementById('xMin').value = 0;
return false;
}
updateChart();
return true;
}
function addRecord() {
const timeInput = document.getElementById('timeInput');
const hotWaterInput = document.getElementById('hotWaterInput');
const coldMilkInput = document.getElementById('coldMilkInput');
const inputError = document.getElementById('inputError');
const time = Math.round(Number(timeInput.value)); // 确保时间为整数
const hotWater = Number(hotWaterInput.value);
const coldMilk = Number(coldMilkInput.value);
// 重置错误消息
inputError.textContent = '';
// 验证输入
if (isNaN(time) || isNaN(hotWater) || isNaN(coldMilk)) {
inputError.textContent = '请输入有效的数字';
return;
}
if (time < 0) {
inputError.textContent = '时间不能为负值';
return;
}
// 添加数据
timeValues.push(time);
hotWaterValues.push(hotWater);
coldMilkValues.push(coldMilk);
// 自动排序
sortRecords();
// 更新输入框的默认值
timeInput.value = time + 1; // 增加整数分钟
hotWaterInput.value = '';
coldMilkInput.value = '';
updateChart();
}
function removeRecord(element) {
if (confirm('确定要删除这条记录吗?')) {
const index = parseInt(element.getAttribute('data-index'));
if (index > -1) {
timeValues.splice(index, 1);
hotWaterValues.splice(index, 1);
coldMilkValues.splice(index, 1);
}
element.remove();
updateRecordIndices();
updateChart();
}
}
function updateRecordIndices() {
const items = document.querySelectorAll('#recordList li');
items.forEach((item, index) => {
item.setAttribute('data-index', index);
});
}
function clearRecords() {
if (confirm('确定要清空所有数据吗?')) {
timeValues = [];
hotWaterValues = [];
coldMilkValues = [];
document.getElementById('recordList').innerHTML = '';
document.getElementById('timeInput').value = '0';
updateChart();
}
}
function sortRecords() {
// 获取所有的排序索引
let sortedIndices = timeValues.map((_, i) => i).sort((a, b) => timeValues[a] - timeValues[b]);
// 更新排序后的数据
let newTimes = sortedIndices.map(i => timeValues[i]);
let newHotWater = sortedIndices.map(i => hotWaterValues[i]);
let newColdMilk = sortedIndices.map(i => coldMilkValues[i]);
// 替换原数组
timeValues = newTimes;
hotWaterValues = newHotWater;
coldMilkValues = newColdMilk;
// 更新显示的列表
const recordList = document.getElementById('recordList');
recordList.innerHTML = '';
for (let i = 0; i < timeValues.length; i++) {
const listItem = document.createElement('li');
listItem.textContent = `时间: ${timeValues[i]}分钟, 热水: ${hotWaterValues[i].toFixed(1)}℃, 牛奶: ${coldMilkValues[i].toFixed(1)}`;
listItem.setAttribute('data-time', timeValues[i]);
listItem.setAttribute('data-hot', hotWaterValues[i]);
listItem.setAttribute('data-cold', coldMilkValues[i]);
listItem.setAttribute('data-index', i);
listItem.onclick = function() { removeRecord(this); };
recordList.appendChild(listItem);
}
}
function updateChart() {
let xMin = document.getElementById('xMin').value;
let xMax = document.getElementById('xMax').value;
let yMin = document.getElementById('yMin').value;
let yMax = document.getElementById('yMax').value;
// 设置默认值和动态范围
xMin = xMin ? Number(xMin) : 0; // 时间最小值始终为0或更大
xMax = xMax ? Number(xMax) : (timeValues.length ? Math.max(...timeValues) + 1 : 10);
let allTemps = [...hotWaterValues, ...coldMilkValues].filter(t => !isNaN(t));
let tempMin = allTemps.length ? Math.min(...allTemps) : 0;
let tempMax = allTemps.length ? Math.max(...allTemps) : 50;
// 为了更好的视觉效果,给温度范围添加一些余量
tempMin = Math.floor(tempMin - 5);
tempMax = Math.ceil(tempMax + 5);
yMin = yMin ? Number(yMin) : tempMin;
yMax = yMax ? Number(yMax) : tempMax;
const data = {
labels: timeValues,
datasets: [
{
label: '热水温度(℃)',
data: timeValues.map((time, i) => ({x: time, y: hotWaterValues[i]})),
borderColor: 'red',
backgroundColor: 'rgba(255, 0, 0, 0.1)',
borderWidth: 2,
fill: false,
tension: 0.2,
pointRadius: 4,
pointHoverRadius: 6
},
{
label: '冷牛奶温度(℃)',
data: timeValues.map((time, i) => ({x: time, y: coldMilkValues[i]})),
borderColor: 'blue',
backgroundColor: 'rgba(0, 0, 255, 0.1)',
borderWidth: 2,
fill: false,
tension: 0.2,
pointRadius: 4,
pointHoverRadius: 6
}
]
};
if (chart) {
chart.destroy();
}
chart = new Chart(ctx, {
type: 'line',
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '热水和冷牛奶温度随时间变化',
font: {
size: 16
}
},
tooltip: {
mode: 'index',
intersect: false
},
legend: {
position: 'top',
labels: {
boxWidth: 40,
padding: 10
}
}
},
scales: {
x: {
type: 'linear',
title: {
display: true,
text: '时间(分钟)',
font: {
weight: 'bold'
}
},
min: xMin,
max: xMax,
ticks: {
stepSize: 1
}
},
y: {
type: 'linear',
title: {
display: true,
text: '温度(℃)',
font: {
weight: 'bold'
}
},
min: yMin,
max: yMax
}
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
},
animation: {
duration: 500
}
}
});
}
// 页面加载完成后初始化图表
document.addEventListener('DOMContentLoaded', function() {
updateChart();
});
// 屏幕大小变化时重新调整图表
window.addEventListener('resize', function() {
updateChart();
});
// 控制坐标轴设置区域的显示/隐藏
function toggleAxisControls() {
const controls = document.getElementById('axisControls');
const button = document.querySelector('.toggle-button');
controls.classList.toggle('collapsed');
button.classList.toggle('collapsed');
}
</script>
</body>
</html>