Migrate project from typing_practiceweb

This commit is contained in:
2026-05-14 16:04:14 +08:00
parent dbc5425706
commit 0e87d5546d
225 changed files with 92811 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
{
"firstSectionTitle": "AI教育常见问题解答",
"firstSectionSubtitle": "智慧教学实践指南",
"firstSectionCount": 4,
"secondSectionTitle": "第一课堂AI平台",
"secondSectionSubtitle": "智慧教学的得力助手,创新学习的引航明灯",
"secondSectionCount": 4,
"thirdSectionTitle": "第一课堂课程中心",
"thirdSectionSubtitle": "智慧教育赋能,创新教学提效",
"thirdSectionCount": 4,
"qaContent": [
{
"question": "中小学老师应该怎么利用AI?",
"answer": "1. 备课助手利用AI生成教案大纲、课件素材和教学建议\n2. 个性化辅导:根据学生学习数据制定针对性教学策略\n3. 作业批改使用AI辅助批改作业提供详细的错误分析\n4. 教学反馈通过AI分析课堂数据优化教学方法",
"category": "教师指南"
},
{
"question": "如何引导学生正确使用AI工具",
"answer": "1. 强调AI是辅助工具不是替代思考的工具\n2. 教导学生验证AI输出的准确性\n3. 培养学生的信息素养和批判性思维\n4. 设置合理的AI使用边界和规范",
"category": "学生指导"
},
{
"question": "AI如何促进因材施教",
"answer": "1. 精准识别学生知识掌握程度\n2. 自动生成适应性练习题\n3. 提供个性化学习路径建议\n4. 实时跟踪学习进度并调整教学策略",
"category": "教学策略"
},
{
"question": "如何将AI融入课堂教学",
"answer": "1. 使用AI进行实时语言翻译和解释\n2. 创建交互式教学内容\n3. 利用AI进行课堂提问和讨论引导\n4. 应用AI辅助教学评估和反馈",
"category": "课堂应用"
},
{
"question": "第一课堂AI平台有哪些特色功能",
"answer": "1. 多模型集成整合了deepseek、阿里千问、智谙AI等主流大模型\n2. 智能教学助手:为教师提供备课、出题、批改作业等全方位支持\n3. 个性化学习:根据学生水平推荐适合的学习内容和练习\n4. 实时互动:支持师生在线交流和即时答疑",
"category": "平台介绍"
},
{
"question": "第一课堂AI平台如何帮助教师",
"answer": "1. 自动生成教案和课件,节省备课时间\n2. 智能分析学生作业和考试数据,掌握学情\n3. 提供个性化教学建议和教学策略优化方案\n4. 协助设计多样化的课堂活动和互动环节",
"category": "教师服务"
},
{
"question": "学生如何充分利用第一课堂AI平台",
"answer": "1. 获取个性化学习建议和解答\n2. 利用AI辅助完成作业和自主学习\n3. 通过智能练习系统巩固知识点\n4. 使用AI工具进行学习规划和时间管理",
"category": "学生服务"
},
{
"question": "第一课堂AI平台如何保障教学质量",
"answer": "1. 智能监测系统实时跟踪学习效果\n2. 定期更新优化AI模型确保答案准确性\n3. 专业教研团队把关教学内容质量\n4. 提供详细的学习数据分析和改进建议",
"category": "质量保障"
},
{
"question": "第一课堂课程中心提供哪些特色课程?",
"answer": "1. AI教学应用系列帮助教师掌握AI教学工具的实践应用\n2. 学习方法创新课程指导学生运用AI提升学习效率\n3. 教学设计工作坊助力教师打造AI融合创新课堂\n4. 数字素养提升课程:培养师生适应智能时代的核心能力",
"category": "课程体系"
},
{
"question": "课程中心采用什么样的学习模式?",
"answer": "1. 基于Moodle的专业在线学习平台随时随地便捷学习\n2. 理论与实践相结合,大量实操案例和练习\n3. 专业教师在线指导,促进教学相长\n4. 学习社区互动,分享教学创新经验",
"category": "学习特色"
},
{
"question": "参与课程学习能获得哪些收获?",
"answer": "1. 掌握前沿AI教育工具提升教学效率\n2. 创新教学方法,激发学生学习兴趣\n3. 获得实用的AI教学策略和经验\n4. 加入专业教师成长社群,持续进步",
"category": "课程价值"
},
{
"question": "课程中心如何保障学习效果?",
"answer": "1. 系统的课程规划,循序渐进的学习路径\n2. 及时的技术支持和学习指导\n3. 丰富的教学资源和案例分享\n4. 定期的教研活动和经验交流会",
"category": "学习支持"
}
]
}

14
public/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="代码打字训练应用" />
<title>AI教育· 第一课堂</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

1
public/output.json Normal file

File diff suppressed because one or more lines are too long

