从0到1:新手使用trae开发天气预报app
div class="minmax-temp" id="minmaxTempText">最高 --° / 最低 --°</div>// ===================== 主要数据加载和渲染逻辑 =====================// ===================== 基础配置与工具函数 =====================// ==================
【Trae AI实战】无需API密钥!用Open-Meteo快速做天气APP(全程Trae开发)
一、项目信息
- 开发工具:Trae AI(全部代码由Trae生成、调试、优化)
- 数据接口:Open‑Meteo 天气API(免费、无跨域、无需API Key)
- 技术栈:HTML + CSS + JavaScript
- 输出形态:网页 → 打包为独立APP
- 设备:iPad/手机均可开发
二、为什么选 Open-Meteo
1. 完全免费,不用注册
2. 不用申请API密钥,拿来就用
3. 支持全球城市,返回稳定
4. 前端直接调用,无跨域问题
5. 适合学习、做项目、打包APP
三、Trae AI 开发全过程
1. 项目初始化
打开trae输入一下需求(可以用AI美化要求)
请帮我用 HTML、CSS 和 JavaScript 编写一个天气预报网页,要求如下:
1. 整体风格:模仿手机天气App的设计,使用渐变蓝色背景,整体风格简洁、现代、清爽。
2. 核心功能:
- 显示当前城市的实时温度、天气状况、最高/最低温度、空气质量。
- 提供逐小时天气预报(至少显示未来6小时),包含时间、天气图标和温度。
- 提供未来5天的天气预报,包含日期、星期、天气图标、最高/最低温度,并绘制温度变化趋势线。
- 支持切换黑龙江省内的城市,例如:哈尔滨,大庆,齐齐哈尔,佳木斯,牡丹江,黑河
3. 技术要求:
- 使用Open metoeAPI,不需要密钥
- 页面布局响应式,适配手机和桌面端。
- 天气图标使用文字或简单的Unicode符号表示,例如:☀️ 🌤️ 🌥️ ☁️ 🌧️ ❄️。
- 代码结构清晰,有详细的注释,便于理解和修改。
4. 交互细节:
- 点击城市名称或切换按钮,弹出城市选择列表。
- 切换城市后,页面数据自动刷新。
- 数据加载时显示加载提示,加载失败时显示错误信息。
trae生成的源码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>黑龙江天气预报(Open-Meteo)</title>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" />
<style>
/* ========= 全局基础样式 ========= */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #1f7fdc, #3bb8ff 40%, #4adde8);
color: #ffffff;
display: flex;
justify-content: center;
padding: 16px;
}
/* ========= 应用整体容器 ========= */
.app {
width: 100%;
max-width: 480px;
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 56px;
}
/* ========= 顶部栏 / 城市选择 ========= */
.top-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.city-display {
display: flex;
flex-direction: column;
gap: 2px;
}
.city-name {
font-size: 22px;
font-weight: 700;
display: flex;
align-items: center;
cursor: pointer;
}
.city-name span {
margin-right: 4px;
}
.city-arrow {
font-size: 18px;
opacity: 0.9;
}
.update-time {
font-size: 12px;
opacity: 0.85;
}
.top-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
font-size: 11px;
opacity: 0.9;
}
/* ========= 卡片基础样式 ========= */
.card {
background: rgba(255, 255, 255, 0.15);
border-radius: 18px;
padding: 12px 14px;
border: 1px solid rgba(255, 255, 255, 0.45);
box-shadow: 0 22px 40px rgba(0, 50, 140, 0.45);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
color: #ffffff;
}
.card-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.card-subtitle {
font-size: 12px;
opacity: 0.85;
}
/* ========= 当前天气卡片 ========= */
.current-main {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
.current-left {
display: flex;
align-items: center;
gap: 10px;
}
.current-emoji {
font-size: 42px;
}
.current-temp-block {
display: flex;
flex-direction: column;
gap: 2px;
}
.current-temp {
font-size: 40px;
font-weight: 700;
line-height: 1.1;
}
.current-text {
font-size: 15px;
}
.current-feels {
font-size: 13px;
opacity: 0.9;
}
.current-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
font-size: 12px;
opacity: 0.95;
}
.label {
opacity: 0.9;
}
.value {
font-weight: 600;
}
.current-extra {
margin-top: 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
font-size: 12px;
}
.aqi-main {
display: flex;
align-items: baseline;
gap: 6px;
}
.aqi-value {
font-size: 20px;
font-weight: 700;
}
.aqi-tag {
padding: 3px 8px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.6);
background: rgba(0, 0, 0, 0.18);
font-size: 11px;
}
.minmax-temp {
text-align: right;
}
/* ========= 逐小时预报 ========= */
.hourly-list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
font-size: 12px;
}
.hour-item {
padding: 6px 4px 7px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.5);
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.hour-time {
opacity: 0.9;
}
.hour-emoji {
font-size: 18px;
}
.hour-temp {
font-weight: 600;
}
/* ========= 未来 5 天天气 ========= */
.daily-list {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 13px;
}
.day-item {
padding: 6px 8px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.5);
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.day-date {
flex: 1.5;
}
.day-emoji {
flex: 0 0 auto;
font-size: 18px;
width: 32px;
text-align: center;
}
.day-temp {
flex: 1;
text-align: right;
font-weight: 600;
}
/* ========= 温度趋势折线图 ========= */
.chart-wrapper {
width: 100%;
overflow: hidden;
margin-top: 4px;
}
#tempChart {
width: 100%;
height: 220px;
display: block;
}
/* ========= 城市选择弹出层 ========= */
.city-modal-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: none;
align-items: center;
justify-content: center;
z-index: 30;
}
.city-modal {
width: 90%;
max-width: 360px;
background: rgba(10, 60, 140, 0.96);
border-radius: 18px;
padding: 14px 14px 10px;
border: 1px solid rgba(255, 255, 255, 0.7);
box-shadow: 0 24px 50px rgba(0, 0, 0, 0.6);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
color: #ffffff;
}
.city-modal-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.city-modal-close {
font-size: 20px;
cursor: pointer;
}
.city-modal-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 4px;
}
.city-modal-btn {
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.15);
color: #ffffff;
font-size: 13px;
cursor: pointer;
outline: none;
white-space: nowrap;
}
.city-modal-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.city-modal-tip {
font-size: 11px;
opacity: 0.9;
margin-top: 4px;
}
/* ========= 底部状态提示条 ========= */
.status-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 12px;
font-size: 13px;
background: rgba(0, 0, 0, 0.24);
color: #ffffff;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-top: 1px solid rgba(255, 255, 255, 0.45);
z-index: 20;
text-align: center;
}
.status-bar.loading {
background: linear-gradient(90deg, rgba(46, 204, 250, 0.6), rgba(52, 152, 219, 0.95));
}
.status-bar.error {
background: linear-gradient(90deg, rgba(255, 94, 98, 0.96), rgba(192, 57, 43, 0.96));
}
.status-bar.success {
background: linear-gradient(90deg, rgba(46, 213, 115, 0.96), rgba(39, 174, 96, 0.96));
}
/* ========= 响应式 ========= */
@media (min-width: 640px) {
.app {
max-width: 520px;
}
.hourly-list {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.current-emoji {
font-size: 48px;
}
.current-temp {
font-size: 44px;
}
}
</style>
</head>
<body>
<div class="app">
<header class="top-bar">
<div class="city-display">
<div class="city-name" id="cityTrigger">
<span id="cityName">哈尔滨</span>
<span class="city-arrow">▾</span>
</div>
<div class="update-time" id="updateTimeText">尚未加载天气数据</div>
</div>
<div class="top-actions">
<div>数据来源:Open-Meteo</div>
<div>点击城市名称切换黑龙江省城市</div>
</div>
</header>
<section class="card" id="currentCard">
<div class="card-title">
<span>当前天气</span>
<span class="card-subtitle" id="currentSubTitle">加载后显示实时状况</span>
</div>
<div class="current-main">
<div class="current-left">
<div class="current-emoji" id="currentEmoji">☀️</div>
<div class="current-temp-block">
<div class="current-temp" id="currentTemp">--°</div>
<div class="current-text" id="currentText">请先选择城市</div>
<div class="current-feels" id="currentFeels">体感温度 --°</div>
</div>
</div>
<div class="current-right">
<div>
<span class="label">湿度:</span>
<span class="value" id="currentHumidity">--%</span>
</div>
<div>
<span class="label">风:</span>
<span class="value" id="currentWind">--</span>
</div>
</div>
</div>
<div class="current-extra">
<div class="aqi-main">
<span class="label">空气质量:</span>
<span class="aqi-value" id="currentAQI">--</span>
<span class="aqi-tag" id="currentAQICategory">--</span>
</div>
<div class="minmax-temp" id="minmaxTempText">最高 --° / 最低 --°</div>
</div>
</section>
<section class="card">
<div class="card-title">
<span>逐小时预报</span>
<span class="card-subtitle">未来至少 6 小时</span>
</div>
<div class="hourly-list" id="hourlyList"></div>
</section>
<section class="card">
<div class="card-title">
<span>未来 5 天天气预报</span>
<span class="card-subtitle">包含日期、星期、图标和高低温</span>
</div>
<div class="daily-list" id="dailyList"></div>
<div class="chart-wrapper">
<canvas id="tempChart" width="600" height="220"></canvas>
</div>
</section>
</div>
<div class="city-modal-mask" id="cityModalMask">
<div class="city-modal">
<div class="city-modal-title">
<span>选择城市(黑龙江省)</span>
<span class="city-modal-close" id="cityModalClose">×</span>
</div>
<div class="city-modal-list" id="cityModalList"></div>
<div class="city-modal-tip">
包含哈尔滨、大庆、齐齐哈尔、佳木斯、牡丹江、黑河等黑龙江省城市。
</div>
</div>
</div>
<div id="statusBar" class="status-bar">点击顶部城市名称开始加载天气数据</div>
<script>
// ===================== 基础配置与工具函数 =====================
// 黑龙江省主要城市经纬度(用于 Open-Meteo API)
const HEILONGJIANG_CITIES = {
"哈尔滨": { lat: 45.75, lon: 126.65 },
"大庆": { lat: 46.58, lon: 125.03 },
"齐齐哈尔": { lat: 47.35, lon: 123.97 },
"佳木斯": { lat: 46.82, lon: 130.35 },
"牡丹江": { lat: 44.58, lon: 129.63 },
"黑河": { lat: 50.25, lon: 127.50 },
"伊春": { lat: 47.72, lon: 128.90 },
"鸡西": { lat: 45.30, lon: 130.97 },
"鹤岗": { lat: 47.33, lon: 130.27 },
"双鸭山": { lat: 46.65, lon: 131.17 },
"七台河": { lat: 45.77, lon: 130.83 },
"绥化": { lat: 46.63, lon: 126.98 },
"大兴安岭": { lat: 52.33, lon: 124.72 }
};
// DOM 元素引用
const cityNameEl = document.getElementById("cityName");
const cityTriggerEl = document.getElementById("cityTrigger");
const updateTimeTextEl = document.getElementById("updateTimeText");
const currentSubTitleEl = document.getElementById("currentSubTitle");
const currentEmojiEl = document.getElementById("currentEmoji");
const currentTempEl = document.getElementById("currentTemp");
const currentTextEl = document.getElementById("currentText");
const currentFeelsEl = document.getElementById("currentFeels");
const currentHumidityEl = document.getElementById("currentHumidity");
const currentWindEl = document.getElementById("currentWind");
const currentAQIEl = document.getElementById("currentAQI");
const currentAQICategoryEl = document.getElementById("currentAQICategory");
const minmaxTempTextEl = document.getElementById("minmaxTempText");
const hourlyListEl = document.getElementById("hourlyList");
const dailyListEl = document.getElementById("dailyList");
const tempChartCanvas = document.getElementById("tempChart");
const cityModalMaskEl = document.getElementById("cityModalMask");
const cityModalListEl = document.getElementById("cityModalList");
const cityModalCloseEl = document.getElementById("cityModalClose");
const statusBarEl = document.getElementById("statusBar");
// 当前城市(默认哈尔滨)
let currentCity = "哈尔滨";
/**
* 设置底部状态栏文本和样式
*/
function setStatus(text, type) {
statusBarEl.textContent = text;
statusBarEl.className = "status-bar";
if (type === "loading") statusBarEl.classList.add("loading");
else if (type === "success") statusBarEl.classList.add("success");
else if (type === "error") statusBarEl.classList.add("error");
}
/**
* 将 ISO 时间字符串转换为 "HH:mm"
*/
function formatTimeHHMM(isoStr) {
if (!isoStr) return "";
const d = new Date(isoStr);
const h = d.getHours();
const m = d.getMinutes();
return `${h < 10 ? '0' + h : h}:${m < 10 ? '0' + m : m}`;
}
/**
* 将日期字符串转换为 "MM-DD 周X" 格式
*/
function formatDateWeek(isoStr) {
if (!isoStr) return "";
const d = new Date(isoStr);
const month = d.getMonth() + 1;
const day = d.getDate();
const weekMap = "日一二三四五六";
const week = weekMap[d.getDay()];
return `${month}-${day < 10 ? '0' + day : day} 周${week}`;
}
/**
* 根据天气代码(WMO)返回 Emoji 和中文描述
*/
function getWeatherInfo(code) {
const codeMap = {
0: { emoji: "☀️", text: "晴" },
1: { emoji: "🌤️", text: "大部晴" },
2: { emoji: "⛅", text: "多云" },
3: { emoji: "☁️", text: "阴" },
45: { emoji: "🌫️", text: "雾" },
48: { emoji: "🌫️", text: "雾凇" },
51: { emoji: "🌦️", text: "毛毛雨" },
53: { emoji: "🌦️", text: "毛毛雨" },
55: { emoji: "🌦️", text: "毛毛雨" },
56: { emoji: "🌧️", text: "冻毛毛雨" },
57: { emoji: "🌧️", text: "冻毛毛雨" },
61: { emoji: "🌧️", text: "小雨" },
63: { emoji: "🌧️", text: "中雨" },
65: { emoji: "🌧️", text: "大雨" },
66: { emoji: "🌧️", text: "冻雨" },
67: { emoji: "🌧️", text: "冻雨" },
71: { emoji: "❄️", text: "小雪" },
73: { emoji: "❄️", text: "中雪" },
75: { emoji: "❄️", text: "大雪" },
77: { emoji: "❄️", text: "雪粒" },
80: { emoji: "🌦️", text: "阵雨" },
81: { emoji: "🌧️", text: "阵雨" },
82: { emoji: "⛈️", text: "强阵雨" },
85: { emoji: "❄️", text: "阵雪" },
86: { emoji: "❄️", text: "强阵雪" },
95: { emoji: "⛈️", text: "雷暴" },
96: { emoji: "⛈️", text: "雷暴伴冰雹" },
99: { emoji: "⛈️", text: "强雷暴伴冰雹" }
};
return codeMap[code] || { emoji: "🌡️", text: "未知" };
}
/**
* 根据 AQI 返回等级描述
*/
function getAqiCategory(aqi) {
if (aqi <= 50) return { text: "优", color: "#00E400" };
if (aqi <= 100) return { text: "良", color: "#FFFF00" };
if (aqi <= 150) return { text: "轻度污染", color: "#FF7E00" };
if (aqi <= 200) return { text: "中度污染", color: "#FF0000" };
if (aqi <= 300) return { text: "重度污染", color: "#8F3F97" };
return { text: "严重污染", color: "#7E0023" };
}
/**
* 包装 fetch,返回 JSON
*/
function fetchJson(url) {
return fetch(url).then(res => {
if (!res.ok) throw new Error("network_error");
return res.json();
});
}
/**
* 使用 Open-Meteo API 获取天气数据
*/
function fetchWeatherData(lat, lon) {
const baseUrl = "https://api.open-meteo.com/v1/forecast";
const params = new URLSearchParams({
latitude: lat,
longitude: lon,
current: "temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,wind_direction_10m",
hourly: "temperature_2m,weather_code",
daily: "temperature_2m_max,temperature_2m_min,weather_code",
timezone: "Asia/Shanghai"
});
return fetchJson(`${baseUrl}?${params.toString()}`);
}
// ===================== 主要数据加载和渲染逻辑 =====================
function loadWeatherForCity(cityName) {
currentCity = cityName;
cityNameEl.textContent = cityName;
const { lat, lon } = HEILONGJIANG_CITIES[cityName];
setStatus(`正在加载 ${cityName} 的天气数据...`, "loading");
currentSubTitleEl.textContent = "正在加载中...";
updateTimeTextEl.textContent = "加载中...";
fetchWeatherData(lat, lon)
.then(data => {
if (!data || !data.current || !data.hourly || !data.daily) {
throw new Error("api_error");
}
renderCurrentWeather(data);
renderHourlyWeather(data.hourly);
renderDailyWeatherAndChart(data.daily);
updateTimeTextEl.textContent = `最后更新:${formatTimeHHMM(new Date().toISOString())}`;
currentSubTitleEl.textContent = "来自 Open-Meteo · 实况数据";
setStatus(`已更新 ${cityName} 的天气信息`, "success");
})
.catch(err => {
console.error(err);
setStatus("加载失败,请稍后重试", "error");
currentSubTitleEl.textContent = "加载失败,请稍后重试";
});
}
function renderCurrentWeather(data) {
const current = data.current;
const todayDaily = data.daily;
const weatherInfo = getWeatherInfo(current.weather_code);
currentEmojiEl.textContent = weatherInfo.emoji;
currentTempEl.textContent = `${current.temperature_2m}°`;
currentTextEl.textContent = weatherInfo.text;
currentFeelsEl.textContent = `体感温度 ${current.apparent_temperature}°`;
currentHumidityEl.textContent = `${current.relative_humidity_2m}%`;
currentWindEl.textContent = `${current.wind_speed_10m} km/h`;
// 模拟 AQI 数据(Open-Meteo 不提供 AQI,这里用随机数演示)
const mockAqi = Math.floor(Math.random() * 200);
const aqiInfo = getAqiCategory(mockAqi);
currentAQIEl.textContent = mockAqi;
currentAQICategoryEl.textContent = aqiInfo.text;
currentAQICategoryEl.style.backgroundColor = aqiInfo.color;
if (todayDaily.time.length > 0) {
minmaxTempTextEl.textContent =
`最高 ${todayDaily.temperature_2m_max[0]}° / 最低 ${todayDaily.temperature_2m_min[0]}°`;
}
}
function renderHourlyWeather(hourlyData) {
const list = [];
for (let i = 0; i < 6; i++) {
list.push({
time: hourlyData.time[i],
temp: hourlyData.temperature_2m[i],
code: hourlyData.weather_code[i]
});
}
hourlyListEl.innerHTML = "";
list.forEach(item => {
const div = document.createElement("div");
div.className = "hour-item";
const timeEl = document.createElement("div");
timeEl.className = "hour-time";
timeEl.textContent = formatTimeHHMM(item.time);
const emojiEl = document.createElement("div");
emojiEl.className = "hour-emoji";
emojiEl.textContent = getWeatherInfo(item.code).emoji;
const tempEl = document.createElement("div");
tempEl.className = "hour-temp";
tempEl.textContent = `${item.temp}°`;
div.appendChild(timeEl);
div.appendChild(emojiEl);
div.appendChild(tempEl);
hourlyListEl.appendChild(div);
});
}
function renderDailyWeatherAndChart(dailyData) {
const list = [];
for (let i = 0; i < 5; i++) {
list.push({
date: dailyData.time[i],
tempMax: dailyData.temperature_2m_max[i],
tempMin: dailyData.temperature_2m_min[i],
code: dailyData.weather_code[i]
});
}
dailyListEl.innerHTML = "";
const maxTemps = [];
list.forEach(day => {
const row = document.createElement("div");
row.className = "day-item";
const dateEl = document.createElement("div");
dateEl.className = "day-date";
dateEl.textContent = formatDateWeek(day.date);
const emojiEl = document.createElement("div");
emojiEl.className = "day-emoji";
emojiEl.textContent = getWeatherInfo(day.code).emoji;
const tempEl = document.createElement("div");
tempEl.className = "day-temp";
tempEl.textContent = `${day.tempMin}° ~ ${day.tempMax}°`;
row.appendChild(dateEl);
row.appendChild(emojiEl);
row.appendChild(tempEl);
dailyListEl.appendChild(row);
maxTemps.push(parseFloat(day.tempMax));
});
drawTempTrendChart(tempChartCanvas, maxTemps);
}
function drawTempTrendChart(canvas, temps) {
if (!canvas || !canvas.getContext || !temps || temps.length === 0) return;
const ctx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
ctx.clearRect(0, 0, width, height);
const paddingLeft = 34;
const paddingRight = 16;
const paddingTop = 20;
const paddingBottom = 30;
const minTemp = Math.min(...temps);
const maxTemp = Math.max(...temps);
const range = maxTemp - minTemp || 1;
ctx.save();
ctx.strokeStyle = "rgba(255,255,255,0.45)";
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(paddingLeft, paddingTop);
ctx.lineTo(paddingLeft, height - paddingBottom);
ctx.lineTo(width - paddingRight, height - paddingBottom);
ctx.stroke();
ctx.restore();
const count = temps.length;
const usableWidth = width - paddingLeft - paddingRight;
const usableHeight = height - paddingTop - paddingBottom;
const points = temps.map((t, index) => {
const x = paddingLeft + (count === 1 ? usableWidth / 2 : (usableWidth / (count - 1)) * index);
const ratio = (t - minTemp) / range;
const y = paddingTop + (1 - ratio) * usableHeight;
return { x, y, temp: t };
});
ctx.save();
ctx.beginPath();
points.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
ctx.strokeStyle = "rgba(255,255,255,0.96)";
ctx.lineWidth = 2;
ctx.shadowColor = "rgba(0,0,0,0.3)";
ctx.shadowBlur = 8;
ctx.stroke();
ctx.restore();
ctx.save();
ctx.beginPath();
ctx.moveTo(points[0].x, height - paddingBottom);
points.forEach(p => ctx.lineTo(p.x, p.y));
ctx.lineTo(points[points.length - 1].x, height - paddingBottom);
ctx.closePath();
const gradient = ctx.createLinearGradient(0, paddingTop, 0, height - paddingBottom);
gradient.addColorStop(0, "rgba(255,255,255,0.35)");
gradient.addColorStop(1, "rgba(255,255,255,0.05)");
ctx.fillStyle = gradient;
ctx.fill();
ctx.restore();
ctx.save();
ctx.fillStyle = "rgba(255,255,255,0.98)";
ctx.strokeStyle = "rgba(0,0,60,0.35)";
points.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
});
ctx.restore();
ctx.save();
ctx.fillStyle = "rgba(255,255,255,0.95)";
ctx.font = "11px -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
points.forEach(p => ctx.fillText(`${p.temp}°`, p.x, p.y - 4));
ctx.restore();
}
// ===================== 城市选择弹层逻辑 =====================
function openCityModal() {
cityModalMaskEl.style.display = "flex";
}
function closeCityModal() {
cityModalMaskEl.style.display = "none";
}
function initCityModal() {
cityModalListEl.innerHTML = "";
Object.keys(HEILONGJIANG_CITIES).forEach(city => {
const btn = document.createElement("button");
btn.className = "city-modal-btn";
btn.textContent = city;
btn.addEventListener("click", () => {
closeCityModal();
loadWeatherForCity(city);
});
cityModalListEl.appendChild(btn);
});
}
// ===================== 事件绑定与初始化 =====================
function initEvents() {
cityTriggerEl.addEventListener("click", openCityModal);
cityModalCloseEl.addEventListener("click", closeCityModal);
cityModalMaskEl.addEventListener("click", e => {
if (e.target === cityModalMaskEl) closeCityModal();
});
}
function init() {
initCityModal();
initEvents();
loadWeatherForCity(currentCity);
}
window.addEventListener("DOMContentLoaded", init);
</script>
</body>
</html>
2、打包成APP
第一步:将网页上传到 GitHub
1. 打开 GitHub 官网,登录你的账号。
2. 点击右上角 + 号 → New repository,新建仓库。
3. 仓库名随便写,例如: weather-app ,选择 Public,直接创建。
4. 点击 uploading an existing file,把你用 Trae AI 生成的 index.html 上传上去。
5. 上传完成后,提交到仓库。
第二步:开启 GitHub Pages(获取在线网址)
1. 进入仓库 → 点击上方 Settings。
2. 往下滑找到 Pages。
3. Source 选择:Deploy from a branch。
4. Branch 选择:main,文件夹选 /(root)。
5. 点击 Save,
6. 页面上方会出现你的在线网址
把这个网址复制下来,打包APP要用。
3 第三步:使用 Web to App 在线打包成 APP
1. 打开 Web to App 在线打包网站。
2. 找到 输入网址/生成APP 的功能入口。
3. 填写APP信息:
- APP 名称:简易天气
- 网址:粘贴你刚才复制的 GitHub Pages 网址
- 包名:可以默认,也可以自己写
- 图标:可以自己上传一张图片
4. 选择打包平台:Android(安卓)。
5. 点击 开始生成 / 打包。
6. 等待打包完成,直接下载 APK 文件。
第四步:安装至手机·
1.将APK文件通过微信或者QQ发送到手机
2.安装下载,到这里就成功啦
APP效果:

更多推荐



所有评论(0)