Migrate project from typing_practiceweb
73
public/config/qa-config.json
Normal 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
@@ -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>
|
||||
BIN
public/minesweeper-skin/cell_closed.gif
Normal file
|
After Width: | Height: | Size: 121 B |
BIN
public/minesweeper-skin/cell_open.gif
Normal file
|
After Width: | Height: | Size: 130 B |
BIN
public/minesweeper-skin/cell_pressed.gif
Normal file
|
After Width: | Height: | Size: 130 B |
BIN
public/minesweeper-skin/digit_0.gif
Normal file
|
After Width: | Height: | Size: 104 B |
BIN
public/minesweeper-skin/digit_1.gif
Normal file
|
After Width: | Height: | Size: 102 B |
BIN
public/minesweeper-skin/digit_2.gif
Normal file
|
After Width: | Height: | Size: 107 B |
BIN
public/minesweeper-skin/digit_3.gif
Normal file
|
After Width: | Height: | Size: 104 B |
BIN
public/minesweeper-skin/digit_4.gif
Normal file
|
After Width: | Height: | Size: 107 B |
BIN
public/minesweeper-skin/digit_5.gif
Normal file
|
After Width: | Height: | Size: 104 B |
BIN
public/minesweeper-skin/digit_6.gif
Normal file
|
After Width: | Height: | Size: 104 B |
BIN
public/minesweeper-skin/digit_7.gif
Normal file
|
After Width: | Height: | Size: 104 B |
BIN
public/minesweeper-skin/digit_8.gif
Normal file
|
After Width: | Height: | Size: 93 B |
BIN
public/minesweeper-skin/digit_9.gif
Normal file
|
After Width: | Height: | Size: 105 B |
BIN
public/minesweeper-skin/digit_colon.gif
Normal file
|
After Width: | Height: | Size: 98 B |
BIN
public/minesweeper-skin/face_lost.gif
Normal file
|
After Width: | Height: | Size: 114 B |
BIN
public/minesweeper-skin/face_normal.gif
Normal file
|
After Width: | Height: | Size: 111 B |
BIN
public/minesweeper-skin/face_win.gif
Normal file
|
After Width: | Height: | Size: 118 B |
BIN
public/minesweeper-skin/flag.gif
Normal file
|
After Width: | Height: | Size: 167 B |
BIN
public/minesweeper-skin/mine.gif
Normal file
|
After Width: | Height: | Size: 143 B |
BIN
public/minesweeper-skin/mine_exploded.gif
Normal file
|
After Width: | Height: | Size: 143 B |
BIN
public/minesweeper-skin/num_1.gif
Normal file
|
After Width: | Height: | Size: 123 B |
BIN
public/minesweeper-skin/num_2.gif
Normal file
|
After Width: | Height: | Size: 177 B |
BIN
public/minesweeper-skin/num_3.gif
Normal file
|
After Width: | Height: | Size: 125 B |
BIN
public/minesweeper-skin/num_4.gif
Normal file
|
After Width: | Height: | Size: 140 B |
BIN
public/minesweeper-skin/num_5.gif
Normal file
|
After Width: | Height: | Size: 125 B |
BIN
public/minesweeper-skin/num_6.gif
Normal file
|
After Width: | Height: | Size: 130 B |
BIN
public/minesweeper-skin/num_7.gif
Normal file
|
After Width: | Height: | Size: 123 B |
BIN
public/minesweeper-skin/num_8.gif
Normal file
|
After Width: | Height: | Size: 115 B |
BIN
public/minesweeper-skin/wrong_flag.gif
Normal file
|
After Width: | Height: | Size: 167 B |
1
public/output.json
Normal file
4588
public/tower-defense/td-pkg-zh-min.js
vendored
Normal file
4565
public/tower-defense/td-pkg-zh.js
Normal file
347
public/tower-defense/td.html
Normal 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
@@ -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
|
After Width: | Height: | Size: 102 KiB |