<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>🌌 随机城市生成器 Pro</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Montserrat:wght@600&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"/>
<!-- PDF 导出依赖 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
background: linear-gradient(135deg, #1e3c72, #2a5298);
color: #fff;
min-height: 100vh;
padding: 20px;
background-attachment: fixed;
}
.container {
max-width: 1300px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
}
h1 {
font: 700 2.8em 'Montserrat', sans-serif;
margin-bottom: 10px;
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.subtitle {
font-size: 1.2em;
opacity: 0.9;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
margin: 20px auto;
}
button {
padding: 12px 24px;
font-size: 1.1em;
border: none;
border-radius: 50px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
}
.btn-generate {
background: #ff6b6b;
color: white;
}
.btn-print {
background: #4ecdc4;
color: white;
}
.btn-pdf {
background: #45b7d1;
color: white;
}
.btn-data {
background: #96ceb4;
color: white;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(0,0,0,0.2);
}
.loading {
text-align: center;
font-size: 1.2em;
margin: 20px 0;
color: #ffd166;
display: none;
}
.city-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-top: 20px;
}
@media (max-width: 900px) {
.city-container {
grid-template-columns: 1fr;
}
.controls {
flex-direction: column;
align-items: center;
}
}
.info-panel {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}
.info-panel h2 {
font-size: 1.6em;
margin-bottom: 15px;
color: #ffd166;
border-bottom: 2px solid #ffd166;
padding-bottom: 5px;
font-family: 'Montserrat', sans-serif;
}
.info-item {
margin: 12px 0;
line-height: 1.6;
}
.info-item i {
width: 28px;
color: #a29bfe;
}
.map-container {
text-align: center;
margin: 30px 0;
}
.map-container h2 {
margin-bottom: 15px;
color: #06d6a0;
}
#city-map {
display: inline-grid;
grid-template-columns: repeat(8, 40px);
grid-template-rows: repeat(8, 40px);
gap: 1px;
margin: 10px;
}
.map-cell {
width: 40px;
height: 40px;
border: 1px solid #333;
position: relative;
transition: transform 0.2s;
}
.map-cell:hover {
transform: scale(1.2);
z-index: 10;
}
.residential { background: #6c5ce7; }
.commercial { background: #00cec9; }
.industrial { background: #d63031; }
.government { background: #0984e3; }
.medical { background: #fdcb6e; }
.education { background: #e17055; }
.park { background: #00b894; }
.entertainment { background: #fd79a8; }
.transport { background: #74b9ff; }
.tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.85);
color: white;
padding: 8px 12px;
border-radius: 8px;
white-space: nowrap;
font-size: 0.85em;
z-index: 100;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.map-cell:hover .tooltip {
opacity: 1;
}
footer {
text-align: center;
margin-top: 50px;
opacity: 0.7;
font-size: 0.9em;
}
@media print {
.controls, .loading, footer, header button {
display: none !important;
}
body {
background: white;
color: black;
}
.info-panel {
background: white !important;
color: black !important;
-webkit-print-color-adjust: exact;
}
.city-container {
grid-template-columns: 1fr !important;
}
}
</style>
</head>
<body>
<!-- 音效 -->
<audio id="bgMusic" loop>
<source src="https://www.soundjay.com/misc/sounds/bell-ringing-05.mp3" type="audio/mpeg">
</audio>
<audio id="clickSound">
<source src="https://www.soundjay.com/buttons/sounds/button-1.mp3" type="audio/mpeg">
</audio>
<div class="container">
<header>
<h1>🌌 随机城市生成器 Pro</h1>
<p class="subtitle">生成、打印、导出、聆听——一个完整城市的诞生</p>
<div class="controls">
<button id="generateBtn" class="btn-generate">
<i class="fas fa-magic"></i> 生成城市
</button>
<button id="printBtn" class="btn-print">
<i class="fas fa-print"></i> 打印报告
</button>
<button id="exportPdfBtn" class="btn-pdf">
<i class="fas fa-file-pdf"></i> 导出 PDF
</button>
<button id="exportDataBtn" class="btn-data">
<i class="fas fa-file-csv"></i> 导出人口数据
</button>
<button id="toggleMusic" class="btn-generate" style="font-size:1em;padding:8px 16px;">
<i class="fas fa-music"></i> 音乐:开
</button>
</div>
<div class="loading" id="loading">🌍 正在生成城市数据...</div>
</header>
<div id="output" style="display: none;">
<div class="city-container">
<!-- 左侧:城市信息 -->
<div class="info-panel">
<h2><i class="fas fa-city"></i> 城市概况</h2>
<div class="info-item"><i class="fas fa-tag"></i> <strong>城市名称:</strong> <span id="cityName"></span></div>
<div class="info-item"><i class="fas fa-globe-americas"></i> <strong>国家:</strong> <span id="country"></span></div>
<div class="info-item"><i class="fas fa-map-marker-alt"></i> <strong>地理位置:</strong> <span id="location"></span></div>
<div class="info-item"><i class="fas fa-mountain"></i> <strong>地形:</strong> <span id="terrain"></span></div>
<div class="info-item"><i class="fas fa-thermometer-half"></i> <strong>气候类型:</strong> <span id="climate"></span></div>
<div class="info-item"><i class="fas fa-sun"></i> <strong>年均温:</strong> <span id="avgTemp"></span></div>
<div class="info-item"><i class="fas fa-cloud"></i> <strong>当前天气:</strong> <span id="weather"></span></div>
<div class="info-item"><i class="fas fa-users"></i> <strong>人口:</strong> <span id="population"></span></div>
<div class="info-item"><i class="fas fa-ruler-combined"></i> <strong>面积:</strong> <span id="area"></span> km²</div>
<div class="info-item"><i class="fas fa-weight-hanging"></i> <strong>人口密度:</strong> <span id="density"></span> 人/km²</div>
<div class="info-item"><i class="fas fa-calendar-alt"></i> <strong>建市年份:</strong> <span id="founded"></span></div>
<div class="info-item"><i class="fas fa-quote-left"></i> <strong>城市口号:</strong> <span id="motto"></span></div>
<div class="info-item"><i class="fas fa-star"></i> <strong>昵称:</strong> <span id="nickname"></span></div>
</div>
<!-- 右侧:经济与政府 -->
<div class="info-panel">
<h2><i class="fas fa-chart-line"></i> 经济与政府</h2>
<div class="info-item"><i class="fas fa-industry"></i> <strong>支柱产业:</strong> <span id="industry"></span></div>
<div class="info-item"><i class="fas fa-dollar-sign"></i> <strong>GDP(总量):</strong> <span id="gdp"></span> 亿美元</div>
<div class="info-item"><i class="fas fa-hand-holding-usd"></i> <strong>人均GDP:</strong> <span id="perCapitaGDP"></span> 美元</div>
<div class="info-item"><i class="fas fa-percentage"></i> <strong>失业率:</strong> <span id="unemployment"></span></div>
<div class="info-item"><i class="fas fa-balance-scale"></i> <strong>税收率:</strong> <span id="taxRate"></span></div>
<div class="info-item"><i class="fas fa-skull-crossbones"></i> <strong>犯罪率:</strong> <span id="crimeRate"></span></div>
<h2 style="margin-top: 20px;"><i class="fas fa-user-tie"></i> 政府高层</h2>
<div class="info-item"><i class="fas fa-user"></i> <strong>市长:</strong> <span id="mayor"></span></div>
<div class="info-item"><i class="fas fa-users-cog"></i> <strong>党派:</strong> <span id="party"></span></div>
<div class="info-item"><i class="fas fa-calendar-check"></i> <strong>任期:</strong> <span id="term"></span></div>
<div class="info-item"><i class="fas fa-shield-alt"></i> <strong>警察局长:</strong> <span id="policeChief"></span></div>
<div class="info-item"><i class="fas fa-money-bill-wave"></i> <strong>财政局长:</strong> <span id="financeChief"></span></div>
</div>
</div>
<!-- 中下:历史与人口 -->
<div class="city-container">
<div class="info-panel">
<h2><i class="fas fa-book"></i> 历史大事记</h2>
<div id="history"></div>
</div>
<div class="info-panel">
<h2><i class="fas fa-democrat"></i> 人口结构</h2>
<div class="info-item"><i class="fas fa-venus-mars"></i> <strong>性别比:</strong> <span id="genderRatio"></span></div>
<div class="info-item"><i class="fas fa-baby"></i> <strong>出生率:</strong> <span id="birthRate"></span></div>
<div class="info-item"><i class="fas fa-skull"></i> <strong>死亡率:</strong> <span id="deathRate"></span></div>
<div class="info-item"><i class="fas fa-heartbeat"></i> <strong>平均寿命:</strong> <span id="lifeExpectancy"></span></div>
<div class="info-item"><i class="fas fa-plane-arrival"></i> <strong>移民率:</strong> <span id="migration"></span></div>
<div class="info-item"><i class="fas fa-user-graduate"></i> <strong>教育水平:</strong> <span id="education"></span></div>
<div class="info-item"><i class="fas fa-layer-group"></i> <strong>年龄分布:</strong> <span id="ageDist"></span></div>
</div>
</div>
<!-- 地图 -->
<div class="info-panel map-container">
<h2><i class="fas fa-th-large"></i> 城市地图(8x8)</h2>
<div id="city-map"></div>
<p><small>📌 悬停区块查看详细建筑</small></p>
</div>
</div>
<footer>
随机城市生成器 v3.0 | 数据完全虚构 | 支持打印与导出 | 使用 HTML/CSS/JS 构建
</footer>
</div>
<script>
// ======================
// 音效控制
// ======================
const bgMusic = document.getElementById('bgMusic');
const clickSound = document.getElementById('clickSound');
const toggleMusicBtn = document.getElementById('toggleMusic');
let musicOn = true;
toggleMusicBtn.addEventListener('click', () => {
musicOn = !musicOn;
if (musicOn) {
bgMusic.play().catch(e => console.log("音乐播放被阻止"));
toggleMusicBtn.innerHTML = '<i class="fas fa-music"></i> 音乐:开';
} else {
bgMusic.pause();
toggleMusicBtn.innerHTML = '<i class="fas fa-music"></i> 音乐:关';
}
});
function playClick() {
if (musicOn) {
clickSound.currentTime = 0;
clickSound.play().catch(() => {});
}
}
// ======================
// 数据词库(同上,略精简)
// ======================
const CITY_PREFIXES = ["New", "Port", "Lake", "North", "South", "East", "West", "Fort", "Grand", "Spring", "Mill", "Rock", "Hill", "Sun", "Bay"];
const CITY_SUFFIXES = ["ville", "ton", "burg", "field", "wood", "port", "shire", "land", "view", "crest", "haven", "dale", "ford", "stone", "beach"];
const COUNTRIES = ["United States", "Canada", "UK", "Germany", "France", "Australia", "Sweden", "Norway", "Austria", "Switzerland"];
const TERRAINS = ["Coastal", "Plains", "Hilly", "Mountainous", "River Valley", "Island", "Desert Edge", "Forest"];
const CLIMATES = [
{ name: "Temperate", temp: "8-15°C" },
{ name: "Mediterranean", temp: "12-20°C" },
{ name: "Continental", temp: "1-25°C" },
{ name: "Arid", temp: "18-35°C" },
{ name: "Subtropical", temp: "15-28°C" },
{ name: "Oceanic", temp: "10-18°C" }
];
const WEATHERS = ["Sunny", "Cloudy", "Rainy", "Snowy", "Foggy", "Stormy"];
const INDUSTRIES = ["Technology", "Manufacturing", "Tourism", "Education", "Healthcare", "Agriculture", "Finance", "Renewable Energy", "Entertainment", "Logistics"];
const MOTTOES = ["Progress Through Unity", "Innovation & Tradition", "Where Nature Meets City", "Built for Tomorrow", "Strong, Safe, Sustainable"];
const NICKNAMES = ["The Emerald City", "Riverfront Gem", "Silicon Valley North", "Heart of the Valley", "The Twin Lakes"];
const NAMES_MALE = ["James", "John", "Robert", "Michael", "William", "David"];
const NAMES_FEMALE = ["Mary", "Patricia", "Jennifer", "Linda", "Elizabeth", "Barbara"];
const SURNAMES = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia"];
const PARTIES = ["Progressive", "Conservative", "Liberal", "Green", "Independent", "Unity"];
const HISTORICAL_EVENTS = [
"Great Fire of {year} destroyed downtown, rebuilt in Art Deco style.",
"Founded as a trading post in {year} by European settlers.",
"Hosted the National Games in {year}, boosting tourism.",
"Tech boom in {year}s transformed the economy.",
"Major flood in {year} led to new levee system.",
"Renowned author {name} was born here in {year}.",
"City declared carbon neutral in {year}."
];
const ZONES = ['residential', 'commercial', 'industrial', 'government', 'medical', 'education', 'park', 'entertainment', 'transport'];
const BUILDINGS = {
'residential': ['Apartment', 'House', 'Condo', 'Townhouse'],
'commercial': ['Shopping Mall', 'Restaurant', 'Cafe', 'Office Building', 'Bank', 'Hotel'],
'industrial': ['Factory', 'Warehouse', 'Power Plant', 'Recycling Center'],
'government': ['City Hall', 'Police Station', 'Fire Station', 'Courthouse', 'Post Office'],
'medical': ['Hospital', 'Clinic', 'Pharmacy'],
'education': ['Elementary School', 'High School', 'University', 'Library'],
'park': ['Park', 'Botanical Garden', 'Zoo', 'Playground'],
'entertainment': ['Cinema', 'Theater', 'Museum', 'Stadium', 'Arcade'],
'transport': ['Bus Station', 'Train Station', 'Airport', 'Subway Entrance', 'Parking Garage']
};
// ======================
// 工具函数
// ======================
function rand(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
function randName() { return (Math.random() > 0.5 ? rand(NAMES_MALE) : rand(NAMES_FEMALE)) + " " + rand(SURNAMES); }
function randFloat(min, max) { return parseFloat((Math.random() * (max - min) + min).toFixed(2)); }
function randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
// ======================
// 生成城市
// ======================
function generateCity() {
const city = {};
city.name = rand(CITY_PREFIXES) + " " + rand(CITY_SUFFIXES);
city.country = rand(COUNTRIES);
city.latitude = randFloat(-70, 70).toFixed(4);
city.longitude = randFloat(-180, 180).toFixed(4);
city.location = `${city.latitude}°N, ${city.longitude}°E`;
city.terrain = rand(TERRAINS);
city.climateObj = rand(CLIMATES);
city.climate = city.climateObj.name;
city.avgTemp = city.climateObj.temp;
city.weather = rand(WEATHERS);
city.population = randInt(20000, 2000000);
city.area = randFloat(50, 1000);
city.density = (city.population / city.area).toFixed(1);
city.founded = randInt(1700, 1950);
city.motto = rand(MOTTOES);
city.nickname = rand(NICKNAMES);
city.industry = rand(INDUSTRIES);
city.gdpTotal = (city.population * randFloat(30000, 80000) / 1e8).toFixed(1);
city.perCapitaGDP = randInt(35000, 90000);
city.unemployment = randFloat(3, 12).toFixed(1) + "%";
city.taxRate = randFloat(8, 25).toFixed(1) + "%";
city.crimeRate = ["Low", "Moderate", "High"][randInt(0, 2)];
city.mayor = randName();
city.party = rand(PARTIES);
city.term = `${randInt(2020, 2024)} - ${randInt(2024, 2028)}`;
city.policeChief = randName();
city.financeChief = randName();
city.genderRatio = randInt(95, 105) + ":100";
city.birthRate = randFloat(8, 16).toFixed(1) + "‰";
city.deathRate = randFloat(6, 12).toFixed(1) + "‰";
city.lifeExpectancy = randInt(75, 85) + " 岁";
city.migration = ["Net Inflow", "Net Outflow", "Balanced"][randInt(0, 2)];
city.education = ["High", "Medium", "Growing"][randInt(0, 2)];
const a = randInt(15, 20), b = randInt(10, 15), c = randInt(35, 45), d = randInt(8, 12), e = 100 - a - b - c - d;
city.ageDist = `0-14: ${a}%, 15-24: ${b}%, 25-54: ${c}%, 55-64: ${d}%, 65+: ${e}%`;
city.history = [];
const usedYears = new Set();
for (let i = 0; i < 3; i++) {
let event = rand(HISTORICAL_EVENTS);
let year = randInt(1800, 2023);
while (usedYears.has(year)) year = randInt(1800, 2023);
usedYears.add(year);
event = event.replace("{year}", year);
event = event.replace("{name}", randName());
city.history.push(event);
}
// 地图
const n = 8;
const center = 3;
city.map = Array(n).fill().map(() => Array(n).fill().map(() => ({})));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
let zone;
const dist = Math.abs(i - center) + Math.abs(j - center);
if (dist <= 1) zone = 'commercial';
else if (dist <= 3) zone = rand(['residential', 'residential', 'government']);
else if (i === 0 || j === 0 || i === n-1 || j === n-1) zone = 'industrial';
else zone = rand(ZONES);
const buildings = rand(BUILDINGS[zone] || ['Office']);
city.map[i][j] = { zone, buildings };
}
}
return city;
}
// ======================
// 渲染
// ======================
function renderCity(city) {
document.getElementById('cityName').textContent = city.name;
document.getElementById('country').textContent = city.country;
document.getElementById('location').textContent = city.location;
document.getElementById('terrain').textContent = city.terrain;
document.getElementById('climate').textContent = city.climate;
document.getElementById('avgTemp').textContent = city.avgTemp;
document.getElementById('weather').textContent = city.weather;
document.getElementById('population').textContent = city.population.toLocaleString();
document.getElementById('area').textContent = city.area.toFixed(1);
document.getElementById('density').textContent = city.density;
document.getElementById('founded').textContent = city.founded;
document.getElementById('motto').textContent = `"${city.motto}"`;
document.getElementById('nickname').textContent = city.nickname;
document.getElementById('industry').textContent = city.industry;
document.getElementById('gdp').textContent = city.gdpTotal;
document.getElementById('perCapitaGDP').textContent = city.perCapitaGDP.toLocaleString();
document.getElementById('unemployment').textContent = city.unemployment;
document.getElementById('taxRate').textContent = city.taxRate;
document.getElementById('crimeRate').textContent = city.crimeRate;
document.getElementById('mayor').textContent = city.mayor;
document.getElementById('party').textContent = city.party;
document.getElementById('term').textContent = city.term;
document.getElementById('policeChief').textContent = city.policeChief;
document.getElementById('financeChief').textContent = city.financeChief;
document.getElementById('genderRatio').textContent = city.genderRatio;
document.getElementById('birthRate').textContent = city.birthRate;
document.getElementById('deathRate').textContent = city.deathRate;
document.getElementById('lifeExpectancy').textContent = city.lifeExpectancy;
document.getElementById('migration').textContent = city.migration;
document.getElementById('education').textContent = city.education;
document.getElementById('ageDist').textContent = city.ageDist;
const historyEl = document.getElementById('history');
historyEl.innerHTML = '';
city.history.forEach(h => {
const p = document.createElement('p');
p.textContent = "• " + h;
p.style.margin = '6px 0';
historyEl.appendChild(p);
});
const mapEl = document.getElementById('city-map');
mapEl.innerHTML = '';
city.map.forEach((row, i) => {
row.forEach((cell, j) => {
const div = document.createElement('div');
div.className = `map-cell ${cell.zone}`;
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.textContent = `${cell.buildings} (${cell.zone})`;
div.appendChild(tooltip);
mapEl.appendChild(div);
});
});
window.generatedCity = city; // 保存供导出使用
}
// ======================
// 导出功能
// ======================
document.getElementById('printBtn').addEventListener('click', () => {
playClick();
window.print();
});
document.getElementById('exportPdfBtn').addEventListener('click', async () => {
playClick();
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'mm', 'a4');
const element = document.body;
doc.setFont('Helvetica');
doc.setFontSize(12);
doc.text("城市报告 - " + (window.generatedCity?.name || "未知城市"), 20, 20);
await html2canvas(element, { scale: 1.5, useCORS: true }).then(canvas => {
const imgData = canvas.toDataURL('image/png');
const imgWidth = 190;
const imgHeight = canvas.height * imgWidth / canvas.width;
doc.addImage(imgData, 'PNG', 10, 30, imgWidth, imgHeight);
doc.save(`城市_${window.generatedCity.name}.pdf`);
});
});
document.getElementById('exportDataBtn').addEventListener('click', () => {
playClick();
const city = window.generatedCity;
const csv = [
["数据项", "数值"],
["城市名称", city.name],
["国家", city.country],
["人口", city.population],
["面积_km2", city.area],
["人口密度", city.density],
["GDP_亿美元", city.gdpTotal],
["人均GDP", city.perCapitaGDP],
["失业率", city.unemployment],
["出生率_‰", city.birthRate.replace('‰','')],
["死亡率_‰", city.deathRate.replace('‰','')],
["平均寿命_岁", city.lifeExpectancy.replace(' 岁','')],
["性别比", city.genderRatio],
["移民率", city.migration],
["教育水平", city.education],
["0-14岁比例", city.ageDist.match(/0-14: (\d+)%/)?.[1] || 0],
["15-24岁比例", city.ageDist.match(/15-24: (\d+)%/)?.[1] || 0],
["25-54岁比例", city.ageDist.match(/25-54: (\d+)%/)?.[1] || 0],
["55-64岁比例", city.ageDist.match(/55-64: (\d+)%/)?.[1] || 0],
["65+岁比例", city.ageDist.match(/65\+: (\d+)%/)?.[1] || 0],
].map(row => row.join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", `城市人口数据_${city.name}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
// ======================
// 生成事件
// ======================
document.getElementById('generateBtn').addEventListener('click', function() {
playClick();
const loading = document.getElementById('loading');
const output = document.getElementById('output');
loading.style.display = 'block';
output.style.display = 'none';
setTimeout(() => {
const city = generateCity();
renderCity(city);
loading.style.display = 'none';
output.style.display = 'block';
}, 800);
});
window.onload = () => {
bgMusic.play().catch(() => console.log("自动播放被阻止"));
document.getElementById('generateBtn').click();
};
</script>
</body>
</html>
index.html
index.html