Migrate project from typing_practiceweb
This commit is contained in:
657
public/xf/xf.html
Normal file
657
public/xf/xf.html
Normal file
@@ -0,0 +1,657 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>消防站点选址优化</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
flex: 2;
|
||||
min-width: 500px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
position: relative;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background-color: #f9f9f9;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
background-color: white;
|
||||
cursor: crosshair;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background-color: #e8f4fd;
|
||||
border-left: 4px solid #2196F3;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.instructions ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.instructions li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.settings {
|
||||
background-color: #f0f8ff;
|
||||
border: 1px solid #cce5ff;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
width: 150px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.setting-input {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.setting-note {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.primary-btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
|
||||
.info-btn {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.info-btn:hover {
|
||||
background-color: #0b7dda;
|
||||
}
|
||||
|
||||
.warning-btn {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.warning-btn:hover {
|
||||
background-color: #e68900;
|
||||
}
|
||||
|
||||
.warning-btn:disabled {
|
||||
background-color: #cccccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.stations-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.station-item {
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.station-number {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: #ff6b6b;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.coordinates {
|
||||
font-family: monospace;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.left-panel, .right-panel {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>消防站点选址优化</h1>
|
||||
<p class="subtitle">交互式消防站点规划工具</p>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="left-panel">
|
||||
<div class="instructions">
|
||||
<h3>使用说明:</h3>
|
||||
<ol>
|
||||
<li>在画布上<strong>单击右键</strong>放置消防站点</li>
|
||||
<li><strong>按住左键拖动</strong>可以在地图上绘制区域</li>
|
||||
<li>使用下方按钮清除站点或绘图</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="settings">
|
||||
<h3>站点设置</h3>
|
||||
<div class="setting-item">
|
||||
<label class="setting-label">最大站点数量:</label>
|
||||
<input type="number" id="maxStationsInput" class="setting-input" min="1" max="5" value="2">
|
||||
<span class="setting-note">(1-5)</span>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">当前站点数量:</span>
|
||||
<span id="currentStationsCount">0</span>
|
||||
<span class="setting-note">/ <span id="maxStationsDisplay">2</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="warning-btn" id="undoBtn" disabled>撤销站点</button>
|
||||
<button class="secondary-btn" id="clearStationsBtn">清除站点</button>
|
||||
<button class="secondary-btn" id="clearDrawingBtn">清除绘图</button>
|
||||
<button class="info-btn" id="resetBtn">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="stations-list">
|
||||
<h3>已放置站点</h3>
|
||||
<div id="stationsContainer">
|
||||
<p id="noStationsMsg">尚未放置任何站点</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<div class="canvas-container">
|
||||
<canvas id="mainCanvas" width="800" height="600"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>消防站点选址优化工具 | 可视化消防站点布局规划</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 获取DOM元素
|
||||
const canvas = document.getElementById('mainCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const clearStationsBtn = document.getElementById('clearStationsBtn');
|
||||
const clearDrawingBtn = document.getElementById('clearDrawingBtn');
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
const undoBtn = document.getElementById('undoBtn');
|
||||
const stationsContainer = document.getElementById('stationsContainer');
|
||||
const noStationsMsg = document.getElementById('noStationsMsg');
|
||||
const maxStationsInput = document.getElementById('maxStationsInput');
|
||||
const currentStationsCount = document.getElementById('currentStationsCount');
|
||||
const maxStationsDisplay = document.getElementById('maxStationsDisplay');
|
||||
|
||||
// 状态变量
|
||||
let stations = [];
|
||||
let isDrawing = false;
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
let drawingData = [];
|
||||
let backgroundImage = null;
|
||||
let maxStations = 2;
|
||||
|
||||
// 初始化画布
|
||||
initializeCanvas();
|
||||
|
||||
// 画布鼠标事件处理
|
||||
canvas.addEventListener('mousedown', startDrawing);
|
||||
canvas.addEventListener('mousemove', draw);
|
||||
canvas.addEventListener('mouseup', stopDrawing);
|
||||
canvas.addEventListener('mouseout', stopDrawing);
|
||||
|
||||
// 右键放置站点
|
||||
canvas.addEventListener('contextmenu', function(e) {
|
||||
e.preventDefault(); // 阻止默认右键菜单
|
||||
|
||||
if (stations.length < maxStations) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// 添加新站点
|
||||
stations.push({x, y});
|
||||
drawStation(x, y, stations.length);
|
||||
updateStationsList();
|
||||
updateStationsCount();
|
||||
updateUndoButton();
|
||||
} else {
|
||||
alert(`已达到最大站点数量限制(${maxStations}个)`);
|
||||
}
|
||||
});
|
||||
|
||||
// 最大站点数量输入事件
|
||||
maxStationsInput.addEventListener('change', function() {
|
||||
let value = parseInt(this.value);
|
||||
|
||||
if (isNaN(value) || value < 1) {
|
||||
value = 1;
|
||||
} else if (value > 5) {
|
||||
value = 5;
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
maxStations = value;
|
||||
maxStationsDisplay.textContent = value;
|
||||
|
||||
// 如果当前站点数量超过新的最大值,移除多余的站点
|
||||
if (stations.length > maxStations) {
|
||||
stations = stations.slice(0, maxStations);
|
||||
redrawCanvas();
|
||||
updateStationsList();
|
||||
updateStationsCount();
|
||||
updateUndoButton();
|
||||
}
|
||||
});
|
||||
|
||||
// 按钮事件处理
|
||||
clearStationsBtn.addEventListener('click', clearStations);
|
||||
clearDrawingBtn.addEventListener('click', clearDrawing);
|
||||
resetBtn.addEventListener('click', resetAll);
|
||||
undoBtn.addEventListener('click', undoLastStation);
|
||||
|
||||
// 初始化画布
|
||||
function initializeCanvas() {
|
||||
// 创建背景图片
|
||||
backgroundImage = new Image();
|
||||
backgroundImage.onload = function() {
|
||||
// 调整画布大小以适应图片
|
||||
canvas.width = backgroundImage.width;
|
||||
canvas.height = backgroundImage.height;
|
||||
|
||||
// 绘制背景图片
|
||||
ctx.drawImage(backgroundImage, 0, 0);
|
||||
};
|
||||
|
||||
// 设置背景图片源 - 这里使用一个示例地图
|
||||
// 在实际应用中,您可以将此URL替换为您本地图片的路径
|
||||
backgroundImage.src = '背景图.png';
|
||||
|
||||
// 如果背景图片加载失败,使用默认背景
|
||||
backgroundImage.onerror = function() {
|
||||
canvas.width = 800;
|
||||
canvas.height = 600;
|
||||
|
||||
// 设置画布背景
|
||||
ctx.fillStyle = '#e9f5ff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 绘制网格
|
||||
drawGrid();
|
||||
|
||||
// 绘制标题
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.font = 'bold 24px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('消防站点选址优化', canvas.width/2, 40);
|
||||
|
||||
// 绘制说明
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillText('右键放置消防站点 | 左键拖动绘制区域', canvas.width/2, canvas.height - 30);
|
||||
};
|
||||
}
|
||||
|
||||
// 绘制网格
|
||||
function drawGrid() {
|
||||
ctx.strokeStyle = '#d0e0f0';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
// 绘制垂直线
|
||||
for (let x = 0; x <= canvas.width; x += 50) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, canvas.height);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// 绘制水平线
|
||||
for (let y = 0; y <= canvas.height; y += 50) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(canvas.width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// 绘图函数
|
||||
function startDrawing(e) {
|
||||
// 只响应左键点击
|
||||
if (e.button !== 0) return;
|
||||
|
||||
isDrawing = true;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
lastX = e.clientX - rect.left;
|
||||
lastY = e.clientY - rect.top;
|
||||
|
||||
// 开始新的绘图路径
|
||||
drawingData.push([]);
|
||||
drawingData[drawingData.length - 1].push({x: lastX, y: lastY});
|
||||
}
|
||||
|
||||
function draw(e) {
|
||||
if (!isDrawing) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// 绘制线条 - 修改为灰色 (#888888)
|
||||
// 如需修改绘图颜色,请更改此处的strokeStyle值
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(lastX, lastY);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.strokeStyle = '#888888'; // 绘图颜色 - 灰色
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.stroke();
|
||||
|
||||
// 保存绘图数据
|
||||
drawingData[drawingData.length - 1].push({x, y});
|
||||
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
}
|
||||
|
||||
function stopDrawing() {
|
||||
isDrawing = false;
|
||||
}
|
||||
|
||||
// 绘制消防站点 - 修改为三角形内显示数字
|
||||
function drawStation(x, y, number) {
|
||||
// 三角形边长 - 增大尺寸
|
||||
const sideLength = 30; // 增大三角形尺寸
|
||||
const height = sideLength * Math.sqrt(3) / 2;
|
||||
|
||||
// 计算三角形顶点坐标(等边三角形,顶点朝上)
|
||||
const topX = x;
|
||||
const topY = y - height / 2;
|
||||
const leftX = x - sideLength / 2;
|
||||
const leftY = y + height / 2;
|
||||
const rightX = x + sideLength / 2;
|
||||
const rightY = y + height / 2;
|
||||
|
||||
// 绘制三角形
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(topX, topY);
|
||||
ctx.lineTo(leftX, leftY);
|
||||
ctx.lineTo(rightX, rightY);
|
||||
ctx.closePath();
|
||||
|
||||
// 填充三角形
|
||||
ctx.fillStyle = '#ff6b6b';
|
||||
ctx.fill();
|
||||
|
||||
// 绘制三角形边框
|
||||
ctx.strokeStyle = 'white';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// 绘制站点编号 - 位置下移,确保数字清晰可见
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = 'bold 14px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(number, x, y + 2); // 将数字下移2像素
|
||||
}
|
||||
|
||||
// 更新站点列表
|
||||
function updateStationsList() {
|
||||
if (stations.length === 0) {
|
||||
noStationsMsg.style.display = 'block';
|
||||
stationsContainer.innerHTML = '';
|
||||
stationsContainer.appendChild(noStationsMsg);
|
||||
} else {
|
||||
noStationsMsg.style.display = 'none';
|
||||
|
||||
let html = '';
|
||||
stations.forEach((station, index) => {
|
||||
html += `
|
||||
<div class="station-item">
|
||||
<div>
|
||||
<span class="station-number">${index + 1}</span>
|
||||
站点 ${index + 1}
|
||||
</div>
|
||||
<div class="coordinates">(${Math.round(station.x)}, ${Math.round(station.y)})</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
stationsContainer.innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新站点计数
|
||||
function updateStationsCount() {
|
||||
currentStationsCount.textContent = stations.length;
|
||||
}
|
||||
|
||||
// 更新撤销按钮状态
|
||||
function updateUndoButton() {
|
||||
undoBtn.disabled = stations.length === 0;
|
||||
}
|
||||
|
||||
// 撤销最后一个站点
|
||||
function undoLastStation() {
|
||||
if (stations.length > 0) {
|
||||
stations.pop();
|
||||
redrawCanvas();
|
||||
updateStationsList();
|
||||
updateStationsCount();
|
||||
updateUndoButton();
|
||||
}
|
||||
}
|
||||
|
||||
// 清除站点
|
||||
function clearStations() {
|
||||
stations = [];
|
||||
redrawCanvas();
|
||||
updateStationsList();
|
||||
updateStationsCount();
|
||||
updateUndoButton();
|
||||
}
|
||||
|
||||
// 清除绘图
|
||||
function clearDrawing() {
|
||||
drawingData = [];
|
||||
redrawCanvas();
|
||||
}
|
||||
|
||||
// 重置所有
|
||||
function resetAll() {
|
||||
stations = [];
|
||||
drawingData = [];
|
||||
maxStationsInput.value = 2;
|
||||
maxStations = 2;
|
||||
maxStationsDisplay.textContent = 2;
|
||||
redrawCanvas();
|
||||
updateStationsList();
|
||||
updateStationsCount();
|
||||
updateUndoButton();
|
||||
}
|
||||
|
||||
// 重绘画布
|
||||
function redrawCanvas() {
|
||||
// 清除画布
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 重新绘制背景
|
||||
if (backgroundImage && backgroundImage.complete) {
|
||||
ctx.drawImage(backgroundImage, 0, 0);
|
||||
} else {
|
||||
// 设置画布背景
|
||||
ctx.fillStyle = '#e9f5ff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 绘制网格
|
||||
drawGrid();
|
||||
|
||||
// 绘制标题
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.font = 'bold 24px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('消防站点选址优化', canvas.width/2, 40);
|
||||
|
||||
// 绘制说明
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillText('右键放置消防站点 | 左键拖动绘制区域', canvas.width/2, canvas.height - 30);
|
||||
}
|
||||
|
||||
// 重绘所有绘图 - 修改为灰色 (#888888)
|
||||
drawingData.forEach(path => {
|
||||
if (path.length > 1) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(path[0].x, path[0].y);
|
||||
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
ctx.lineTo(path[i].x, path[i].y);
|
||||
}
|
||||
|
||||
ctx.strokeStyle = '#888888'; // 绘图颜色 - 灰色
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
|
||||
// 重绘所有站点
|
||||
stations.forEach((station, index) => {
|
||||
drawStation(station.x, station.y, index + 1);
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化站点计数和撤销按钮状态
|
||||
updateStationsCount();
|
||||
updateUndoButton();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
public/xf/背景图.png
Normal file
BIN
public/xf/背景图.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
Reference in New Issue
Block a user