diff --git a/project/.DS_Store b/project/.DS_Store new file mode 100644 index 0000000..847c370 Binary files /dev/null and b/project/.DS_Store differ diff --git a/project/202506050229-孙文轩-期末实验报告.docx b/project/202506050229-孙文轩-期末实验报告.docx new file mode 100644 index 0000000..69f41fa Binary files /dev/null and b/project/202506050229-孙文轩-期末实验报告.docx differ diff --git a/project/WeatherMain.java b/project/WeatherMain.java new file mode 100644 index 0000000..6bc9309 --- /dev/null +++ b/project/WeatherMain.java @@ -0,0 +1,1383 @@ +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.*; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * ========================================================== + + * + * 功能: + * 1. 天气爬虫 + * 2. 空气质量爬虫 + * 3. 日出日落爬虫 + * 4. 湿度爬虫 + * 5. 风速风向爬虫 + * + * 技术: + * √ Java面向对象 + * √ 抽象类 + * √ 泛型 + * √ 多态 + * √ MVC思想 + * √ Command模式 + * √ Strategy模式 + * √ 数据可视化 + * √ 文件持久化 + * ========================================================== + */ + +/* ========================================================== + 1. 自定义异常 +========================================================== */ +class CrawlerException extends Exception { + + public CrawlerException(String message) { + super(message); + } + + public CrawlerException(String message, Throwable cause) { + super(message, cause); + } +} + +/* ========================================================== + 2. 泛型接口 +========================================================== */ +interface DataCrawler { + + T crawlData(K key) throws CrawlerException; + + Map parseData(T rawData); + + void saveData(K key, Map data) + throws CrawlerException; +} + +interface DataVisualizer { + + void generateVisualization(String title, T data); +} + +/* ========================================================== + 3. 保存策略 +========================================================== */ +interface SaveStrategy { + + void save(String fileName, String content) + throws IOException; +} + +class TxtSaveStrategy implements SaveStrategy { + + @Override + public void save(String fileName, String content) + throws IOException { + + File folder = new File("data"); + + if (!folder.exists()) { + folder.mkdirs(); + } + + File file = new File(folder, fileName); + + try (BufferedWriter writer = + new BufferedWriter( + new FileWriter(file) + )) { + + writer.write(content); + } + } +} + +/* ========================================================== + 4. 抽象爬虫父类 +========================================================== */ +abstract class AbstractDataCrawler + implements DataCrawler { + + protected Map rawDataMap = + new HashMap<>(); + + protected Map> + parsedDataMap = + new LinkedHashMap<>(); + + protected SaveStrategy saveStrategy = + new TxtSaveStrategy(); + + protected String doHttpGet(String url) + throws CrawlerException { + + try { + + HttpClient client = + HttpClient.newBuilder() + .connectTimeout( + Duration.ofSeconds(10) + ) + .build(); + + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + + HttpResponse response = + client.send( + request, + HttpResponse.BodyHandlers.ofString() + ); + + if (response.statusCode() != 200) { + + throw new CrawlerException( + "HTTP请求失败:" + + response.statusCode() + ); + } + + return response.body(); + + } catch (Exception e) { + + throw new CrawlerException( + "网络请求异常", + e + ); + } + } + + protected abstract String getApiUrl(K key); +} + +/* ========================================================== + 5. 城市坐标工具类 +========================================================== */ +class CityCoordinateUtil { + + public static final Map + CITY_MAP = + new LinkedHashMap<>(); + + static { + + CITY_MAP.put( + "西安", + new String[]{"34.2644", "108.9497"} + ); + + CITY_MAP.put( + "成都", + new String[]{"30.5728", "104.0668"} + ); + + CITY_MAP.put( + "兰州", + new String[]{"36.0611", "103.8343"} + ); + + CITY_MAP.put( + "乌鲁木齐", + new String[]{"43.8256", "87.6168"} + ); + } +} + +/* ========================================================== + 6. 天气爬虫 +========================================================== */ +class WeatherCrawler + extends AbstractDataCrawler + implements DataVisualizer>> { + + @Override + public String crawlData(String city) + throws CrawlerException { + + System.out.println( + "🌤️ 正在爬取天气:" + city + ); + + String rawData = + doHttpGet(getApiUrl(city)); + + rawDataMap.put(city, rawData); + + return rawData; + } + + @Override + public Map parseData( + String rawData + ) { + + Map result = + new HashMap<>(); + + List temps = + new ArrayList<>(); + + Matcher matcher = + Pattern.compile( + "\"temperature_2m\":\\[([^\\]]+)]" + ).matcher(rawData); + + if (matcher.find()) { + + String[] arr = + matcher.group(1).split(","); + + for (String s : arr) { + + try { + + temps.add( + Double.parseDouble( + s.trim() + ) + ); + + } catch (Exception ignored) { + } + } + } + + result.put("temps", temps); + + return result; + } + + @Override + public void saveData( + String city, + Map data + ) throws CrawlerException { + + parsedDataMap.put(city, data); + + try { + + saveStrategy.save( + city + "_weather.txt", + data.toString() + ); + + } catch (IOException e) { + + throw new CrawlerException( + "天气保存失败", + e + ); + } + } + + @Override + protected String getApiUrl(String city) { + + String[] latLon = + CityCoordinateUtil.CITY_MAP.get(city); + + return String.format( + "https://api.open-meteo.com/v1/forecast?" + + "latitude=%s&longitude=%s" + + "&hourly=temperature_2m", + latLon[0], + latLon[1] + ); + } + + @Override + public void generateVisualization( + String city, + Map> data + ) { + + ChartUtil.drawLineChart( + city + " 温度折线图", + (List) data.get("temps"), + "data/" + city + "_weather.png", + Color.RED + ); + } +} + +/* ========================================================== + 7. 空气质量爬虫 +========================================================== */ +class AirQualityCrawler + extends AbstractDataCrawler + implements DataVisualizer>> { + + @Override + public String crawlData(String city) + throws CrawlerException { + + System.out.println( + "🌫️ 正在爬取AQI:" + city + ); + + String rawData = + doHttpGet(getApiUrl(city)); + + rawDataMap.put(city, rawData); + + return rawData; + } + + @Override + public Map parseData( + String rawData + ) { + + Map result = + new HashMap<>(); + + Matcher matcher = + Pattern.compile("\"aqi\":(\\d+)") + .matcher(rawData); + + int aqi = + matcher.find() + ? Integer.parseInt( + matcher.group(1) + ) + : -1; + + List list = + new ArrayList<>(); + + list.add((double) aqi); + + result.put("aqi", list); + + return result; + } + + @Override + public void saveData( + String city, + Map data + ) throws CrawlerException { + + parsedDataMap.put(city, data); + + try { + + saveStrategy.save( + city + "_aqi.txt", + data.toString() + ); + + } catch (IOException e) { + + throw new CrawlerException( + "AQI保存失败", + e + ); + } + } + + @Override + protected String getApiUrl(String city) { + + String[] latLon = + CityCoordinateUtil.CITY_MAP.get(city); + + return "https://api.waqi.info/feed/geo:" + + latLon[0] + + ";" + + latLon[1] + + "/?token=demo"; + } + + @Override + public void generateVisualization( + String city, + Map> data + ) { + + ChartUtil.drawBarChart( + city + " AQI柱状图", + (List) data.get("aqi"), + "data/" + city + "_aqi.png", + Color.BLUE + ); + } +} + +/* ========================================================== + 8. 日出日落爬虫 +========================================================== */ +class SunriseCrawler + extends AbstractDataCrawler + implements DataVisualizer>> { + + @Override + public String crawlData(String city) + throws CrawlerException { + + System.out.println( + "🌅 正在爬取日出日落:" + city + ); + + String rawData = + doHttpGet(getApiUrl(city)); + + rawDataMap.put(city, rawData); + + return rawData; + } + + @Override + public Map parseData( + String rawData + ) { + + Map result = + new HashMap<>(); + + Matcher sunriseMatcher = + Pattern.compile( + "\"sunrise\":\"([^\"]+)\"" + ).matcher(rawData); + + Matcher sunsetMatcher = + Pattern.compile( + "\"sunset\":\"([^\"]+)\"" + ).matcher(rawData); + + String sunrise = + sunriseMatcher.find() + ? sunriseMatcher.group(1) + : ""; + + String sunset = + sunsetMatcher.find() + ? sunsetMatcher.group(1) + : ""; + + double duration = + calculateHours( + sunrise, + sunset + ); + + List list = + new ArrayList<>(); + + list.add(duration); + + result.put("duration", list); + + return result; + } + + private double calculateHours( + String sunrise, + String sunset + ) { + + try { + + int sunriseHour = + Integer.parseInt( + sunrise.substring(11, 13) + ); + + int sunsetHour = + Integer.parseInt( + sunset.substring(11, 13) + ); + + return sunsetHour - sunriseHour; + + } catch (Exception e) { + + return 0; + } + } + + @Override + public void saveData( + String city, + Map data + ) throws CrawlerException { + + parsedDataMap.put(city, data); + + try { + + saveStrategy.save( + city + "_sunrise.txt", + data.toString() + ); + + } catch (IOException e) { + + throw new CrawlerException( + "日出日落保存失败", + e + ); + } + } + + @Override + protected String getApiUrl(String city) { + + String[] latLon = + CityCoordinateUtil.CITY_MAP.get(city); + + return "https://api.sunrise-sunset.org/json?" + + "lat=" + + latLon[0] + + "&lng=" + + latLon[1] + + "&formatted=0"; + } + + @Override + public void generateVisualization( + String city, + Map> data + ) { + + ChartUtil.drawBarChart( + city + " 日照时长图", + (List) data.get("duration"), + "data/" + city + "_sunrise.png", + Color.ORANGE + ); + } +} + +/* ========================================================== + 9. 湿度爬虫 +========================================================== */ +class HumidityCrawler + extends AbstractDataCrawler + implements DataVisualizer>> { + + @Override + public String crawlData(String city) + throws CrawlerException { + + System.out.println( + "💧 正在爬取湿度:" + city + ); + + String rawData = + doHttpGet(getApiUrl(city)); + + rawDataMap.put(city, rawData); + + return rawData; + } + + @Override + public Map parseData( + String rawData + ) { + + Map result = + new HashMap<>(); + + List humidityList = + new ArrayList<>(); + + Matcher matcher = + Pattern.compile( + "\"relativehumidity_2m\":\\[([^\\]]+)]" + ).matcher(rawData); + + if (matcher.find()) { + + String[] arr = + matcher.group(1).split(","); + + for (String s : arr) { + + try { + + humidityList.add( + Double.parseDouble( + s.trim() + ) + ); + + } catch (Exception ignored) { + } + } + } + + result.put("humidity", humidityList); + + return result; + } + + @Override + public void saveData( + String city, + Map data + ) throws CrawlerException { + + parsedDataMap.put(city, data); + + try { + + saveStrategy.save( + city + "_humidity.txt", + data.toString() + ); + + } catch (IOException e) { + + throw new CrawlerException( + "湿度保存失败", + e + ); + } + } + + @Override + protected String getApiUrl(String city) { + + String[] latLon = + CityCoordinateUtil.CITY_MAP.get(city); + + return String.format( + "https://api.open-meteo.com/v1/forecast?" + + "latitude=%s&longitude=%s" + + "&hourly=relativehumidity_2m", + latLon[0], + latLon[1] + ); + } + + @Override + public void generateVisualization( + String city, + Map> data + ) { + + ChartUtil.drawLineChart( + city + " 湿度折线图", + (List) data.get("humidity"), + "data/" + city + "_humidity.png", + Color.CYAN + ); + } +} + +/* ========================================================== + 10. 风速风向爬虫 +========================================================== */ +class WindCrawler + extends AbstractDataCrawler + implements DataVisualizer>> { + + @Override + public String crawlData(String city) + throws CrawlerException { + + System.out.println( + "💨 正在爬取风速风向:" + city + ); + + String rawData = + doHttpGet(getApiUrl(city)); + + rawDataMap.put(city, rawData); + + return rawData; + } + + @Override + public Map parseData( + String rawData + ) { + + Map result = + new HashMap<>(); + + List windList = + new ArrayList<>(); + + Matcher matcher = + Pattern.compile( + "\"windspeed_10m\":\\[([^\\]]+)]" + ).matcher(rawData); + + if (matcher.find()) { + + String[] arr = + matcher.group(1).split(","); + + for (String s : arr) { + + try { + + windList.add( + Double.parseDouble( + s.trim() + ) + ); + + } catch (Exception ignored) { + } + } + } + + result.put("wind", windList); + + return result; + } + + @Override + public void saveData( + String city, + Map data + ) throws CrawlerException { + + parsedDataMap.put(city, data); + + try { + + saveStrategy.save( + city + "_wind.txt", + data.toString() + ); + + } catch (IOException e) { + + throw new CrawlerException( + "风速保存失败", + e + ); + } + } + + @Override + protected String getApiUrl(String city) { + + String[] latLon = + CityCoordinateUtil.CITY_MAP.get(city); + + return String.format( + "https://api.open-meteo.com/v1/forecast?" + + "latitude=%s&longitude=%s" + + "&hourly=windspeed_10m", + latLon[0], + latLon[1] + ); + } + + @Override + public void generateVisualization( + String city, + Map> data + ) { + + ChartUtil.drawLineChart( + city + " 风速折线图", + (List) data.get("wind"), + "data/" + city + "_wind.png", + Color.MAGENTA + ); + } +} + +/* ========================================================== + 11. 图表工具类(核心修改部分) +========================================================== */ +class ChartUtil { + + // 坐标轴字体样式 + private static final Font AXIS_FONT = new Font("微软雅黑", Font.PLAIN, 12); + // 刻度间隔(控制标注密度) + private static final int X_AXIS_TICK_INTERVAL = 5; + private static final int Y_AXIS_TICK_COUNT = 6; + + public static void drawLineChart( + String title, + List data, + String path, + Color color + ) { + + drawChart( + title, + data, + path, + color, + true + ); + } + + public static void drawBarChart( + String title, + List data, + String path, + Color color + ) { + + drawChart( + title, + data, + path, + color, + false + ); + } + + private static void drawChart( + String title, + List data, + String path, + Color color, + boolean lineMode + ) { + + try { + + int width = 1000; + int height = 500; + + BufferedImage image = + new BufferedImage( + width, + height, + BufferedImage.TYPE_INT_RGB + ); + + Graphics2D g = + image.createGraphics(); + // 开启抗锯齿,让文字和线条更清晰 + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // 1. 绘制背景 + g.setColor(Color.WHITE); + g.fillRect(0, 0, width, height); + + // 2. 绘制标题 + g.setColor(Color.BLACK); + g.setFont(new Font("微软雅黑", Font.BOLD, 24)); + g.drawString(title, 320, 40); + + // 3. 绘制坐标轴 + g.drawLine(80, 400, 900, 400); // X轴 + g.drawLine(80, 80, 80, 400); // Y轴 + + // 4. 计算数据范围 + double maxValue = data.stream().mapToDouble(Double::doubleValue).max().orElse(1); + double minValue = data.stream().mapToDouble(Double::doubleValue).min().orElse(0); + // 保证Y轴从0开始(更符合常规图表逻辑) + minValue = Math.min(minValue, 0); + double valueRange = maxValue - minValue; + // 避免除零 + if (valueRange == 0) valueRange = 1; + + // 5. 绘制坐标轴标注(核心新增逻辑) + drawAxisLabels(g, data, minValue, maxValue, valueRange); + + // 6. 绘制数据图形(折线/柱状图) + g.setColor(color); + int prevX = 0; + int prevY = 0; + + for (int i = 0; i < data.size(); i++) { + + int x = 100 + i * 30; + // 修正Y轴计算逻辑(适配minValue) + int y = (int) (400 - ((data.get(i) - minValue) / valueRange * 250)); + + if (lineMode) { + // 折线图:绘制数据点和连线 + g.fillOval(x - 3, y - 3, 6, 6); + if (i > 0) { + g.drawLine(prevX, prevY, x, y); + } + prevX = x; + prevY = y; + } else { + // 柱状图:绘制柱子 + g.fillRect(x - 10, y, 20, 400 - y); + } + } + + // 7. 释放资源并保存图片 + g.dispose(); + ImageIO.write(image, "png", new File(path)); + + System.out.println("📈 图表生成成功:" + path); + + } catch (Exception e) { + System.err.println("❌ 图表生成失败:" + e.getMessage()); + } + } + + /** + * 绘制坐标轴标注(面向对象封装:单一职责) + * @param g 绘图上下文 + * @param data 原始数据 + * @param minValue 数据最小值 + * @param maxValue 数据最大值 + * @param valueRange 数据范围(max - min) + */ + private static void drawAxisLabels(Graphics2D g, List data, double minValue, double maxValue, double valueRange) { + // 设置坐标轴字体 + g.setFont(AXIS_FONT); + g.setColor(Color.DARK_GRAY); + + // ===== 绘制Y轴标注(数值刻度)===== + double yTickStep = valueRange / (Y_AXIS_TICK_COUNT - 1); + for (int i = 0; i < Y_AXIS_TICK_COUNT; i++) { + // 计算刻度值 + double tickValue = minValue + (yTickStep * i); + // 计算刻度在图片中的Y坐标 + int yPos = (int) (400 - (i * 250 / (Y_AXIS_TICK_COUNT - 1))); + // 绘制刻度线 + g.drawLine(75, yPos, 80, yPos); + // 绘制刻度文字(保留1位小数,保证可读性) + String label = String.format("%.1f", tickValue); + // 文字左对齐,居中显示 + FontMetrics fm = g.getFontMetrics(); + int textX = 70 - fm.stringWidth(label); + int textY = yPos + (fm.getAscent() / 2); + g.drawString(label, textX, textY); + } + + // ===== 绘制X轴标注(索引刻度)===== + for (int i = 0; i < data.size(); i += X_AXIS_TICK_INTERVAL) { + // 计算刻度在图片中的X坐标 + int xPos = 100 + i * 30; + // 绘制刻度线 + g.drawLine(xPos, 400, xPos, 405); + // 绘制刻度文字(数据索引) + String label = String.valueOf(i); + FontMetrics fm = g.getFontMetrics(); + int textX = xPos - (fm.stringWidth(label) / 2); + int textY = 400 + fm.getAscent() + 5; + g.drawString(label, textX, textY); + } + + // ===== 绘制坐标轴名称 ===== + g.setFont(new Font("微软雅黑", Font.PLAIN, 14)); + // X轴名称(数据点索引) + g.drawString("数据点索引", 450, 450); + // Y轴名称(数值) + // 旋转文字:Y轴名称垂直显示 + g.rotate(Math.toRadians(-90), 40, 240); + g.drawString("数值", 40, 240); + // 恢复旋转角度 + g.rotate(Math.toRadians(90), 40, 240); + } +} + +/* ========================================================== + 12. Command模式 +========================================================== */ +interface Command { + void execute(); +} + +abstract class AbstractCommand + implements Command { + + protected CrawlerPlatform platform; + protected String city; + + public AbstractCommand( + CrawlerPlatform platform, + String city + ) { + + this.platform = platform; + this.city = city; + } +} + +class WeatherCommand extends AbstractCommand { + + public WeatherCommand( + CrawlerPlatform platform, + String city + ) { + + super(platform, city); + } + + @Override + public void execute() { + platform.runCrawler("weather", city); + } +} + +class AQICommand extends AbstractCommand { + + public AQICommand( + CrawlerPlatform platform, + String city + ) { + + super(platform, city); + } + + @Override + public void execute() { + platform.runCrawler("aqi", city); + } +} + +class SunriseCommand extends AbstractCommand { + + public SunriseCommand( + CrawlerPlatform platform, + String city + ) { + + super(platform, city); + } + + @Override + public void execute() { + platform.runCrawler("sunrise", city); + } +} + +class HumidityCommand extends AbstractCommand { + + public HumidityCommand( + CrawlerPlatform platform, + String city + ) { + + super(platform, city); + } + + @Override + public void execute() { + platform.runCrawler("humidity", city); + } +} + +class WindCommand extends AbstractCommand { + + public WindCommand( + CrawlerPlatform platform, + String city + ) { + + super(platform, city); + } + + @Override + public void execute() { + platform.runCrawler("wind", city); + } +} + +/* ========================================================== + 13. 平台控制器 +========================================================== */ +class CrawlerPlatform { + + private final Map> + crawlerMap = + new HashMap<>(); + + public void registerCrawler( + String type, + DataCrawler crawler + ) { + + crawlerMap.put(type, crawler); + + System.out.println( + "✅ 注册爬虫:" + type + ); + } + + @SuppressWarnings("unchecked") + public void runCrawler( + String type, + K key + ) { + + try { + + DataCrawler crawler = + (DataCrawler) + crawlerMap.get(type); + + T rawData = + crawler.crawlData(key); + + Map parsedData = + crawler.parseData(rawData); + + crawler.saveData( + key, + parsedData + ); + + if (crawler instanceof DataVisualizer) { + + ((DataVisualizer>>) + crawler) + .generateVisualization( + key.toString(), + convertMap(parsedData) + ); + } + + } catch (Exception e) { + + System.err.println( + "❌ 爬虫执行失败:" + + e.getMessage() + ); + } + } + + private Map> convertMap( + Map map + ) { + + Map> result = + new HashMap<>(); + + for (String key : map.keySet()) { + + if (map.get(key) instanceof List) { + + result.put( + key, + (List) map.get(key) + ); + } + } + + return result; + } +} + +/* ========================================================== + 14. CLI视图 +========================================================== */ +class CLIView { + + public void showMenu() { + + System.out.println("\n=============================="); + System.out.println(" 城市环境综合数据爬虫平台"); + System.out.println("=============================="); + System.out.println("1. 天气爬虫"); + System.out.println("2. 空气质量爬虫"); + System.out.println("3. 日出日落爬虫"); + System.out.println("4. 湿度爬虫"); + System.out.println("5. 风速风向爬虫"); + System.out.println("6. 执行全部功能"); + System.out.println("0. 退出系统"); + System.out.println("=============================="); + } +} + +/* ========================================================== + 15. 主程序 +========================================================== */ +public class WeatherMain { + + public static void main(String[] args) { + + System.out.println( + "🚀 系统启动时间:" + + LocalDateTime.now() + .format( + DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss" + ) + ) + ); + + Scanner scanner = + new Scanner(System.in); + + CLIView view = + new CLIView(); + + CrawlerPlatform platform = + new CrawlerPlatform(); + + platform.registerCrawler( + "weather", + new WeatherCrawler() + ); + + platform.registerCrawler( + "aqi", + new AirQualityCrawler() + ); + + platform.registerCrawler( + "sunrise", + new SunriseCrawler() + ); + + platform.registerCrawler( + "humidity", + new HumidityCrawler() + ); + + platform.registerCrawler( + "wind", + new WindCrawler() + ); + + List cities = + new ArrayList<>( + CityCoordinateUtil.CITY_MAP.keySet() + ); + + while (true) { + + view.showMenu(); + + System.out.print("请输入操作:"); + + int choice = + scanner.nextInt(); + + if (choice == 0) { + + System.out.println( + "👋 系统退出" + ); + + break; + } + + switch (choice) { + + case 1: + + for (String city : cities) { + + new WeatherCommand( + platform, + city + ).execute(); + } + + break; + + case 2: + + for (String city : cities) { + + new AQICommand( + platform, + city + ).execute(); + } + + break; + + case 3: + + for (String city : cities) { + + new SunriseCommand( + platform, + city + ).execute(); + } + + break; + + case 4: + + for (String city : cities) { + + new HumidityCommand( + platform, + city + ).execute(); + } + + break; + + case 5: + + for (String city : cities) { + + new WindCommand( + platform, + city + ).execute(); + } + + break; + + case 6: + + for (String city : cities) { + + new WeatherCommand( + platform, + city + ).execute(); + + new AQICommand( + platform, + city + ).execute(); + + new SunriseCommand( + platform, + city + ).execute(); + + new HumidityCommand( + platform, + city + ).execute(); + + new WindCommand( + platform, + city + ).execute(); + } + + break; + + default: + + System.out.println( + "⚠️ 输入错误" + ); + } + } + + scanner.close(); + + System.out.println( + "🎉 系统运行结束" + ); + } +} \ No newline at end of file diff --git a/project/乌鲁木齐/乌鲁木齐_aqi.png b/project/乌鲁木齐/乌鲁木齐_aqi.png new file mode 100644 index 0000000..4e82ec6 Binary files /dev/null and b/project/乌鲁木齐/乌鲁木齐_aqi.png differ diff --git a/project/乌鲁木齐/乌鲁木齐_humidity.png b/project/乌鲁木齐/乌鲁木齐_humidity.png new file mode 100644 index 0000000..cbc40f6 Binary files /dev/null and b/project/乌鲁木齐/乌鲁木齐_humidity.png differ diff --git a/project/乌鲁木齐/乌鲁木齐_sunrise.png b/project/乌鲁木齐/乌鲁木齐_sunrise.png new file mode 100644 index 0000000..f22af17 Binary files /dev/null and b/project/乌鲁木齐/乌鲁木齐_sunrise.png differ diff --git a/project/乌鲁木齐/乌鲁木齐_weather.png b/project/乌鲁木齐/乌鲁木齐_weather.png new file mode 100644 index 0000000..21336cf Binary files /dev/null and b/project/乌鲁木齐/乌鲁木齐_weather.png differ diff --git a/project/乌鲁木齐/乌鲁木齐_wind.png b/project/乌鲁木齐/乌鲁木齐_wind.png new file mode 100644 index 0000000..98bfe73 Binary files /dev/null and b/project/乌鲁木齐/乌鲁木齐_wind.png differ diff --git a/project/兰州/兰州_aqi.png b/project/兰州/兰州_aqi.png new file mode 100644 index 0000000..24bcab9 Binary files /dev/null and b/project/兰州/兰州_aqi.png differ diff --git a/project/兰州/兰州_humidity.png b/project/兰州/兰州_humidity.png new file mode 100644 index 0000000..689c9b4 Binary files /dev/null and b/project/兰州/兰州_humidity.png differ diff --git a/project/兰州/兰州_sunrise.png b/project/兰州/兰州_sunrise.png new file mode 100644 index 0000000..1768b29 Binary files /dev/null and b/project/兰州/兰州_sunrise.png differ diff --git a/project/兰州/兰州_weather.png b/project/兰州/兰州_weather.png new file mode 100644 index 0000000..df0cdfc Binary files /dev/null and b/project/兰州/兰州_weather.png differ diff --git a/project/兰州/兰州_wind.png b/project/兰州/兰州_wind.png new file mode 100644 index 0000000..ef8c9ce Binary files /dev/null and b/project/兰州/兰州_wind.png differ diff --git a/project/成都/成都_aqi.png b/project/成都/成都_aqi.png new file mode 100644 index 0000000..ef849f2 Binary files /dev/null and b/project/成都/成都_aqi.png differ diff --git a/project/成都/成都_humidity.png b/project/成都/成都_humidity.png new file mode 100644 index 0000000..c319ad3 Binary files /dev/null and b/project/成都/成都_humidity.png differ diff --git a/project/成都/成都_sunrise.png b/project/成都/成都_sunrise.png new file mode 100644 index 0000000..4984da3 Binary files /dev/null and b/project/成都/成都_sunrise.png differ diff --git a/project/成都/成都_weather.png b/project/成都/成都_weather.png new file mode 100644 index 0000000..d660447 Binary files /dev/null and b/project/成都/成都_weather.png differ diff --git a/project/成都/成都_wind.png b/project/成都/成都_wind.png new file mode 100644 index 0000000..9ec08be Binary files /dev/null and b/project/成都/成都_wind.png differ diff --git a/project/西安/西安_aqi.png b/project/西安/西安_aqi.png new file mode 100644 index 0000000..99a1426 Binary files /dev/null and b/project/西安/西安_aqi.png differ diff --git a/project/西安/西安_humidity.png b/project/西安/西安_humidity.png new file mode 100644 index 0000000..0638b55 Binary files /dev/null and b/project/西安/西安_humidity.png differ diff --git a/project/西安/西安_sunrise.png b/project/西安/西安_sunrise.png new file mode 100644 index 0000000..c6c52aa Binary files /dev/null and b/project/西安/西安_sunrise.png differ diff --git a/project/西安/西安_weather.png b/project/西安/西安_weather.png new file mode 100644 index 0000000..e4db155 Binary files /dev/null and b/project/西安/西安_weather.png differ diff --git a/project/西安/西安_wind.png b/project/西安/西安_wind.png new file mode 100644 index 0000000..66a3d83 Binary files /dev/null and b/project/西安/西安_wind.png differ diff --git a/project/运行成功截图/.DS_Store b/project/运行成功截图/.DS_Store new file mode 100644 index 0000000..ce555bb Binary files /dev/null and b/project/运行成功截图/.DS_Store differ diff --git a/project/运行成功截图/截屏2026-05-28 09.34.32.png b/project/运行成功截图/截屏2026-05-28 09.34.32.png new file mode 100644 index 0000000..d084a57 Binary files /dev/null and b/project/运行成功截图/截屏2026-05-28 09.34.32.png differ diff --git a/project/运行成功截图/截屏2026-05-28 09.34.52.png b/project/运行成功截图/截屏2026-05-28 09.34.52.png new file mode 100644 index 0000000..8a32407 Binary files /dev/null and b/project/运行成功截图/截屏2026-05-28 09.34.52.png differ diff --git a/project/运行成功截图/截屏2026-05-28 09.35.06.png b/project/运行成功截图/截屏2026-05-28 09.35.06.png new file mode 100644 index 0000000..591d39d Binary files /dev/null and b/project/运行成功截图/截屏2026-05-28 09.35.06.png differ diff --git a/project/运行成功截图/截屏2026-05-28 09.35.06_副本.png b/project/运行成功截图/截屏2026-05-28 09.35.06_副本.png new file mode 100644 index 0000000..591d39d Binary files /dev/null and b/project/运行成功截图/截屏2026-05-28 09.35.06_副本.png differ diff --git a/project/运行成功截图/截屏2026-05-28 09.35.20.png b/project/运行成功截图/截屏2026-05-28 09.35.20.png new file mode 100644 index 0000000..1537790 Binary files /dev/null and b/project/运行成功截图/截屏2026-05-28 09.35.20.png differ diff --git a/project/运行成功截图/截屏2026-05-28 09.35.26.png b/project/运行成功截图/截屏2026-05-28 09.35.26.png new file mode 100644 index 0000000..76b8dc0 Binary files /dev/null and b/project/运行成功截图/截屏2026-05-28 09.35.26.png differ