package ui import ( "fmt" "net/url" "strconv" "strings" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" "ip_scan_go/internal/scan" ) const appVersion = "V0.0.1" const ( ipPlaceholder = "例子:\n127.0.0.1 - 单IP\n192.168.0.0/24 - 掩码IP段\n0.0.0.0-255.255.255.255 - IP段\ndomain.com - 单域名" portPlaceholder = "例子:\n22 - 单端口\n80;443 - 多个不连续端口\n3000-65535 - 连续的端口范围" ) type scanForm struct { app fyne.App ipList *widget.Entry portList *widget.Entry timeout *widget.Entry threads *widget.Entry output *widget.Entry progress *widget.ProgressBar scanBtn *widget.Button pingBtn *widget.Button keypadBtn []*widget.Button dispatcher *scan.Dispatcher scanning bool } func newScanForm(app fyne.App) *scanForm { f := &scanForm{app: app} f.ipList = widget.NewMultiLineEntry() f.ipList.SetPlaceHolder(ipPlaceholder) f.ipList.SetMinRowsVisible(4) f.portList = widget.NewMultiLineEntry() f.portList.SetPlaceHolder(portPlaceholder) f.portList.SetMinRowsVisible(4) f.timeout = newIntEntry(100, 1, 10000) f.threads = newIntEntry(64, 1, 1024) f.output = widget.NewMultiLineEntry() f.output.SetMinRowsVisible(6) f.progress = widget.NewProgressBar() f.scanBtn = widget.NewButton("开始扫描", f.toggleScan) f.pingBtn = widget.NewButton("仅Ping", f.togglePing) return f } func (f *scanForm) build() fyne.CanvasObject { ipSection := container.NewVBox( widget.NewLabel("IP"), f.ipList, ) ipKeys, ipBtns := newIPKeypad(f.ipList) ipSection.Add(ipKeys) portSection := container.NewVBox( widget.NewLabel("端口"), f.portList, ) portKeys, portBtns := newPortKeypad(f.portList) portSection.Add(portKeys) f.keypadBtn = append(f.keypadBtn, ipBtns...) f.keypadBtn = append(f.keypadBtn, portBtns...) topRow := container.New(layout.NewGridLayoutWithColumns(2), ipSection, portSection) controlRow := f.buildControlRow() topArea := container.NewVBox(topRow, controlRow) footer := container.NewHBox( widget.NewLabel(appVersion), layout.NewSpacer(), widget.NewButton("Blog", func() { f.openURL("https://wnfed.com/") }), widget.NewButton("Gitea", func() { f.openURL("https://git.lmve.net/kevin/ip_scan_go") }), widget.NewButton("GitHub", func() { f.openURL("https://github.com/wuwenfengmi1998") }), ) return container.NewBorder(topArea, footer, nil, nil, f.output) } func (f *scanForm) buildControlRow() fyne.CanvasObject { left := container.NewHBox( widget.NewButton("清空输出", func() { f.output.SetText("") }), widget.NewButton("保存输出", f.saveOutput), container.NewHBox(widget.NewLabel("超时ms"), f.timeout), container.NewHBox(widget.NewLabel("线程"), f.threads), widget.NewButton("清空IP", func() { f.ipList.SetText("") }), f.pingBtn, ) right := container.NewHBox( widget.NewButton("清空端口", func() { f.portList.SetText("") }), f.scanBtn, ) return container.NewBorder(nil, nil, left, right, f.progress) } func (f *scanForm) toggleScan() { if !f.scanning { ips, err := scan.ParseIPs(f.ipList.Text) if err != nil { f.appendOutput("IP解析错误: " + err.Error() + "\n") return } ports, err := scan.ParsePorts(f.portList.Text) if err != nil { f.appendOutput("端口解析错误: " + err.Error() + "\n") return } if len(ips) == 0 || len(ports) == 0 { f.appendOutput("请输入IP和端口\n") return } f.startScan(false, ips, ports) } else { f.stopScan() } } func (f *scanForm) togglePing() { if !f.scanning { ips, err := scan.ParseIPs(f.ipList.Text) if err != nil { f.appendOutput("IP解析错误: " + err.Error() + "\n") return } if len(ips) == 0 { f.appendOutput("请输入IP\n") return } f.startScan(true, ips, nil) } else { f.stopScan() } } func (f *scanForm) startScan(pingOnly bool, ips []string, ports []int) { f.scanning = true if pingOnly { f.pingBtn.SetText("停止Ping") f.scanBtn.Disable() } else { f.scanBtn.SetText("停止扫描") f.pingBtn.Disable() } f.setInputsEnabled(false) f.progress.SetValue(0) timeout := time.Duration(parseIntEntry(f.timeout, 100)) * time.Millisecond workers := parseIntEntry(f.threads, 64) mode := "扫描" if pingOnly { mode = "Ping" } f.appendOutput(fmt.Sprintf("开始%s (IP:%d 线程:%d 超时:%s)\n", mode, len(ips), workers, timeout)) d := &scan.Dispatcher{ IPs: ips, Ports: ports, Timeout: timeout, Workers: workers, PingOnly: pingOnly, OnResult: func(r scan.Result) { fyne.Do(func() { f.appendOutput(r.Text + "\n") }) }, OnProgress: func(p scan.Progress) { pct := float64(p.Done) / float64(p.Total) fyne.Do(func() { f.progress.SetValue(pct) }) }, OnFinish: func() { fyne.Do(func() { f.resetAfterScan(pingOnly) f.appendOutput(fmt.Sprintf("%s完成\n", mode)) }) }, } f.dispatcher = d go d.Start() } func (f *scanForm) stopScan() { if f.dispatcher != nil { f.dispatcher.Stop() } } func (f *scanForm) resetAfterScan(pingOnly bool) { f.scanning = false if pingOnly { f.pingBtn.SetText("仅Ping") } else { f.scanBtn.SetText("开始扫描") } f.scanBtn.Enable() f.pingBtn.Enable() f.setInputsEnabled(true) f.dispatcher = nil } func (f *scanForm) setInputsEnabled(enabled bool) { for _, b := range f.keypadBtn { if enabled { b.Enable() } else { b.Disable() } } if enabled { f.ipList.Enable() f.portList.Enable() f.timeout.Enable() f.threads.Enable() } else { f.ipList.Disable() f.portList.Disable() f.timeout.Disable() f.threads.Disable() } } func (f *scanForm) saveOutput() { f.appendOutput("(TODO: 保存输出到文件)\n") } func (f *scanForm) appendOutput(text string) { f.output.Append(text) f.output.CursorRow = strings.Count(f.output.Text, "\n") f.output.CursorColumn = 0 f.output.Refresh() } func (f *scanForm) openURL(urlStr string) { u, err := url.Parse(urlStr) if err != nil { return } _ = f.app.OpenURL(u) } func newIntEntry(val, min, max int) *widget.Entry { e := widget.NewEntry() e.SetText(strconv.Itoa(val)) e.Validator = func(s string) error { n, err := strconv.Atoi(s) if err != nil || n < min || n > max { return fmt.Errorf("需为 %d-%d", min, max) } return nil } return e } func parseIntEntry(e *widget.Entry, fallback int) int { n, err := strconv.Atoi(e.Text) if err != nil || n <= 0 { return fallback } return n }