Files
pengxiao ddbb79b9f6 Initial commit: 国土空间规划课程智能体 v1.0
单容器 Docker 架构的国土空间规划课程智能问答系统,集成 FastAPI 后端与 Next.js 前端。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 09:40:18 +08:00

395 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>p5.js 城市数据可视化</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.9.4/lib/p5.min.js"></script>
<style>
body { margin: 0; overflow: hidden; background: #0a0a1a; }
canvas { display: block; }
#info {
position: fixed; bottom: 20px; left: 50%;
transform: translateX(-50%);
color: rgba(255,255,255,0.6);
font-family: system-ui, sans-serif;
font-size: 14px;
pointer-events: none;
text-align: center;
}
</style>
</head>
<body>
<div id="info">移动鼠标查看区域数据 | 点击切换数据维度 | 按空格切换视图</div>
<script>
let buildings = [];
let roads = [];
let districts = [];
let currentMetric = 0;
let metricNames = ['人口密度', '绿化率', '容积率', '交通便利度'];
let metricColors = [
[255, 100, 100],
[100, 220, 100],
[100, 150, 255],
[255, 220, 80]
];
let camX = 0, camY = 0;
let targetCamX = 0, targetCamY = 0;
let hoveredBuilding = null;
let viewMode = 0;
let time = 0;
let particles = [];
function setup() {
createCanvas(windowWidth, windowHeight);
generateCity();
}
function generateCity() {
buildings = [];
roads = [];
districts = [];
particles = [];
// 生成道路网格
let gridSize = 100;
for (let x = -800; x <= 800; x += gridSize) {
roads.push({ x1: x, y1: -800, x2: x, y2: 800 });
}
for (let y = -800; y <= 800; y += gridSize) {
roads.push({ x1: -800, y1: y, x2: 800, y2: y });
}
// 生成主路(更宽)
for (let x = -800; x <= 800; x += gridSize * 3) {
roads.push({ x1: x, y1: -800, x2: x, y2: 800, main: true });
}
for (let y = -800; y <= 800; y += gridSize * 3) {
roads.push({ x1: -800, y1: y, x2: 800, y2: y, main: true });
}
// 生成建筑
let types = ['residential', 'commercial', 'industrial', 'park'];
let typeLabels = { residential: '住宅区', commercial: '商业区', industrial: '工业区', park: '公园绿地' };
for (let gx = -750; gx < 750; gx += gridSize) {
for (let gy = -750; gy < 750; gy += gridSize) {
let cx = gx + gridSize / 2;
let cy = gy + gridSize / 2;
let dist = sqrt(cx * cx + cy * cy);
let type;
if (dist < 200) type = 'commercial';
else if (dist < 500) type = random() > 0.2 ? 'residential' : 'park';
else type = random() > 0.3 ? 'residential' : 'industrial';
if (type === 'park') {
buildings.push({
x: cx, y: cy,
w: gridSize * 0.8, h: gridSize * 0.8,
height: 0, type: type, label: typeLabels[type],
metrics: {
pop: random(5, 20),
green: random(70, 95),
far: random(0.1, 0.3),
traffic: random(20, 40)
}
});
continue;
}
let count = floor(random(1, 5));
for (let i = 0; i < count; i++) {
let bw = random(15, gridSize * 0.4);
let bh = random(15, gridSize * 0.4);
let bx = cx + random(-gridSize * 0.3, gridSize * 0.3);
let by = cy + random(-gridSize * 0.3, gridSize * 0.3);
let bHeight;
if (type === 'commercial') bHeight = random(60, 180);
else if (type === 'residential') bHeight = random(20, 80);
else bHeight = random(15, 40);
buildings.push({
x: bx, y: by,
w: bw, h: bh,
height: bHeight,
type: type,
label: typeLabels[type],
metrics: {
pop: type === 'commercial' ? random(200, 800) : random(50, 300),
green: random(10, 40),
far: bHeight / 50,
traffic: random(30, 90)
}
});
}
}
}
// 流动粒子(代表交通)
for (let i = 0; i < 200; i++) {
particles.push({
roadIdx: floor(random(roads.length)),
t: random(1),
speed: random(0.002, 0.008),
color: [255, 220, 80, random(100, 200)]
});
}
}
function draw() {
background(10, 10, 26);
time += 0.01;
// 平滑相机
camX = lerp(camX, targetCamX, 0.05);
camY = lerp(camY, targetCamY, 0.05);
push();
translate(width / 2 + camX, height / 2 + camY);
// 绘制地面
fill(15, 15, 35);
noStroke();
rect(-800, -800, 1600, 1600);
// 绘制道路
for (let r of roads) {
if (r.main) {
stroke(40, 40, 70);
strokeWeight(4);
} else {
stroke(30, 30, 55);
strokeWeight(1);
}
line(r.x1, r.y1, r.x2, r.y2);
}
// 更新和绘制流动粒子
noStroke();
for (let p of particles) {
p.t += p.speed;
if (p.t > 1) p.t -= 1;
let r = roads[p.roadIdx];
let px = lerp(r.x1, r.x2, p.t);
let py = lerp(r.y1, r.y2, p.t);
fill(p.color);
ellipse(px, py, 3, 3);
}
hoveredBuilding = null;
// 绘制建筑
for (let b of buildings) {
let mc = metricColors[currentMetric];
let metricVal;
if (currentMetric === 0) metricVal = b.metrics.pop / 800;
else if (currentMetric === 1) metricVal = b.metrics.green / 95;
else if (currentMetric === 2) metricVal = b.metrics.far / 3.6;
else metricVal = b.metrics.traffic / 90;
metricVal = constrain(metricVal, 0, 1);
if (viewMode === 0) {
drawBuildingTop(b, mc, metricVal);
} else {
drawBuildingIso(b, mc, metricVal);
}
// 检测鼠标悬停
let mx = mouseX - width / 2 - camX;
let my = mouseY - height / 2 - camY;
if (abs(mx - b.x) < b.w / 2 + 5 && abs(my - b.y) < b.h / 2 + 5) {
hoveredBuilding = b;
}
}
pop();
// 绘制UI
drawUI();
// 绘制悬停信息
if (hoveredBuilding) {
drawTooltip(hoveredBuilding);
}
}
function drawBuildingTop(b, mc, val) {
if (b.type === 'park') {
fill(30, 100 + val * 80, 30, 150);
noStroke();
rect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h, 4);
// 树
for (let i = 0; i < 5; i++) {
let tx = b.x + random(-b.w * 0.3, b.w * 0.3);
let ty = b.y + random(-b.h * 0.3, b.h * 0.3);
fill(40, 140, 40, 180);
ellipse(tx, ty, 8, 8);
}
} else {
let pulse = sin(time * 2 + b.x * 0.01) * 0.1 + 0.9;
let alpha = 80 + val * 170;
fill(mc[0], mc[1], mc[2], alpha * pulse);
noStroke();
rect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h, 2);
// 高度阴影
fill(0, 0, 0, b.height * 0.3);
let offset = b.height * 0.1;
rect(b.x - b.w / 2 + offset, b.y - b.h / 2 + offset, b.w, b.h, 2);
}
}
function drawBuildingIso(b, mc, val) {
let h = b.height * 0.3;
if (b.type === 'park') {
fill(30, 120, 30, 150);
noStroke();
rect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h, 4);
return;
}
let alpha = 100 + val * 155;
// 右侧面
fill(mc[0] * 0.6, mc[1] * 0.6, mc[2] * 0.6, alpha);
noStroke();
beginShape();
vertex(b.x + b.w / 2, b.y - b.h / 2);
vertex(b.x + b.w / 2, b.y - b.h / 2 - h);
vertex(b.x + b.w / 2 + h * 0.4, b.y - b.h / 2 - h - h * 0.4);
vertex(b.x + b.w / 2 + h * 0.4, b.y - b.h / 2 + h * 0.4);
endShape(CLOSE);
// 正面
fill(mc[0] * 0.8, mc[1] * 0.8, mc[2] * 0.8, alpha);
beginShape();
vertex(b.x - b.w / 2, b.y - b.h / 2);
vertex(b.x - b.w / 2, b.y - b.h / 2 - h);
vertex(b.x + b.w / 2, b.y - b.h / 2 - h);
vertex(b.x + b.w / 2, b.y - b.h / 2);
endShape(CLOSE);
// 顶面
fill(mc[0], mc[1], mc[2], alpha);
beginShape();
vertex(b.x - b.w / 2, b.y - b.h / 2 - h);
vertex(b.x - b.w / 2 + h * 0.4, b.y - b.h / 2 - h - h * 0.4);
vertex(b.x + b.w / 2 + h * 0.4, b.y - b.h / 2 - h - h * 0.4);
vertex(b.x + b.w / 2, b.y - b.h / 2 - h);
endShape(CLOSE);
}
function drawUI() {
// 左上角标题
fill(255, 255, 255, 200);
noStroke();
textSize(18);
textAlign(LEFT, TOP);
textFont('system-ui');
text('城市数据可视化', 20, 20);
textSize(12);
fill(255, 255, 255, 120);
text('当前指标: ' + metricNames[currentMetric], 20, 48);
// 图例
let legendY = 80;
for (let i = 0; i < 4; i++) {
let mc = metricColors[i];
if (i === currentMetric) {
fill(mc[0], mc[1], mc[2], 255);
rect(20, legendY, 12, 12, 2);
fill(255);
textSize(13);
text(metricNames[i], 38, legendY);
} else {
fill(mc[0], mc[1], mc[2], 80);
rect(20, legendY, 12, 12, 2);
fill(150);
textSize(13);
text(metricNames[i], 38, legendY);
}
legendY += 22;
}
// 色阶条
let barX = 20, barY = height - 50, barW = 200, barH = 12;
let mc = metricColors[currentMetric];
for (let i = 0; i < barW; i++) {
let t = i / barW;
fill(mc[0] * t, mc[1] * t, mc[2] * t);
noStroke();
rect(barX + i, barY, 1, barH);
}
fill(255, 255, 255, 120);
textSize(11);
text('低', barX, barY + barH + 14);
textAlign(RIGHT, TOP);
text('高', barX + barW, barY + barH + 14);
// 建筑数量
textAlign(RIGHT, TOP);
fill(255, 255, 255, 100);
textSize(12);
text('建筑: ' + buildings.length + ' | 粒子: ' + particles.length, width - 20, 20);
text('视图: ' + (viewMode === 0 ? '俯视图' : '等轴测'), width - 20, 38);
}
function drawTooltip(b) {
let tx = mouseX + 15;
let ty = mouseY - 10;
let tw = 160;
let th = 100;
if (tx + tw > width) tx = mouseX - tw - 15;
if (ty + th > height) ty = mouseY - th - 10;
fill(20, 20, 40, 220);
stroke(80, 80, 120);
strokeWeight(1);
rect(tx, ty, tw, th, 6);
noStroke();
fill(255);
textSize(13);
textAlign(LEFT, TOP);
text(b.label, tx + 10, ty + 8);
fill(180);
textSize(11);
let keys = ['人口密度', '绿化率', '容积率', '交通便利'];
let vals = [b.metrics.pop, b.metrics.green, b.metrics.far, b.metrics.traffic];
let units = ['人/hm²', '%', '', '分'];
for (let i = 0; i < 4; i++) {
let mc = metricColors[i];
fill(mc[0], mc[1], mc[2], i === currentMetric ? 255 : 120);
text(keys[i] + ': ' + (i === 1 ? vals[i].toFixed(0) : vals[i].toFixed(1)) + units[i], tx + 10, ty + 30 + i * 17);
}
}
function mousePressed() {
currentMetric = (currentMetric + 1) % 4;
}
function keyPressed() {
if (key === ' ') {
viewMode = (viewMode + 1) % 2;
}
}
function mouseDragged() {
targetCamX += mouseX - pmouseX;
targetCamY += mouseY - pmouseY;
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
</script>
</body>
</html>