4588
public/tower-defense/td-pkg-zh-min.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,347 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=1.0, user-scalable=yes">
<style>
html, body {
margin: 0;
padding: 0;
font-size: 12px;
line-height: 20px;
font-family: Verdana, "Times New Roman", serif;
background: #1A74BA;
}
h1 {
padding: 0;
margin: 0;
line-height: 48px;
font-size: 18px;
font-weight: bold;
font-family: Verdana, "Times New Roman", serif;
letter-spacing: 0.12em;
}
#wrapper {
margin: 0 auto;
max-width: 980px;
}
#td-wrapper {
padding: 8px 24px 24px 24px;
background: #E0F4FC;
}
#td-board {
display: none;
font-size: 16px;
}
#td-board canvas#td-canvas {
position: relative;
background: #fff;
border: solid 1px #cdf;
}
#td-loading {
font-size: 18px;
line-height: 48px;
padding: 60px 0 120px 0;
font-style: italic;
}
</style>
</head>
<body id="tower-defense">
<div id="wrapper">
<div id="td-wrapper">
<div id="td-loading">加载中...</div>
<div id="td-board">
<canvas id="td-canvas">抱歉,您的浏览器不支持 HTML 5 Canvas 标签,请使用 Chrome / Edge 等现代浏览器打开。</canvas>
</div>
</div>
</div>
<script src="td-pkg-zh.js"></script>
<script>
window.onload = function () {
console.log('[init] window.onload called');
// 原站点的初始化方式_TD.init('td-board', true)
_TD.init('td-board', true);
document.getElementById('td-loading').style.display = 'none';
document.getElementById('td-board').style.display = 'block';
};
// --- Monster save/load enhancement ---
(function() {
setTimeout(function() {
if (typeof TD === 'undefined') {
console.error('[MonsterSaveLoad] TD is still undefined after 500ms');
return;
}
console.log('[MonsterSaveLoad] Applying monster save/load enhancement...');
window.__TD_getState = function () {
try {
console.log('[__TD_getState] ===== START SAVING STATE =====');
var scene = TD.stage && TD.stage.current_act && TD.stage.current_act.current_scene;
var map = scene && scene.map;
var buildings = [];
if (map && map.buildings && map.buildings.length) {
for (var i = 0; i < map.buildings.length; i++) {
var b = map.buildings[i];
if (!b || !b.grid) continue;
buildings.push({ type: b.type, mx: b.grid.mx, my: b.grid.my, level: b.level, money: b.money });
}
}
var monsters_data = [];
if (map && map.monsters && map.monsters.length) {
console.log('[__TD_getState] Found ' + map.monsters.length + ' monsters');
for (var i = 0; i < map.monsters.length; i++) {
var m = map.monsters[i];
if (!m || !m.is_valid || !m.grid) continue;
var monster_data = {
idx: m.idx,
difficulty: m.difficulty,
life: m.life,
life0: m.life0,
shield: m.shield,
speed: m.speed,
damage: m.damage,
money: m.money,
mx: m.grid.mx,
my: m.grid.my,
cx: m.cx,
cy: m.cy,
r: m.r,
color: m.color,
toward: m.toward,
way: m.way || [],
next_grid_mx: m.next_grid ? m.next_grid.mx : null,
next_grid_my: m.next_grid ? m.next_grid.my : null,
_dx: m._dx || 0,
_dy: m._dy || 0,
step_level: m.step_level,
render_level: m.render_level,
is_paused: m.is_paused,
is_blocked: m.is_blocked
};
monsters_data.push(monster_data);
}
console.log('[__TD_getState] Gathered ' + monsters_data.length + ' valid monsters data:', monsters_data);
}
var state = {
money: TD.money,
life: TD.life,
score: TD.score,
difficulty: TD.difficulty,
wave_damage: TD.wave_damage,
wave: (scene && scene.wave) || 0,
map_step_level: (map && map.step_level) || 1,
map_render_level: (map && map.render_level) || 2,
buildings: buildings,
monsters: monsters_data
};
console.log('[__TD_getState] Returning state:', state);
return state;
} catch (e) {
console.error('[__TD_getState] error', e);
return null;
}
};
window.__TD_loadState = function (s) {
try {
console.log('[__TD_loadState] ===== START LOADING STATE =====', s);
if (!s) return;
var scene = TD.stage && TD.stage.current_act && TD.stage.current_act.current_scene;
var map = scene && scene.map;
if (!map) return;
// clear existing
for (var i = 0; i < map.grids.length; i++) {
var g = map.grids[i];
if (g && g.building) g.removeBuilding();
}
if (map.monsters && map.monsters.length) {
for (var mi = 0; mi < map.monsters.length; mi++) {
if (map.monsters[mi]) {
map.monsters[mi].pause();
map.monsters[mi].del();
}
}
map.monsters = [];
}
// restore buildings
if (s.buildings) {
for (var j = 0; j < s.buildings.length; j++) {
var bi = s.buildings[j];
var grid = map.getGrid(bi.mx, bi.my);
if (grid) {
grid.addBuilding(bi.type);
var b = grid.building;
if (b) {
if (typeof bi.level !== 'undefined') b.level = bi.level;
if (typeof bi.money !== 'undefined') b.money = bi.money;
b.updateBtnDesc && b.updateBtnDesc();
}
}
}
}
// restore monsters
if (s.monsters) {
for (var mj = 0; mj < s.monsters.length; mj++) {
var md = s.monsters[mj];
if (md.life <= 0) continue;
var m_grid = map.getGrid(md.mx, md.my);
if (!m_grid || m_grid === map.exit) continue;
var monster = new TD.Monster(null, {
idx: md.idx,
difficulty: md.difficulty,
step_level: md.step_level,
render_level: md.render_level
});
monster.life = md.life;
monster.life0 = md.life0;
monster.shield = md.shield;
monster.speed = md.speed;
monster.damage = md.damage;
monster.money = md.money;
monster.r = md.r;
monster.color = md.color;
monster.toward = md.toward;
monster._dx = md._dx;
monster._dy = md._dy;
monster.beAddToGrid(m_grid);
monster.cx = md.cx;
monster.cy = md.cy;
monster.caculatePos();
if (md.next_grid_mx !== null && md.next_grid_my !== null) {
monster.next_grid = map.getGrid(md.next_grid_mx, md.next_grid_my);
}
monster.way = md.way || [];
m_grid.scene.addElement(monster, monster.step_level, monster.render_level);
map.monsters.push(monster);
if (!monster.is_paused) monster.start();
}
}
if (typeof s.money !== 'undefined') TD.money = s.money;
if (typeof s.life !== 'undefined') TD.life = s.life;
if (typeof s.score !== 'undefined') TD.score = s.score;
if (typeof s.difficulty !== 'undefined') TD.difficulty = s.difficulty;
if (typeof s.wave_damage !== 'undefined') TD.wave_damage = s.wave_damage;
if (typeof s.wave !== 'undefined' && scene) {
scene.wave = s.wave;
// 补偿逻辑:如果难度系数过低,根据波数自动修正
var min_difficulty = Math.pow(1.05, scene.wave - 1);
if (TD.difficulty < min_difficulty) {
TD.difficulty = min_difficulty;
}
}
if (typeof s.map_step_level !== 'undefined' && map) map.step_level = s.map_step_level;
if (typeof s.map_render_level !== 'undefined' && map) map.render_level = s.map_render_level;
console.log('[__TD_loadState] Load completed');
} catch (e) {
console.error('[__TD_loadState] error', e);
}
};
console.log('[MonsterSaveLoad] Enhancement applied successfully!');
}, 500);
})();
// Bridge: respond to parent requests for game state (debug-friendly)
window.addEventListener('message', function (e) {
try {
console.debug('td.html received postMessage', e.origin, e.data);
if (!e.data || !e.data.type) return;
// only accept same-origin or allow if parent is our host during dev
if (e.origin !== window.location.origin && !e.origin.startsWith('http://localhost')) {
console.debug('Ignoring postMessage from', e.origin);
return;
}
if (e.data.type === 'tower-defense:request-state') {
// Try several possible getters
var state = null;
try {
if (typeof window.__TD_getState === 'function') {
console.log('[request-state] Using window.__TD_getState()');
state = window.__TD_getState();
console.log('[request-state] State from __TD_getState:', state);
} else {
console.log('[request-state] No TD getter available');
}
} catch (err) {
console.error('state getter threw', err);
}
e.source.postMessage({ type: 'tower-defense:state', state: state }, e.origin);
console.debug('Replied with tower-defense:state', state);
} else if (e.data.type === 'tower-defense:load') {
// Forward load to any exposed loader
try {
if (typeof window.__TD_loadState === 'function') {
window.__TD_loadState(e.data.state);
console.debug('Invoked __TD_loadState');
} else {
console.debug('No load API available');
}
} catch (err) {
console.error('load handling error', err);
}
}
} catch (err) {
console.error('postMessage bridge error', err);
}
}, false);
// --- Shortcut keys enhancement ---
window.addEventListener('keydown', function (e) {
if (typeof TD === 'undefined' || !TD.stage || !TD.stage.current_act || !TD.stage.current_act.current_scene) return;
var map = TD.stage.current_act.current_scene.map;
if (!map) return;
var key = e.key.toLowerCase();
if (key === 's') {
// S: Sell selected building
if (map.selected_building) {
console.log('[Shortcut] Selling building:', map.selected_building.id);
map.selected_building.tryToSell();
}
} else if (key === 'w') {
// W: Select Wall (路障)
if (!map.selected_building) {
console.log('[Shortcut] Pre-building Wall');
map.preBuild('wall');
}
} else if (key === 'e') {
// E: Select Laser Gun (激光枪)
if (!map.selected_building) {
console.log('[Shortcut] Pre-building Laser Gun');
map.preBuild('laser_gun');
}
} else if (key === 'r') {
// R: Select Heavy Machine Gun (重机枪)
if (!map.selected_building) {
console.log('[Shortcut] Pre-building HMG');
map.preBuild('HMG');
}
}
});
</script>
</body>
</html>

657
public/xf/xf.html Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB