[form_tag]submit之后地址栏多出的X,y参数

本文解释了一个常见的网页表单问题,即在表单提交时URL中出现x和y坐标的情况。这通常是因为使用图片作为提交按钮导致的,点击图片的不同位置会记录下对应的坐标。文章提供了具体的解决方案。
明明网页form表单中没有X,Y,而提交的时候地址栏中却有x=num&y=num等字样。

原因是:

在form表单中用一张图片代替了传统的sumit按钮造成的。


<%= image_submit_tag...%>


x和y的坐标是你点到那个图上的坐标。

解决办法:

<% form_tag(:action, :method => :get, :onsubmit => "this.submit();return false;") do -%>
......
<% end -%>
import tkinter as tk from tkinter import ttk, simpledialog, messagebox, filedialog import pandas as pd import subprocess import queue from datetime import datetime import concurrent.futures import time import os import re from PIL import Image, ImageDraw, ImageFont import numpy as np class SettingsDialog(tk.Toplevel): """设置对话框""" def __init__(self, parent, current_settings): super().__init__(parent) self.title("监控设置") self.geometry("400x350") self.parent = parent self.settings = current_settings.copy() self.result = None # 使对话框模态化 self.transient(parent) self.grab_set() # 创建表单框架 form_frame = tk.Frame(self, padx=10, pady=10) form_frame.pack(fill=tk.BOTH, expand=True) # 线程数设置 tk.Label(form_frame, text="并发线程数 (1-1000):").grid(row=0, column=0, sticky=tk.W, pady=5) self.threads_var = tk.IntVar(value=self.settings['thread_count']) tk.Spinbox(form_frame, from_=1, to=1000, textvariable=self.threads_var, width=10).grid(row=0, column=1, sticky=tk.W, padx=5) # Ping间隔设置 tk.Label(form_frame, text="Ping间隔 (秒):").grid(row=1, column=0, sticky=tk.W, pady=5) self.interval_var = tk.DoubleVar(value=self.settings['ping_interval']) tk.Spinbox(form_frame, from_=0.1, to=10, increment=0.1, textvariable=self.interval_var, width=10).grid(row=1, column=1, sticky=tk.W, padx=5) # Ping超时设置 tk.Label(form_frame, text="Ping超时 (毫秒):").grid(row=2, column=0, sticky=tk.W, pady=5) self.timeout_var = tk.IntVar(value=self.settings['ping_timeout']) tk.Spinbox(form_frame, from_=100, to=5000, increment=100, textvariable=self.timeout_var, width=10).grid(row=2, column=1, sticky=tk.W, padx=5) # Excel文件路径 tk.Label(form_frame, text="Excel文件路径:").grid(row=3, column=0, sticky=tk.W, pady=5) self.excel_path_var = tk.StringVar(value=self.settings['excel_path']) tk.Entry(form_frame, textvariable=self.excel_path_var, width=30).grid(row=3, column=1, sticky=tk.W, padx=5) tk.Button(form_frame, text="浏览...", command=self.browse_excel).grid(row=3, column=2, padx=5) # 断开计数阈值 tk.Label(form_frame, text="断开报警阈值:").grid(row=4, column=0, sticky=tk.W, pady=5) self.threshold_var = tk.IntVar(value=self.settings['disconnect_threshold']) tk.Spinbox(form_frame, from_=1, to=100, increment=1, textvariable=self.threshold_var, width=10).grid(row=4, column=1, sticky=tk.W, padx=5) # 关键设备类型 tk.Label(form_frame, text="关键设备类型:").grid(row=5, column=0, sticky=tk.W, pady=5) self.critical_types_var = tk.StringVar(value=",".join(self.settings['critical_types'])) tk.Entry(form_frame, textvariable=self.critical_types_var, width=30).grid(row=5, column=1, columnspan=2, sticky=tk.W, padx=5) tk.Label(form_frame, text="(逗号分隔,如:二层交换机,三层交换机,服务器,UPS)").grid(row=6, column=1, sticky=tk.W, padx=5) # 按钮框架 button_frame = tk.Frame(self) button_frame.pack(pady=10) tk.Button(button_frame, text="确定", width=10, command=self.ok).grid(row=0, column=0, padx=10) tk.Button(button_frame, text="取消", width=10, command=self.cancel).grid(row=0, column=1, padx=10) self.protocol("WM_DELETE_WINDOW", self.cancel) def browse_excel(self): """浏览Excel文件""" file_path = filedialog.askopenfilename( filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")], title="选择设备列表Excel文件" ) if file_path: self.excel_path_var.set(file_path) def ok(self): """确定按钮处理""" self.settings = { 'thread_count': self.threads_var.get(), 'ping_interval': self.interval_var.get(), 'ping_timeout': self.timeout_var.get(), 'excel_path': self.excel_path_var.get(), 'disconnect_threshold': self.threshold_var.get(), 'critical_types': [t.strip() for t in self.critical_types_var.get().split(",")] } self.destroy() def cancel(self): """取消按钮处理""" self.settings = None self.destroy() def show(self): """显示对话框并返回设置""" self.wait_window() return self.settings class PingMonitorApp: def __init__(self, root): self.root = root self.root.title("IP Ping 监控工具 - 专业版") self.root.geometry("1100x700") # 窗口大小 # 控制面板框架 control_frame = tk.Frame(root) control_frame.pack(fill=tk.X, padx=10, pady=5) # 操作按钮框架 button_frame = tk.Frame(control_frame) button_frame.pack(side=tk.LEFT) # 开始/停止按钮 self.start_btn = tk.Button(button_frame, text="开始监控", command=self.toggle_monitoring, width=10) self.start_btn.pack(side=tk.LEFT, padx=5) # 设置按钮 self.settings_btn = tk.Button(button_frame, text="设置", command=self.open_settings, width=10) self.settings_btn.pack(side=tk.LEFT, padx=5) # 导出按钮 export_frame = tk.Frame(control_frame) export_frame.pack(side=tk.LEFT, padx=20) tk.Label(export_frame, text="导出断网设备:").pack(side=tk.LEFT) self.export_excel_btn = tk.Button(export_frame, text="Excel", command=self.export_disconnected_to_excel, width=8) self.export_excel_btn.pack(side=tk.LEFT, padx=2) self.export_txt_btn = tk.Button(export_frame, text="TXT", command=self.export_disconnected_to_txt, width=8) self.export_txt_btn.pack(side=tk.LEFT, padx=2) self.export_img_btn = tk.Button(export_frame, text="图片", command=self.export_disconnected_to_image, width=8) self.export_img_btn.pack(side=tk.LEFT, padx=2) # 状态指示框架 status_frame = tk.Frame(control_frame) status_frame.pack(side=tk.RIGHT) # 状态指示器 self.status_indicator = tk.Canvas(status_frame, width=20, height=20) self.status_indicator.pack(side=tk.LEFT, padx=5) self.draw_indicator("gray") # 初始灰色 # 速度显示 self.speed_label = tk.Label(status_frame, text="速度: --") self.speed_label.pack(side=tk.LEFT, padx=10) # 断开设备计数 self.disconnected_count = tk.IntVar(value=0) tk.Label(status_frame, text="断开设备:").pack(side=tk.LEFT) self.disconnected_label = tk.Label(status_frame, textvariable=self.disconnected_count) self.disconnected_label.pack(side=tk.LEFT, padx=5) # 创建表格 self.tree = ttk.Treeview(root) self.tree["columns"] = ("#1", "#2", "#3", "#4", "#5", "#6", "#7", "#8") self.tree.column("#0", width=0, stretch=tk.NO) # 隐藏首列 self.tree.column("#1", width=50, anchor=tk.CENTER) # 序号 self.tree.column("#2", width=160, anchor=tk.CENTER) # 时间戳 self.tree.column("#3", width=120, anchor=tk.CENTER) # sheet页名 self.tree.column("#4", width=120, anchor=tk.CENTER) # 设备IP self.tree.column("#5", width=150, anchor=tk.CENTER) # 车站 self.tree.column("#6", width=200, anchor=tk.CENTER) # 设备名称 self.tree.column("#7", width=100, anchor=tk.CENTER) # 断开次数 self.tree.column("#8", width=150, anchor=tk.CENTER) # Ping结果 self.tree.heading("#1", text="序号") self.tree.heading("#2", text="时间戳") self.tree.heading("#3", text="sheet页名") self.tree.heading("#4", text="设备IP") self.tree.heading("#5", text="车站") self.tree.heading("#6", text="设备名称") self.tree.heading("#7", text="断开次数") self.tree.heading("#8", text="Ping结果") self.tree.pack(fill=tk.BOTH, expand=True) # 配置标签颜色 self.tree.tag_configure("critical", background="#ffffcc") # 关键设备断开 - 黄色 self.tree.tag_configure("failed", background="#ffe6e6") # 普通设备超过阈值 - 红色 # 状态栏 self.status_var = tk.StringVar() self.status_bar = tk.Label(root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) # 创建线程安全队列 self.results_queue = queue.Queue() self.device_queue = queue.Queue() # 存储所有设备信息 self.devices = [] self.device_map = {} # IP到设备的映射 # 线程控制 self.running = False self.thread_pool = None self.executor = None # 默认设置 self.settings = { 'thread_count': 100, 'ping_interval': 1.0, 'ping_timeout': 300, # 毫秒 'excel_path': "D:/1.xlsx", 'disconnect_threshold': 5, # 断开次数报警阈值 'critical_types': ['二层交换机', '三层交换机', '服务器', 'UPS'] # 关键设备类型 } # 性能统计 self.last_update_time = time.time() self.ping_count = 0 # 加载IP列表 self.load_ips() # GUI更新循环 self.update_gui() # 关闭窗口时清理资源 self.root.protocol("WM_DELETE_WINDOW", self.on_close) def draw_indicator(self, color): """绘制状态指示灯""" self.status_indicator.delete("all") self.status_indicator.create_oval(2, 2, 18, 18, fill=color, outline="black") def open_settings(self): """打开设置对话框""" # 如果正在运行,先停止监控 was_running = self.running if was_running: self.stop_monitoring() # 打开设置对话框 dialog = SettingsDialog(self.root, self.settings) new_settings = dialog.show() if new_settings: # 应用新设置 self.settings = new_settings self.status_var.set("设置已更新") # 重新加载设备列表 self.load_ips() # 如果之前正在运行,重新启动监控 if was_running: self.start_monitoring() def toggle_monitoring(self): """切换监控状态""" if self.running: self.stop_monitoring() self.start_btn.config(text="开始监控") self.draw_indicator("red") self.status_var.set("监控已停止") else: self.start_monitoring() self.start_btn.config(text="停止监控") self.draw_indicator("green") self.status_var.set("监控已启动") def is_critical_device(self, device_name): """检查设备是否为关键设备""" device_name = str(device_name).lower() for device_type in self.settings['critical_types']: if device_type.lower() in device_name: return True return False def load_ips(self): """从Excel文件加载设备信息""" try: # 读取Excel文件 excel_path = self.settings['excel_path'] if not os.path.exists(excel_path): self.status_var.set(f"错误:文件不存在 - {excel_path}") return xls = pd.ExcelFile(excel_path) # 存储所有设备 self.devices = [] self.device_map = {} device_counter = 1 # 遍历所有sheet页 for sheet_name in xls.sheet_names: df = pd.read_excel(excel_path, sheet_name=sheet_name) # 检查必要列是否存在 required_columns = ["设备IP", "车站", "设备名称"] if not all(col in df.columns for col in required_columns): self.status_var.set(f"错误:Sheet '{sheet_name}' 缺少必要列") continue # 提取设备信息 for _, row in df.iterrows(): ip = row["设备IP"] device_name = row["设备名称"] # 检查设备是否为关键设备 is_critical = self.is_critical_device(device_name) device_info = { "id": device_counter, "sheet": sheet_name, "ip": ip, "station": row["车站"], "device_name": device_name, "is_critical": is_critical, # 标记关键设备 "status": "等待开始...", "timestamp": "", "success": None, "last_failed": False, # 记录最近一次是否失败 "disconnect_count": 0, # 断开次数计数器 "last_ping_time": 0, # 上次Ping时间 "last_status_change": None # 上次状态变更时间 } self.devices.append(device_info) self.device_map[ip] = device_info device_counter += 1 self.status_var.set(f"成功加载 {len(self.devices)} 个设备,点击开始按钮开始监控") # 清空设备队列 while not self.device_queue.empty(): self.device_queue.get_nowait() # 清空表格并添加设备 self.tree.delete(*self.tree.get_children()) for device in self.devices: self.tree.insert("", "end", values=( device["id"], device["timestamp"], device["sheet"], device["ip"], device["station"], device["device_name"], device["disconnect_count"], device["status"] )) # 添加设备到队列 self.device_queue.put(device["ip"]) except Exception as e: self.status_var.set(f"加载Excel文件错误: {str(e)}") def start_monitoring(self): """启动监控服务""" if not self.running: self.running = True self.executor = concurrent.futures.ThreadPoolExecutor( max_workers=self.settings['thread_count']) # 启动线程池工作线程 for _ in range(self.settings['thread_count']): self.executor.submit(self.ping_worker) # 更新设备状态 for device in self.devices: device["status"] = "监控中..." self.status_var.set(f"监控已启动 ({self.settings['thread_count']}线程)") def stop_monitoring(self): """停止监控服务""" if self.running: self.running = False if self.executor: self.executor.shutdown(wait=False) # 更新设备状态 for device in self.devices: device["status"] = "已停止" self.status_var.set("监控已停止") def ping_worker(self): """工作线程:执行Ping操作""" while self.running: try: # 从队列获取设备IP ip = self.device_queue.get(timeout=1.0) # 检查设备是否存在 if ip not in self.device_map: self.device_queue.put(ip) continue device = self.device_map[ip] # 检查是否需要Ping(根据时间间隔) current_time = time.time() if current_time - device["last_ping_time"] < self.settings['ping_interval']: # 还没到时间,放回队列等待 self.device_queue.put(ip) time.sleep(0.01) # 短暂休眠避免忙等待 continue # 更新最后Ping时间 device["last_ping_time"] = current_time # 执行Ping命令 timeout_ms = self.settings['ping_timeout'] result = subprocess.run( ["ping", "-n", "1", "-w", str(timeout_ms), str(ip)], capture_output=True, text=True ) # 解析结果 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") success = "请求超时" not in result.stdout and "无法访问" not in result.stdout status = "成功" if success else "失败" # 更新性能计数 self.ping_count += 1 # 将结果放入队列 self.results_queue.put((ip, timestamp, status, success)) # 将设备放回队列以继续监控 self.device_queue.put(ip) except queue.Empty: # 如果队列为空,短暂休眠后重试 time.sleep(0.1) except Exception as e: self.results_queue.put((None, None, f"错误: {str(e)}", False)) time.sleep(1.0) # 出错时休眠避免频繁错误 def update_gui(self): """更新GUI界面,确保不通的设备显示在最上方""" try: # 更新性能统计 if self.running: current_time = time.time() if current_time - self.last_update_time >= 1.0: # 每秒更新一次速度 speed = self.ping_count / (current_time - self.last_update_time) self.speed_label.config(text=f"速度: {speed:.1f} Ping/秒") self.last_update_time = current_time self.ping_count = 0 # 处理结果队列 disconnected_devices = 0 while not self.results_queue.empty(): ip, timestamp, status, success = self.results_queue.get_nowait() if ip and ip in self.device_map: device = self.device_map[ip] # 记录状态变更 if success != device["success"]: device["last_status_change"] = timestamp # 更新断开次数 if not success: device["disconnect_count"] += 1 device["timestamp"] = timestamp device["status"] = status device["success"] = success device["last_failed"] = not success self.results_queue.task_done() # 重新排序设备:不通的设备在前,通的在后 self.devices.sort(key=lambda x: (not x["last_failed"], x["id"])) # 清空表格并重新添加所有设备(按新顺序) self.tree.delete(*self.tree.get_children()) disconnected_devices = 0 for device in self.devices: if not device["success"]: disconnected_devices += 1 # 根据设备状态和类型设置标签 tags = () if not device["success"]: # 设备断开 if device["is_critical"]: # 关键设备断开(黄色) tags = ("critical",) elif device["disconnect_count"] >= self.settings['disconnect_threshold']: # 普通设备超过阈值(红色) tags = ("failed",) self.tree.insert("", "end", values=( device["id"], device["timestamp"], device["sheet"], device["ip"], device["station"], device["device_name"], device["disconnect_count"], device["status"] ), tags=tags) # 更新断开设备计数 self.disconnected_count.set(disconnected_devices) except queue.Empty: pass except Exception as e: self.status_var.set(f"GUI更新错误: {str(e)}") # 每100毫秒检查一次更新 self.root.after(100, self.update_gui) def on_close(self): """关闭窗口时的清理操作""" self.running = False if self.executor: self.executor.shutdown(wait=False) self.root.destroy() def get_disconnected_devices(self): """获取所有断网设备列表""" return [device for device in self.devices if not device["success"]] def export_disconnected_to_excel(self): """导出断网设备到Excel文件(已取消关键设备列)""" disconnected = self.get_disconnected_devices() if not disconnected: messagebox.showinfo("无断网设备", "当前没有断网设备") return try: # 创建DataFrame data = [] for device in disconnected: data.append({ "序号": device["id"], "时间戳": device["timestamp"], "Sheet页名": device["sheet"], "设备IP": device["ip"], "车站": device["station"], "设备名称": device["device_name"], "断开次数": device["disconnect_count"], "状态": device["status"] }) df = pd.DataFrame(data) # 弹出保存对话框 file_path = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")], title="保存断网设备列表" ) if not file_path: # 用户取消 return # 保存到Excel df.to_excel(file_path, index=False) self.status_var.set(f"成功导出 {len(disconnected)} 个断网设备到 {file_path}") except Exception as e: messagebox.showerror("导出错误", f"导出Excel时出错: {str(e)}") def export_disconnected_to_txt(self): """导出断网设备到TXT文件(已取消关键设备列)""" disconnected = self.get_disconnected_devices() if not disconnected: messagebox.showinfo("无断网设备", "当前没有断网设备") return try: # 弹出保存对话框 file_path = filedialog.asksaveasfilename( defaultextension=".txt", filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")], title="保存断网设备列表" ) if not file_path: # 用户取消 return # 写入文件 with open(file_path, "w", encoding="utf-8") as f: # 写入标题行 f.write("序号\t时间戳\tSheet页名\t设备IP\t车站\t设备名称\t断开次数\t状态\n") # 写入设备数据 for device in disconnected: f.write(f"{device['id']}\t{device['timestamp']}\t{device['sheet']}\t") f.write(f"{device['ip']}\t{device['station']}\t{device['device_name']}\t") f.write(f"{device['disconnect_count']}\t{device['status']}\n") self.status_var.set(f"成功导出 {len(disconnected)} 个断网设备到 {file_path}") except Exception as e: messagebox.showerror("导出错误", f"导出TXT时出错: {str(e)}") def export_disconnected_to_image(self): """导出断网设备到图片文件(已取消关键设备列)""" disconnected = self.get_disconnected_devices() if not disconnected: messagebox.showinfo("无断网设备", "当前没有断网设备") return try: # 弹出保存对话框 file_path = filedialog.asksaveasfilename( defaultextension=".png", filetypes=[("PNG 图片", "*.png"), ("JPEG 图片", "*.jpg"), ("所有文件", "*.*")], title="保存断网设备列表图片" ) if not file_path: # 用户取消 return # 创建图片 columns = ["序号", "时间戳", "Sheet页名", "设备IP", "车站", "设备名称", "断开次数", "状态"] row_height = 30 col_widths = [50, 160, 120, 120, 150, 200, 100, 150] header_height = 40 # 计算图片尺寸 width = sum(col_widths) + 20 height = header_height + len(disconnected) * row_height + 40 # 创建白色背景图片 img = Image.new("RGB", (width, height), "white") draw = ImageDraw.Draw(img) try: # 尝试加载系统字体 font = ImageFont.truetype("simsun.ttc", 12) # 宋体 header_font = ImageFont.truetype("simsun.ttc", 14, encoding="unic") except: # 回退到默认字体 font = ImageFont.load_default() header_font = ImageFont.load_default() # 绘制标题 draw.text((10, 10), f"断网设备列表 - 共 {len(disconnected)} 台", fill="black", font=header_font) # 绘制表头 x = 10 for i, col in enumerate(columns): draw.rectangle([x, header_height, x + col_widths[i], header_height + row_height], outline="black") draw.text((x + 5, header_height + 5), col, fill="black", font=font) x += col_widths[i] # 绘制设备行 y = header_height + row_height for device in disconnected: x = 10 values = [ str(device["id"]), device["timestamp"], device["sheet"], device["ip"], device["station"], device["device_name"], str(device["disconnect_count"]), device["status"] ] # 根据设备类型设置行背景色 bg_color = "white" if not device["success"]: if device["is_critical"]: bg_color = "#ffffcc" # 黄色 elif device["disconnect_count"] >= self.settings['disconnect_threshold']: bg_color = "#ffe6e6" # 红色 # 绘制行背景 draw.rectangle([x, y, width - 10, y + row_height], fill=bg_color) # 绘制单元格内容 for i, value in enumerate(values): draw.rectangle([x, y, x + col_widths[i], y + row_height], outline="black") draw.text((x + 5, y + 5), str(value), fill="black", font=font) x += col_widths[i] y += row_height # 保存图片 img.save(file_path) self.status_var.set(f"成功导出 {len(disconnected)} 个断网设备图片到 {file_path}") except Exception as e: messagebox.showerror("导出错误", f"导出图片时出错: {str(e)}") if __name__ == "__main__": root = tk.Tk() app = PingMonitorApp(root) root.mainloop() 添加窗口排序功能
最新发布
11-27
/* * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" #include "esp_system.h" #include "esp_wifi.h" #include "esp_event.h" #include "esp_log.h" #include "nvs_flash.h" #include "esp_http_server.h" #include "esp_netif.h" #include "esp_spiffs.h" #define MIN(a, b) (((a) < (b)) ? (a) : (b)) /* FreeRTOS event group to signal when we are connected & ready to make a request */ static EventGroupHandle_t wifi_event_group; /* The event group allows multiple bits for each event, but we only care about one event - are we connected to the AP with an IP? */ const int WIFI_CONNECTED_BIT = BIT0; const int AP_STARTED_BIT = BIT1; static const char *TAG = "wifi_config"; /* Structure to hold received WiFi configuration */ typedef struct { char ssid[32]; char password[64]; bool valid; } wifi_config_storage_t; static wifi_config_storage_t wifi_config_storage; /* An HTTP server that serves a configuration form and handles form submissions */ static httpd_handle_t server = NULL; /* HTML form for WiFi configuration */ // 简化HTML页面(移除不必要的空格和换行) // 最小化HTML页面(移除DOCTYPE和所有不必要的空格) const char *wifi_config_page = "<html><body><form action=/config method=post>SSID:<input name=ssid><br>Password:<input type=password name=password><br><input type=submit value=Connect></form></body></html>"; /* HTML page for success message */ const char *success_page = "<html><body>" "<h1>Configuration Successful</h1>" "<p>ESP32 will now attempt to connect to the WiFi network.</p>" "<p>You can close this page.</p></body></html>"; /* HTML page for error message */ const char *error_page = "<html><body>" "<h1>Configuration Error</h1>" "<p>Failed to save WiFi configuration. Please try again.</p>" "<a href=\"/\">Back to configuration</a></body></html>"; static void connect_to_wifi_task(void *pvParameters); /* Handler for root endpoint (/) */ static esp_err_t root_get_handler(httpd_req_t *req) { httpd_resp_send(req, wifi_config_page, strlen(wifi_config_page)); return ESP_OK; } /* Handler for configuration endpoint (/config) */ static esp_err_t config_post_handler(httpd_req_t *req) { char temp_buf[128] = {0}; // 减小缓冲区大小 int ret; // 读取并丢弃请求头(只处理POST数据) while ((ret = httpd_req_recv(req, temp_buf, sizeof(temp_buf))) > 0) { // 查找表单数据边界(跳过请求头) char *body_start = strstr(temp_buf, "\r\n\r\n"); if (body_start) { body_start += 4; // 跳过\r\n\r\n // 解析SSID和密码 char *ssid_start = strstr(body_start, "ssid="); char *pass_start = strstr(body_start, "password="); if (ssid_start && pass_start) { ssid_start += 5; // 跳过"ssid=" pass_start += 9; // 跳过"password=" // 查找参数结束位置 char *ssid_end = strchr(ssid_start, '&'); char *pass_end = strchr(pass_start, '&'); // 复制SSID if (ssid_end) { size_t len = MIN(ssid_end - ssid_start, sizeof(wifi_config_storage.ssid) - 1); strncpy(wifi_config_storage.ssid, ssid_start, len); } else { strncpy(wifi_config_storage.ssid, ssid_start, MIN(strlen(ssid_start), sizeof(wifi_config_storage.ssid) - 1)); } // 复制密码(通常是最后一个参数) strncpy(wifi_config_storage.password, pass_start, MIN(strlen(pass_start), sizeof(wifi_config_storage.password) - 1)); // 验证并处理 if (strlen(wifi_config_storage.ssid) > 0) { wifi_config_storage.valid = true; httpd_resp_send(req, success_page, strlen(success_page)); httpd_stop(server); xTaskCreate(connect_to_wifi_task, "connect_to_wifi", 4096, NULL, 5, NULL); return ESP_OK; } } } } // 处理失败 httpd_resp_send(req, error_page, strlen(error_page)); return ESP_OK; } /* HTTP server configuration */ static httpd_uri_t uri_get = { .uri = "/", .method = HTTP_GET, .handler = root_get_handler }; static httpd_uri_t uri_post = { .uri = "/config", .method = HTTP_POST, .handler = config_post_handler }; /* Start the HTTP server */ static void start_webserver(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = 80; config.max_open_sockets = 5; config.max_uri_handlers = 4; config.max_resp_headers = 8; config.backlog_conn = 10; config.lru_purge_enable = true; ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port); if (httpd_start(&server, &config) == ESP_OK) { ESP_LOGI(TAG, "Registering URI handlers"); httpd_register_uri_handler(server, &uri_get); httpd_register_uri_handler(server, &uri_post); } else { ESP_LOGE(TAG, "Error starting server!"); } } /* Event handler for WiFi and IP events */ static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { esp_wifi_connect(); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { esp_wifi_connect(); xEventGroupClearBits(wifi_event_group, WIFI_CONNECTED_BIT); ESP_LOGI(TAG, "Connect to the AP failed"); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; ESP_LOGI(TAG, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip)); xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_START) { xEventGroupSetBits(wifi_event_group, AP_STARTED_BIT); ESP_LOGI(TAG, "AP Started"); start_webserver(); } } /* Task to connect to the configured WiFi network */ static void connect_to_wifi_task(void *pvParameters) { // Wait for a little while to let the client receive the success page vTaskDelay(pdMS_TO_TICKS(1000)); // Configure the ESP32 to connect to the specified WiFi network wifi_config_t wifi_config = { .sta = { .threshold.authmode = WIFI_AUTH_OPEN, }, }; strncpy((char*)wifi_config.sta.ssid, wifi_config_storage.ssid, sizeof(wifi_config.sta.ssid)); strncpy((char*)wifi_config.sta.password, wifi_config_storage.password, sizeof(wifi_config.sta.password)); ESP_LOGI(TAG, "Connecting to SSID: %s", wifi_config.sta.ssid); // Set WiFi mode to station ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); // Apply the configuration ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config)); // Start the WiFi interface ESP_ERROR_CHECK(esp_wifi_start()); // Wait for WiFi connection EventBits_t bits = xEventGroupWaitBits(wifi_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY); if (bits & WIFI_CONNECTED_BIT) { ESP_LOGI(TAG, "Connected to WiFi"); // Save the configuration to NVS for future use nvs_handle_t nvs_handle; esp_err_t err = nvs_open("wifi_config", NVS_READWRITE, &nvs_handle); if (err == ESP_OK) { // 强制转换为 const char* 以匹配函数参数类型 nvs_set_str(nvs_handle, "ssid", (const char*)wifi_config.sta.ssid); nvs_set_str(nvs_handle, "password", (const char*)wifi_config.sta.password); nvs_commit(nvs_handle); nvs_close(nvs_handle); ESP_LOGI(TAG, "WiFi configuration saved to NVS"); } } vTaskDelete(NULL); } /* Initialize WiFi as AP */ static void init_wifi_ap(void) { // Set static IP for the AP interface esp_netif_create_default_wifi_ap(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); // Register event handler ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL)); // Configure the AP wifi_config_t wifi_config = { .ap = { .ssid = "ESP32-Config", .ssid_len = strlen("ESP32-Config"), .password = "12345678", .max_connection = 4, .authmode = WIFI_AUTH_WPA_WPA2_PSK }, }; // Set WiFi mode to AP ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); // Apply the configuration ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config)); // Start the WiFi interface ESP_ERROR_CHECK(esp_wifi_start()); ESP_LOGI(TAG, "WiFi AP initialized with SSID: ESP32-Config, Password: 12345678"); } /* Check if WiFi configuration exists in NVS */ static bool check_saved_wifi_config(void) { nvs_handle_t nvs_handle; esp_err_t err = nvs_open("wifi_config", NVS_READONLY, &nvs_handle); if (err != ESP_OK) { ESP_LOGI(TAG, "No saved WiFi configuration found"); return false; } size_t required_size = 0; err = nvs_get_str(nvs_handle, "ssid", NULL, &required_size); if (err != ESP_OK || required_size == 0) { nvs_close(nvs_handle); return false; } char *ssid = malloc(required_size); err = nvs_get_str(nvs_handle, "ssid", ssid, &required_size); if (err != ESP_OK) { free(ssid); nvs_close(nvs_handle); return false; } // Check if password exists required_size = 0; err = nvs_get_str(nvs_handle, "password", NULL, &required_size); if (err != ESP_OK || required_size == 0) { free(ssid); nvs_close(nvs_handle); return false; } free(ssid); nvs_close(nvs_handle); return true; } /* Try to connect to saved WiFi configuration */ static bool try_connect_saved_wifi(void) { nvs_handle_t nvs_handle; esp_err_t err = nvs_open("wifi_config", NVS_READONLY, &nvs_handle); if (err != ESP_OK) { return false; } // Get SSID size_t ssid_size = 0; err = nvs_get_str(nvs_handle, "ssid", NULL, &ssid_size); if (err != ESP_OK || ssid_size == 0) { nvs_close(nvs_handle); return false; } char *ssid = malloc(ssid_size); err = nvs_get_str(nvs_handle, "ssid", ssid, &ssid_size); if (err != ESP_OK) { free(ssid); nvs_close(nvs_handle); return false; } // Get password size_t password_size = 0; err = nvs_get_str(nvs_handle, "password", NULL, &password_size); if (err != ESP_OK || password_size == 0) { free(ssid); nvs_close(nvs_handle); return false; } char *password = malloc(password_size); err = nvs_get_str(nvs_handle, "password", password, &password_size); if (err != ESP_OK) { free(ssid); free(password); nvs_close(nvs_handle); return false; } // Configure the ESP32 to connect to the saved WiFi network wifi_config_t wifi_config = { .sta = { .threshold.authmode = WIFI_AUTH_OPEN, }, }; strncpy((char*)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid)); strncpy((char*)wifi_config.sta.password, password, sizeof(wifi_config.sta.password)); free(ssid); free(password); nvs_close(nvs_handle); ESP_LOGI(TAG, "Trying to connect to saved SSID: %s", wifi_config.sta.ssid); // Set WiFi mode to station ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); // Apply the configuration ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config)); // Start the WiFi interface ESP_ERROR_CHECK(esp_wifi_start()); // Wait for WiFi connection (with timeout) EventBits_t bits = xEventGroupWaitBits(wifi_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, pdMS_TO_TICKS(10000)); // 10-second timeout if (bits & WIFI_CONNECTED_BIT) { ESP_LOGI(TAG, "Successfully connected to saved WiFi"); return true; } else { ESP_LOGI(TAG, "Failed to connect to saved WiFi"); // Reset WiFi to stop any ongoing connection attempts ESP_ERROR_CHECK(esp_wifi_stop()); return false; } } void app_main(void) { // Initialize NVS esp_err_t ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } ESP_ERROR_CHECK(ret); // Initialize WiFi event group wifi_event_group = xEventGroupCreate(); // Initialize TCP/IP stack esp_netif_init(); // Initialize the event loop ESP_ERROR_CHECK(esp_event_loop_create_default()); // Check if saved WiFi configuration exists and try to connect if (check_saved_wifi_config()) { ESP_LOGI(TAG, "Saved WiFi configuration found, attempting to connect"); if (try_connect_saved_wifi()) { // Connection successful, proceed with normal operation ESP_LOGI(TAG, "WiFi connection successful, starting normal operation"); // Add your application code here return; } } // If no saved configuration or connection failed, start AP mode for configuration ESP_LOGI(TAG, "Starting AP mode for WiFi configuration"); init_wifi_ap(); // Wait for AP to start xEventGroupWaitBits(wifi_event_group, AP_STARTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY); // The web server is automatically started when the AP is initialized // The user can now connect to the ESP32-Config network and configure WiFi }这是程序源码,当手机进入192.168.4.2的网页时,idf调试窗口会弹出如下提示:W (24108) httpd_uri: httpd_uri: URI '/favicon.ico' not found W (24108) httpd_txrx: httpd_resp_send_err: 404 Not Found - Nothing matches the given URI
07-13
<template> <div class="app-container"> <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch"> <el-form-item label="菜单名称" prop="menuName"> <el-input v-model="queryParams.menuName" placeholder="请输入菜单名称" clearable @keyup.enter.native="handleQuery" /> </el-form-item> <el-form-item label="状态" prop="status"> <el-select v-model="queryParams.status" placeholder="菜单状态" clearable> <el-option v-for="dict in dict.type.sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> </el-form-item> </el-form> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['system:menu:add']" >新增</el-button> </el-col> <el-col :span="1.5"> <el-button type="info" plain icon="el-icon-sort" size="mini" @click="toggleExpandAll" >展开/折叠</el-button> </el-col> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> </el-row> <el-table v-if="refreshTable" v-loading="loading" :data="menuList" row-key="menuId" :default-expand-all="isExpandAll" :tree-props="{children: 'children', hasChildren: 'hasChildren'}" > <el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column> <el-table-column prop="icon" label="图标" align="center" width="100"> <template slot-scope="scope"> <svg-icon :icon-class="scope.row.icon" /> </template> </el-table-column> <el-table-column prop="orderNum" label="排序" width="60"></el-table-column> <el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column> <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column> <el-table-column prop="status" label="状态" width="80"> <template slot-scope="scope"> <dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/> </template> </el-table-column> <el-table-column label="创建时间" align="center" prop="createTime"> <template slot-scope="scope"> <span>{{ parseTime(scope.row.createTime) }}</span> </template> </el-table-column> <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <template slot-scope="scope"> <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:menu:edit']" >修改</el-button> <el-button size="mini" type="text" icon="el-icon-plus" @click="handleAdd(scope.row)" v-hasPermi="['system:menu:add']" >新增</el-button> <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['system:menu:remove']" >删除</el-button> </template> </el-table-column> </el-table> <!-- 添加或修改菜单对话框 --> <el-dialog :title="title" :visible.sync="open" width="680px" append-to-body> <el-form ref="form" :model="form" :rules="rules" label-width="100px"> <el-row> <el-col :span="24"> <el-form-item label="上级菜单" prop="parentId"> <treeselect v-model="form.parentId" :options="menuOptions" :normalizer="normalizer" :show-count="true" placeholder="选择上级菜单" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="菜单类型" prop="menuType"> <el-radio-group v-model="form.menuType"> <el-radio label="M">目录</el-radio> <el-radio label="C">菜单</el-radio> <el-radio label="F">按钮</el-radio> </el-radio-group> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12" v-if="form.menuType != 'F'"> <el-form-item label="菜单图标" prop="icon"> <el-popover placement="bottom-start" width="460" trigger="click" @show="$refs['iconSelect'].reset()" > <IconSelect ref="iconSelect" @selected="selected" :active-icon="form.icon" /> <el-input slot="reference" v-model="form.icon" placeholder="点击选择图标" readonly> <svg-icon v-if="form.icon" slot="prefix" :icon-class="form.icon" style="width: 25px;" /> <i v-else slot="prefix" class="el-icon-search el-input__icon" /> </el-input> </el-popover> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="显示排序" prop="orderNum"> <el-input-number v-model="form.orderNum" controls-position="right" :min="0" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="菜单名称" prop="menuName"> <el-input v-model="form.menuName" placeholder="请输入菜单名称" /> </el-form-item> </el-col> <el-col :span="12" v-if="form.menuType == 'C'"> <el-form-item prop="routeName"> <el-input v-model="form.routeName" placeholder="请输入路由名称" /> <span slot="label"> <el-tooltip content="默认不填则和路由地址相同:如地址为:`user`,则名称为`User`(注意:为避免名字的冲突,特殊情况下请自定义,保证唯一性)" placement="top"> <i class="el-icon-question"></i> </el-tooltip> 路由名称 </span> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12" v-if="form.menuType != 'F'"> <el-form-item prop="isFrame"> <span slot="label"> <el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top"> <i class="el-icon-question"></i> </el-tooltip> 是否外链 </span> <el-radio-group v-model="form.isFrame"> <el-radio label="0">是</el-radio> <el-radio label="1">否</el-radio> </el-radio-group> </el-form-item> </el-col> <el-col :span="12" v-if="form.menuType != 'F'"> <el-form-item prop="path"> <span slot="label"> <el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top"> <i class="el-icon-question"></i> </el-tooltip> 路由地址 </span> <el-input v-model="form.path" placeholder="请输入路由地址" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12" v-if="form.menuType == 'C'"> <el-form-item prop="component"> <span slot="label"> <el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top"> <i class="el-icon-question"></i> </el-tooltip> 组件路径 </span> <el-input v-model="form.component" placeholder="请输入组件路径" /> </el-form-item> </el-col> <el-col :span="12" v-if="form.menuType != 'M'"> <el-form-item prop="perms"> <el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" /> <span slot="label"> <el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top"> <i class="el-icon-question"></i> </el-tooltip> 权限字符 </span> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12" v-if="form.menuType == 'C'"> <el-form-item prop="query"> <el-input v-model="form.query" placeholder="请输入路由参数" maxlength="255" /> <span slot="label"> <el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top"> <i class="el-icon-question"></i> </el-tooltip> 路由参数 </span> </el-form-item> </el-col> <el-col :span="12" v-if="form.menuType == 'C'"> <el-form-item prop="isCache"> <span slot="label"> <el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top"> <i class="el-icon-question"></i> </el-tooltip> 是否缓存 </span> <el-radio-group v-model="form.isCache"> <el-radio label="0">缓存</el-radio> <el-radio label="1">不缓存</el-radio> </el-radio-group> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12" v-if="form.menuType != 'F'"> <el-form-item prop="visible"> <span slot="label"> <el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top"> <i class="el-icon-question"></i> </el-tooltip> 显示状态 </span> <el-radio-group v-model="form.visible"> <el-radio v-for="dict in dict.type.sys_show_hide" :key="dict.value" :label="dict.value" >{{dict.label}}</el-radio> </el-radio-group> </el-form-item> </el-col> <el-col :span="12"> <el-form-item prop="status"> <span slot="label"> <el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top"> <i class="el-icon-question"></i> </el-tooltip> 菜单状态 </span> <el-radio-group v-model="form.status"> <el-radio v-for="dict in dict.type.sys_normal_disable" :key="dict.value" :label="dict.value" >{{dict.label}}</el-radio> </el-radio-group> </el-form-item> </el-col> </el-row> </el-form> <div slot="footer" class="dialog-footer"> <el-button type="primary" @click="submitForm">确 定</el-button> <el-button @click="cancel">取 消</el-button> </div> </el-dialog> </div> </template> <script> import { listMenu, getMenu, delMenu, addMenu, updateMenu } from "@/api/system/menu" import Treeselect from "@riophae/vue-treeselect" import "@riophae/vue-treeselect/dist/vue-treeselect.css" import IconSelect from "@/components/IconSelect" export default { name: "Menu", dicts: ['sys_show_hide', 'sys_normal_disable'], components: { Treeselect, IconSelect }, data() { return { // 遮罩层 loading: true, // 显示搜索条件 showSearch: true, // 菜单表格树数据 menuList: [], // 菜单树选项 menuOptions: [], // 弹出层标题 title: "", // 是否显示弹出层 open: false, // 是否展开,默认全部折叠 isExpandAll: false, // 重新渲染表格状态 refreshTable: true, // 查询参数 queryParams: { menuName: undefined, visible: undefined }, // 表单参数 form: {}, // 表单校验 rules: { menuName: [ { required: true, message: "菜单名称不能为空", trigger: "blur" } ], orderNum: [ { required: true, message: "菜单顺序不能为空", trigger: "blur" } ], path: [ { required: true, message: "路由地址不能为空", trigger: "blur" } ] } } }, created() { this.getList() }, methods: { // 选择图标 selected(name) { this.form.icon = name }, /** 查询菜单列表 */ getList() { this.loading = true listMenu(this.queryParams).then(response => { this.menuList = this.handleTree(response.data, "menuId") this.loading = false }) }, /** 转换菜单数据结构 */ normalizer(node) { if (node.children && !node.children.length) { delete node.children } return { id: node.menuId, label: node.menuName, children: node.children } }, /** 查询菜单下拉树结构 */ getTreeselect() { listMenu().then(response => { this.menuOptions = [] const menu = { menuId: 0, menuName: '主类目', children: [] } menu.children = this.handleTree(response.data, "menuId") this.menuOptions.push(menu) }) }, // 取消按钮 cancel() { this.open = false this.reset() }, // 表单重置 reset() { this.form = { menuId: undefined, parentId: 0, menuName: undefined, icon: undefined, menuType: "M", orderNum: undefined, isFrame: "1", isCache: "0", visible: "0", status: "0" } this.resetForm("form") }, /** 搜索按钮操作 */ handleQuery() { this.getList() }, /** 重置按钮操作 */ resetQuery() { this.resetForm("queryForm") this.handleQuery() }, /** 新增按钮操作 */ handleAdd(row) { this.reset() this.getTreeselect() if (row != null && row.menuId) { this.form.parentId = row.menuId } else { this.form.parentId = 0 } this.open = true this.title = "添加菜单" }, /** 展开/折叠操作 */ toggleExpandAll() { this.refreshTable = false this.isExpandAll = !this.isExpandAll this.$nextTick(() => { this.refreshTable = true }) }, /** 修改按钮操作 */ handleUpdate(row) { this.reset() this.getTreeselect() getMenu(row.menuId).then(response => { this.form = response.data this.open = true this.title = "修改菜单" }) }, /** 提交按钮 */ submitForm: function() { this.$refs["form"].validate(valid => { if (valid) { if (this.form.menuId != undefined) { updateMenu(this.form).then(response => { this.$modal.msgSuccess("修改成功") this.open = false this.getList() }) } else { addMenu(this.form).then(response => { this.$modal.msgSuccess("新增成功") this.open = false this.getList() }) } } }) }, /** 删除按钮操作 */ handleDelete(row) { this.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?').then(function() { return delMenu(row.menuId) }).then(() => { this.getList() this.$modal.msgSuccess("删除成功") }).catch(() => {}) } } } </script> 美化
09-24
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值