http/page.go

1436 lines
36 KiB
Go
Raw Normal View History

2025-07-21 00:09:21 +08:00
package http
import (
"errors"
"fmt"
"io"
"strings"
"time"
"apigo.cc/gojs"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/input"
"github.com/go-rod/rod/lib/proto"
"github.com/ssgo/u"
)
type Page struct {
page *rod.Page
longTimeoutPage *rod.Page
2025-07-22 20:45:03 +08:00
originPage *rod.Page
2025-07-21 00:09:21 +08:00
// Res []Resource
}
type Element struct {
element *rod.Element
page *rod.Page
}
type Resource struct {
Url string
Type string
Size int
MimeType string
LastModified string
Success bool
}
type Position struct {
X float64
Y float64
}
type Rect struct{ X, Y, Width, Height float64 }
func (ch *Chrome) Open(url string) (*Page, error) {
page, err := ch.browser.Page(proto.TargetCreateTarget{URL: url})
if err != nil {
return nil, gojs.Err(err)
}
2025-07-22 20:45:03 +08:00
pg := &Page{originPage: page}
pg.ResetTimeout()
pg.WaitPageLoad()
2025-07-21 00:09:21 +08:00
return pg, nil
}
func (pg *Page) Close() error {
// pg.Res = []Resource{}
return gojs.Err(pg.page.Close())
}
func (pg *Page) GetResList() ([]Resource, error) {
r, err := proto.PageGetResourceTree{}.Call(pg.longTimeoutPage)
2025-07-21 00:09:21 +08:00
if err != nil {
return nil, gojs.Err(err)
}
list := []Resource{}
for _, res := range r.FrameTree.Resources {
list = append(list, Resource{
Url: res.URL,
Type: string(res.Type),
Size: u.Int(res.ContentSize),
MimeType: res.MIMEType,
LastModified: res.LastModified.String(),
Success: !res.Failed && !res.Canceled,
})
}
return list, nil
}
func (pg *Page) GetResListByType(resType string) ([]Resource, error) {
list, err := pg.GetResList()
if err != nil {
return nil, err
}
result := []Resource{}
for _, res := range list {
if strings.EqualFold(res.Type, resType) {
result = append(result, res)
}
}
return result, nil
}
func (pg *Page) GetResListByMimeType(mimeType string) ([]Resource, error) {
list, err := pg.GetResList()
if err != nil {
return nil, err
}
isMatch := strings.HasSuffix(mimeType, "/*")
mimeType = strings.TrimSuffix(mimeType, "*")
result := []Resource{}
for _, res := range list {
if isMatch {
if strings.HasPrefix(res.MimeType, mimeType) {
result = append(result, res)
}
} else {
if strings.EqualFold(res.MimeType, mimeType) {
result = append(result, res)
}
}
}
return result, nil
}
func (pg *Page) GetResContent(url string) ([]byte, error) {
return pg.longTimeoutPage.GetResource(url)
}
func (pg *Page) Find(selector string) (*Element, error) {
el, err := pg.page.Element(selector)
if err != nil {
return nil, gojs.Err(err)
}
return &Element{element: el, page: pg.page}, nil
}
func (pg *Page) FindN(selector string) *Element {
els, err := pg.FindAll(selector)
if err != nil {
return nil
}
if len(els) == 0 {
return nil
}
return els[0]
}
func (pg *Page) FindAll(selector string) ([]*Element, error) {
elements, err := pg.page.Elements(selector)
if err != nil {
return nil, gojs.Err(err)
}
result := make([]*Element, len(elements))
for i, el := range elements {
result[i] = &Element{element: el, page: pg.page}
}
return result, nil
}
// FindX 通过XPath查找元素
func (pg *Page) FindX(xpath string) (*Element, error) {
el, err := pg.page.ElementX(xpath)
if err != nil {
return nil, gojs.Err(err)
}
return &Element{element: el, page: pg.page}, nil
}
func (pg *Page) FindXN(selector string) *Element {
el, err := pg.FindX(selector)
if err != nil {
return nil
}
return el
}
// FindsX 通过XPath查找多个元素
func (pg *Page) FindAllX(xpath string) ([]*Element, error) {
elements, err := pg.page.ElementsX(xpath)
if err != nil {
return nil, gojs.Err(err)
}
result := make([]*Element, len(elements))
for i, el := range elements {
result[i] = &Element{element: el, page: pg.page}
}
return result, nil
}
func (pg *Page) Navigate(url string) error {
err := pg.longTimeoutPage.Navigate(url)
if err != nil {
return gojs.Err(err)
}
2025-07-22 20:45:03 +08:00
pg.WaitPageLoad()
2025-07-21 00:09:21 +08:00
return nil
}
// WaitLoad 等待页面加载完成
func (pg *Page) WaitPageLoad() error {
2025-07-22 20:45:03 +08:00
time.Sleep(100 * time.Millisecond)
err := pg.page.WaitLoad()
if err != nil {
return gojs.Err(err)
}
pg.ResetTimeout()
return nil
2025-07-21 00:09:21 +08:00
}
// WaitIdle 等待页面空闲无网络请求和JS执行
func (pg *Page) WaitIdle(ms *int) error {
if ms == nil {
ms = u.IntPtr(1000)
}
return gojs.Err(pg.page.WaitIdle(time.Millisecond * time.Duration(*ms)))
}
2025-07-22 20:45:03 +08:00
// Wait 等待回调函数函数返回true
func (pg *Page) Wait(ms int, fn *func() bool) {
for i := 0; i < ms; i += 100 {
if fn != nil && (*fn)() {
break
}
time.Sleep(100 * time.Millisecond)
}
pg.ResetTimeout()
}
// RandWait 随机等待
func (pg *Page) RandWait(msMin int, msMax int) {
ms := u.GlobalRand1.Intn(msMax-msMin) + msMin
pg.Wait(ms, nil)
}
func (pg *Page) ResetTimeout() {
pg.page = pg.originPage.Timeout(time.Second * 5)
pg.longTimeoutPage = pg.originPage.Timeout(time.Second * 15)
}
2025-07-21 00:09:21 +08:00
// WaitStable 等待页面DOM稳定
// func (pg *Page) WaitStable(ms *int) error {
// if ms == nil {
// ms = u.IntPtr(100)
// }
// return gojs.Err(pg.page.WaitStable(time.Millisecond * time.Duration(*ms)))
// }
// Screenshot 截图
func (pg *Page) ScreenshotFullPage() ([]byte, error) {
buf, err := pg.longTimeoutPage.Screenshot(true, nil)
return buf, gojs.Err(err)
}
func (pg *Page) Screenshot() ([]byte, error) {
buf, err := pg.longTimeoutPage.Screenshot(false, nil)
return buf, gojs.Err(err)
}
// PDF 生成页面PDF
func (pg *Page) PDF() ([]byte, error) {
r, err := pg.longTimeoutPage.PDF(&proto.PagePrintToPDF{})
if err != nil {
return nil, gojs.Err(err)
}
bin, err := io.ReadAll(r)
if err != nil {
return nil, gojs.Err(err)
}
return bin, nil
}
func (pg *Page) Title() (string, error) {
info, err := pg.page.Info()
if err != nil {
return "", gojs.Err(err)
}
return info.Title, nil
}
func (pg *Page) Html() (string, error) {
html, err := pg.page.HTML()
return html, gojs.Err(err)
}
func (pg *Page) Url() (string, error) {
info, err := pg.page.Info()
if err != nil {
return "", gojs.Err(err)
}
return info.URL, nil
}
func (pg *Page) Eval(js string) (interface{}, error) {
r, err := pg.page.Eval(js)
if err != nil {
return nil, gojs.Err(err)
}
return r.Value.Val(), nil
}
func (pg *Page) MouseMoveTo(x, y float64) error {
return gojs.Err(pg.page.Mouse.MoveTo(proto.Point{X: x, Y: y}))
}
func (pg *Page) MouseClick() error {
return gojs.Err(pg.page.Mouse.Click(proto.InputMouseButtonLeft, 1))
}
func (pg *Page) MouseRightClick() error {
return gojs.Err(pg.page.Mouse.Click(proto.InputMouseButtonRight, 1))
}
func (pg *Page) MouseMiddleClick() error {
return gojs.Err(pg.page.Mouse.Click(proto.InputMouseButtonMiddle, 1))
}
func (pg *Page) MouseDown() error {
return gojs.Err(pg.page.Mouse.Down(proto.InputMouseButtonLeft, 1))
}
func (pg *Page) MouseUp() error {
return gojs.Err(pg.page.Mouse.Up(proto.InputMouseButtonLeft, 1))
}
func (pg *Page) SetCookies(cookies map[string]string) error {
cks := []*proto.NetworkCookieParam{}
for k, v := range cookies {
cks = append(cks, &proto.NetworkCookieParam{
Name: k,
Value: v,
})
}
err := pg.page.SetCookies(cks)
if err != nil {
return gojs.Err(err)
}
return nil
}
func (pg *Page) GetCookies() (map[string]string, error) {
cookies, err := pg.page.Cookies(nil)
if err != nil {
return nil, gojs.Err(err)
}
result := make(map[string]string)
for _, c := range cookies {
result[c.Name] = c.Value
}
return result, nil
}
// SetViewport 设置视口大小
func (pg *Page) SetViewport(width, height int) error {
return gojs.Err(pg.page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
Width: width,
Height: height,
DeviceScaleFactor: 0,
Mobile: false,
}))
}
func (pg *Page) SetPhoneViewport(width, height int) error {
return gojs.Err(pg.page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
Width: width,
Height: height,
DeviceScaleFactor: 0,
Mobile: true,
}))
}
// ScrollTo 滚动到指定位置
func (pg *Page) ScrollTo(x, y float64) error {
_, err := pg.page.Eval(`(x, y) => window.scrollTo(x, y)`, x, y)
return gojs.Err(err)
}
// Frame 获取iframe内容
func (pg *Page) Frame(selector string) (*Page, error) {
el, err := pg.page.Element(selector)
if err != nil {
return nil, gojs.Err(err)
}
p, err := el.Frame()
if err != nil {
return nil, gojs.Err(err)
}
return &Page{page: p}, nil
}
var isControlKey = map[string]bool{
"ShiftLeft": true,
"ShiftRight": true,
"Shift": true,
"ControlLeft": true,
"ControlRight": true,
"Ctrl": true,
"Control": true,
"MetaLeft": true,
"MetaRight": true,
"Meta": true,
"Cmd": true,
"Windows": true,
"Win": true,
"AltLeft": true,
"AltRight": true,
"AltGraph": true,
"Option": true,
"Alt": true,
}
var KeyMap = map[string]input.Key{
"Escape": input.Escape,
"Esc": input.Escape,
"F1": input.F1,
"F2": input.F2,
"F3": input.F3,
"F4": input.F4,
"F5": input.F5,
"F6": input.F6,
"F7": input.F7,
"F8": input.F8,
"F9": input.F9,
"F10": input.F10,
"F11": input.F11,
"F12": input.F12,
"Backquote": input.Backquote,
"1": input.Digit1,
"2": input.Digit2,
"3": input.Digit3,
"4": input.Digit4,
"5": input.Digit5,
"6": input.Digit6,
"7": input.Digit7,
"8": input.Digit8,
"9": input.Digit9,
"0": input.Digit0,
"Digit1": input.Digit1,
"Digit2": input.Digit2,
"Digit3": input.Digit3,
"Digit4": input.Digit4,
"Digit5": input.Digit5,
"Digit6": input.Digit6,
"Digit7": input.Digit7,
"Digit8": input.Digit8,
"Digit9": input.Digit9,
"Digit0": input.Digit0,
"Minus": input.Minus,
"Equal": input.Equal,
"Backslash": input.Backslash,
"Backspace": input.Backspace,
"Tab": input.Tab,
"KeyQ": input.KeyQ,
"Q": input.KeyQ,
"KeyW": input.KeyW,
"W": input.KeyW,
"KeyE": input.KeyE,
"E": input.KeyE,
"KeyR": input.KeyR,
"R": input.KeyR,
"KeyT": input.KeyT,
"T": input.KeyT,
"KeyY": input.KeyY,
"Y": input.KeyY,
"KeyU": input.KeyU,
"U": input.KeyU,
"KeyI": input.KeyI,
"I": input.KeyI,
"KeyO": input.KeyO,
"O": input.KeyO,
"KeyP": input.KeyP,
"P": input.KeyP,
"BracketLeft": input.BracketLeft,
"BracketRight": input.BracketRight,
"CapsLock": input.CapsLock,
"KeyA": input.KeyA,
"A": input.KeyA,
"KeyS": input.KeyS,
"S": input.KeyS,
"KeyD": input.KeyD,
"D": input.KeyD,
"KeyF": input.KeyF,
"F": input.KeyF,
"KeyG": input.KeyG,
"G": input.KeyG,
"KeyH": input.KeyH,
"H": input.KeyH,
"KeyJ": input.KeyJ,
"J": input.KeyJ,
"KeyK": input.KeyK,
"K": input.KeyK,
"KeyL": input.KeyL,
"L": input.KeyL,
"Semicolon": input.Semicolon,
"Quote": input.Quote,
"Enter": input.Enter,
"ShiftLeft": input.ShiftLeft,
"KeyZ": input.KeyZ,
"Z": input.KeyZ,
"KeyX": input.KeyX,
"X": input.KeyX,
"KeyC": input.KeyC,
"C": input.KeyC,
"KeyV": input.KeyV,
"V": input.KeyV,
"KeyB": input.KeyB,
"B": input.KeyB,
"KeyN": input.KeyN,
"N": input.KeyN,
"KeyM": input.KeyM,
"M": input.KeyM,
"Comma": input.Comma,
"Period": input.Period,
"Slash": input.Slash,
"ShiftRight": input.ShiftRight,
"ControlLeft": input.ControlLeft,
"MetaLeft": input.MetaLeft,
"AltLeft": input.AltLeft,
"Space": input.Space,
" ": input.Space,
"AltRight": input.AltRight,
"AltGraph": input.AltGraph,
"MetaRight": input.MetaRight,
"ContextMenu": input.ContextMenu,
"ControlRight": input.ControlRight,
"PrintScreen": input.PrintScreen,
"ScrollLock": input.ScrollLock,
"Pause": input.Pause,
"PageUp": input.PageUp,
"PageDown": input.PageDown,
"Insert": input.Insert,
"Delete": input.Delete,
"Home": input.Home,
"End": input.End,
"ArrowLeft": input.ArrowLeft,
"ArrowUp": input.ArrowUp,
"ArrowRight": input.ArrowRight,
"ArrowDown": input.ArrowDown,
"Left": input.ArrowLeft,
"Up": input.ArrowUp,
"Right": input.ArrowRight,
"Down": input.ArrowDown,
"NumLock": input.NumLock,
"NumpadDivide": input.NumpadDivide,
"NumpadMultiply": input.NumpadMultiply,
"NumpadSubtract": input.NumpadSubtract,
"Numpad7": input.Numpad7,
"Numpad8": input.Numpad8,
"Numpad9": input.Numpad9,
"Numpad4": input.Numpad4,
"Numpad5": input.Numpad5,
"Numpad6": input.Numpad6,
"NumpadAdd": input.NumpadAdd,
"Numpad1": input.Numpad1,
"Numpad2": input.Numpad2,
"Numpad3": input.Numpad3,
"Numpad0": input.Numpad0,
"NumpadDecimal": input.NumpadDecimal,
"NumpadEnter": input.NumpadEnter,
"NumDivide": input.NumpadDivide,
"NumMultiply": input.NumpadMultiply,
"NumSubtract": input.NumpadSubtract,
"Num7": input.Numpad7,
"Num8": input.Numpad8,
"Num9": input.Numpad9,
"Num4": input.Numpad4,
"Num5": input.Numpad5,
"Num6": input.Numpad6,
"NumAdd": input.NumpadAdd,
"Num1": input.Numpad1,
"Num2": input.Numpad2,
"Num3": input.Numpad3,
"Num0": input.Numpad0,
"NumDecimal": input.NumpadDecimal,
"NumEnter": input.NumpadEnter,
"Ctrl": input.ControlLeft,
"Control": input.ControlLeft,
"Cmd": input.MetaLeft,
"Windows": input.MetaLeft,
"Meta": input.MetaLeft,
"Win": input.MetaLeft,
"Option": input.AltLeft,
"Alt": input.AltLeft,
",": input.Comma,
".": input.Period,
"/": input.Slash,
";": input.Semicolon,
"'": input.Quote,
"[": input.BracketLeft,
"]": input.BracketRight,
"\\": input.Backslash,
"-": input.Minus,
"=": input.Equal,
"`": input.Backquote,
}
func keyFilter(controlKey bool, keys ...string) []string {
var filteredKeys []string
for _, key := range keys {
if (controlKey && isControlKey[key]) || (!controlKey && !isControlKey[key]) {
filteredKeys = append(filteredKeys, key)
}
}
return filteredKeys
}
func keyAction(page *rod.Page, action string, keys ...string) error {
for _, key := range keys {
if k, ok := KeyMap[key]; ok {
var err error
switch action {
case "press":
err = page.Keyboard.Press(k)
case "release":
err = page.Keyboard.Release(k)
case "type":
err = page.Keyboard.Type(k)
}
if err != nil {
return gojs.Err(err)
}
}
}
return nil
}
func (pg *Page) KeyPress(keys ...string) error {
return keyAction(pg.page, "press", keys...)
}
func (pg *Page) KeyRelease(keys ...string) error {
return keyAction(pg.page, "release", keys...)
}
func (pg *Page) KeyType(keys ...string) error {
return keyAction(pg.page, "type", keys...)
}
func (pg *Page) Key(keys ...string) error {
controlKeys := keyFilter(true, keys...)
// 按下所有控制键
err := keyAction(pg.page, "press", controlKeys...)
if err == nil {
// 输入所有非控制键
err = keyAction(pg.page, "type", keyFilter(false, keys...)...)
}
if err == nil {
// 释放所有控制键
err = keyAction(pg.page, "release", controlKeys...)
}
return err
2025-07-21 00:09:21 +08:00
}
func (pg *Page) SetUserAgent(ua string) error {
return gojs.Err(pg.page.SetUserAgent(&proto.NetworkSetUserAgentOverride{
UserAgent: ua,
}))
}
func (pg *Page) SetUserAgentInfo(ua string, platform string, acceptLanguage string) error {
return gojs.Err(pg.page.SetUserAgent(&proto.NetworkSetUserAgentOverride{
UserAgent: ua,
Platform: platform,
AcceptLanguage: acceptLanguage,
}))
}
func (pg *Page) Back() error {
err := pg.page.NavigateBack()
if err != nil {
return gojs.Err(err)
}
2025-07-22 20:45:03 +08:00
pg.WaitPageLoad()
2025-07-21 00:09:21 +08:00
return nil
}
func (pg *Page) Forward() error {
err := pg.page.NavigateForward()
if err != nil {
return gojs.Err(err)
}
2025-07-22 20:45:03 +08:00
pg.WaitPageLoad()
2025-07-21 00:09:21 +08:00
return nil
}
// ================== 对话框控制 ==================
// HandleDialog 设置弹框处理器
// 示例pg.HandleDialog(true, "") 接受所有弹框
func (pg *Page) HandleDialog(accept bool, promptText string) error {
wait, handle := pg.longTimeoutPage.HandleDialog()
wait()
return gojs.Err(handle(&proto.PageHandleJavaScriptDialog{
Accept: accept,
PromptText: promptText,
}))
}
// AutoAcceptAlerts 自动接受所有弹框
func (pg *Page) AccepDialog() error {
return gojs.Err(pg.HandleDialog(true, ""))
}
// AutoDismissAlerts 自动取消所有弹框
func (pg *Page) DismissDialog() error {
return gojs.Err(pg.HandleDialog(false, ""))
}
func (pg *Page) SetPrompt(text string) error {
return gojs.Err(pg.HandleDialog(true, text))
}
// ================== Element 方法 ==================
func (el *Element) Click() error {
return gojs.Err(el.element.Click(proto.InputMouseButtonLeft, 1))
}
func (el *Element) Input(text string) error {
return gojs.Err(el.element.Input(text))
}
func (el *Element) Text() (string, error) {
text, err := el.element.Text()
return text, gojs.Err(err)
}
func (el *Element) InnerHTML() (string, error) {
2025-07-22 20:45:03 +08:00
r, err := el.element.Eval(`el => {return el.innerHTML}`)
2025-07-21 00:09:21 +08:00
if err != nil {
return "", gojs.Err(err)
}
return r.Value.String(), nil
}
func (el *Element) OuterHTML() (string, error) {
html, err := el.element.HTML()
return html, gojs.Err(err)
}
func (el *Element) SetInnerHTML(html string) error {
_, err := el.element.Eval(`(el, html) => el.innerHTML = html`, html)
return gojs.Err(err)
}
func (el *Element) SetText(text string) error {
_, err := el.element.Eval(`(el, text) => el.innerText = text`, text)
return gojs.Err(err)
}
// ----------
func (el *Element) WaitVisible() error {
return gojs.Err(el.element.WaitVisible())
}
func (el *Element) WaitInvisible() error {
return gojs.Err(el.element.WaitInvisible())
}
func (el *Element) WaitEnabled() error {
return gojs.Err(el.element.WaitEnabled())
}
func (el *Element) Focus() error {
return gojs.Err(el.element.Focus())
}
func (el *Element) ScrollIntoView() error {
return gojs.Err(el.element.ScrollIntoView())
}
func (el *Element) UploadFile(filePaths []string) error {
return gojs.Err(el.element.SetFiles(filePaths))
}
func (el *Element) Hover() error {
return gojs.Err(el.element.Hover())
}
// Select 选择下拉框的选项(单选或多选)
func (el *Element) Select(values ...string) error {
if len(values) == 0 {
return nil
}
return gojs.Err(el.element.Select(values, true, rod.SelectorTypeText))
}
// GetSelectedValues 获取当前选中的值(单选返回字符串,多选返回切片)
func (el *Element) GetChecked() ([]string, error) {
selected, err := el.element.Elements(":checked")
if err != nil {
return nil, gojs.Err(err)
}
values := make([]string, len(selected))
for i, opt := range selected {
val, err := opt.Attribute("value")
if err != nil {
return nil, gojs.Err(err)
}
values[i] = u.String(val)
}
return values, nil
}
// GetAllOptions 获取所有选项(值和文本)
func (el *Element) GetOptions() ([]map[string]string, error) {
options, err := el.element.Elements("option")
if err != nil {
return nil, gojs.Err(err)
}
result := make([]map[string]string, len(options))
for i, opt := range options {
val, err := opt.Attribute("value")
if err != nil {
return nil, gojs.Err(err)
}
text, err := opt.Text()
if err != nil {
return nil, gojs.Err(err)
}
result[i] = map[string]string{
"value": u.String(val),
"text": text,
}
}
return result, nil
}
// RadioGroupValue 获取单选按钮组的当前值
func (el *Element) RadioGroupValue() (string, error) {
name, err := el.GetAttribute("name")
if err != nil {
return "", gojs.Err(err)
}
if name == nil {
return "", nil
}
checked, err := el.page.ElementX(fmt.Sprintf("input[type='radio'][name='%s']:checked", *name))
if err != nil {
return "", gojs.Err(err)
}
if checked == nil {
return "", nil
}
val, err := checked.Attribute("value")
if err != nil {
return "", gojs.Err(err)
}
return u.String(val), nil
}
// SetRadioGroupValue 设置单选按钮组的值
func (el *Element) SetRadioGroupValue(value string) error {
name, err := el.GetAttribute("name")
if err != nil {
return gojs.Err(err)
}
if name == nil {
return nil
}
radio, err := el.page.ElementX(fmt.Sprintf("input[type='radio'][name='%s'][value='%s']", *name, value))
if err != nil {
return gojs.Err(err)
}
return gojs.Err(radio.Click(proto.InputMouseButtonLeft, 1))
}
// SelectByValue 通过值选择选项
func (el *Element) SelectByValue(value string) error {
opt, err := el.element.ElementX(fmt.Sprintf(".//option[@value='%s']", value))
if err != nil {
return gojs.Err(err)
}
return gojs.Err(opt.Click(proto.InputMouseButtonLeft, 1))
}
// SelectByText 通过文本选择选项
func (el *Element) SelectByText(text string) error {
opt, err := el.element.ElementX(fmt.Sprintf(".//option[normalize-space()='%s']", text))
if err != nil {
return gojs.Err(err)
}
return gojs.Err(opt.Click(proto.InputMouseButtonLeft, 1))
}
// SetRadioValue 设置单选框值
func (el *Element) SetRadioValue(value string) error {
radio, err := el.element.ElementX(fmt.Sprintf(".//input[@type='radio'][@value='%s']", value))
if err != nil {
return gojs.Err(err)
}
return gojs.Err(radio.Click(proto.InputMouseButtonLeft, 1))
}
// GetForm 获取当前元素所属的表单
func (el *Element) GetForm() (*Element, error) {
form, err := el.element.ElementX("ancestor::form")
if err != nil {
return nil, gojs.Err(err)
}
return &Element{element: form, page: el.page}, nil
}
// GetFormData 获取表单数据
func (el *Element) GetFormData() (map[string]string, error) {
r, err := el.element.Eval(`form => {
const formData = new FormData(form);
return Object.fromEntries(formData.entries());
}`)
if err != nil {
return nil, gojs.Err(err)
}
data := r.Value.Map()
result := make(map[string]string)
for k, v := range data {
result[k] = v.String()
}
return result, nil
}
// SetValue 设置表单元素的值input/textarea
func (el *Element) SetValue(value string) error {
// _, err := el.element.Eval(`(el, v) => { el.value = v }`, value)
err := el.element.Input(value)
if err == nil {
return nil
}
el.WaitStable(nil)
return nil
}
// GetValue 获取表单元素的值
func (el *Element) GetValue() (string, error) {
prop, err := el.element.Property("value")
if err != nil {
return "", gojs.Err(err)
}
return prop.Str(), nil
}
// SetChecked 设置复选框/单选框的选中状态
func (el *Element) SetChecked(checked bool) error {
current, err := el.IsChecked()
if err != nil {
return gojs.Err(err)
}
if current != checked {
return gojs.Err(el.element.Click(proto.InputMouseButtonLeft, 1))
}
return nil
}
// IsChecked 获取复选框/单选框的选中状态
func (el *Element) IsChecked() (bool, error) {
prop, err := el.element.Property("checked")
if err != nil {
return false, gojs.Err(err)
}
return prop.Bool(), nil
}
// IsDisabled 判断元素是否被禁用
func (el *Element) IsDisabled() (bool, error) {
prop, err := el.element.Property("disabled")
if err != nil {
return false, gojs.Err(err)
}
return prop.Bool(), nil
}
const maxParentDepth = 100 // 防止无限递归
func findParentRecursive(el *rod.Element, selector string, depth int) (*rod.Element, error) {
if depth > maxParentDepth {
return nil, gojs.Err(fmt.Errorf("max recursion depth (%d) reached", maxParentDepth))
}
// 获取直接父元素
parent, err := el.Parent()
if err != nil || parent == nil {
return nil, gojs.Err(err)
}
// 检查是否匹配选择器
matches, err := parent.Matches(selector)
if err != nil {
return nil, gojs.Err(err)
}
if matches {
return parent, nil
}
// 继续向上递归查找
return findParentRecursive(parent, selector, depth+1)
}
// FindParent 向上查找匹配选择器的父元素
func (el *Element) FindParent(selector string) (*Element, error) {
parent, err := findParentRecursive(el.element, selector, 0)
if err != nil {
return nil, gojs.Err(err)
}
return &Element{element: parent, page: el.page}, nil
}
func (el *Element) FindParentN(selector string) *Element {
el, err := el.FindParent(selector)
if err != nil {
return nil
}
return el
}
// FindParent 向上查找匹配选择器的父元素
func (el *Element) FindParentX(selector string) (*Element, error) {
parent, err := el.element.ElementX(fmt.Sprintf("ancestor::%s", selector))
if err != nil {
return nil, gojs.Err(err)
}
return &Element{element: parent, page: el.page}, nil
}
func (el *Element) FindParentXN(selector string) *Element {
el, err := el.FindParentX(selector)
if err != nil {
return nil
}
return el
}
// Parent 获取直接父元素
func (el *Element) Parent() (*Element, error) {
parent, err := el.element.ElementX("..")
if err != nil {
return nil, gojs.Err(err)
}
return &Element{element: parent, page: el.page}, nil
}
// Children 获取所有直接子元素
func (el *Element) Children() ([]*Element, error) {
children, err := el.element.ElementsX("./*")
if err != nil {
return nil, gojs.Err(err)
}
result := make([]*Element, len(children))
for i, c := range children {
result[i] = &Element{element: c, page: el.page}
}
return result, nil
}
// Find 在当前元素下查找匹配选择器的第一个元素
func (el *Element) FindChild(selector string) (*Element, error) {
elem, err := el.element.Element(":scope > " + selector)
if err != nil {
return nil, gojs.Err(err)
}
return &Element{element: elem, page: el.page}, nil
}
func (el *Element) FindChildN(selector string) *Element {
el, err := el.FindChild(selector)
if err != nil {
return nil
}
return el
}
// FindAll 在当前元素下查找所有匹配选择器的元素
func (el *Element) FindChildren(selector string) ([]*Element, error) {
elements, err := el.element.Elements(":scope > " + selector)
if err != nil {
return nil, gojs.Err(err)
}
result := make([]*Element, len(elements))
for i, e := range elements {
result[i] = &Element{element: e, page: el.page}
}
return result, nil
}
// Find 在当前元素下查找匹配选择器的第一个元素
func (el *Element) Find(selector string) (*Element, error) {
elem, err := el.element.Element(selector)
if err != nil {
return nil, gojs.Err(err)
}
return &Element{element: elem, page: el.page}, nil
}
func (el *Element) FindN(selector string) *Element {
el, err := el.Find(selector)
if err != nil {
return nil
}
return el
}
// FindAll 在当前元素下查找所有匹配选择器的元素
func (el *Element) FindAll(selector string) ([]*Element, error) {
elements, err := el.element.Elements(selector)
if err != nil {
return nil, gojs.Err(err)
}
result := make([]*Element, len(elements))
for i, e := range elements {
result[i] = &Element{element: e, page: el.page}
}
return result, nil
}
// Find 在当前元素下查找匹配选择器的第一个元素
func (el *Element) FindX(selector string) (*Element, error) {
elem, err := el.element.ElementX(selector)
if err != nil {
return nil, gojs.Err(err)
}
return &Element{element: elem, page: el.page}, nil
}
func (el *Element) FindXN(selector string) *Element {
el, err := el.FindX(selector)
if err != nil {
return nil
}
return el
}
// FindAll 在当前元素下查找所有匹配选择器的元素
func (el *Element) FindAllX(selector string) ([]*Element, error) {
elements, err := el.element.ElementsX(selector)
if err != nil {
return nil, gojs.Err(err)
}
result := make([]*Element, len(elements))
for i, e := range elements {
result[i] = &Element{element: e, page: el.page}
}
return result, nil
}
// Submit 提交表单必须在FORM元素上调用
func (el *Element) Submit() error {
tagName, err := el.element.Eval("el => el.tagName")
if err != nil {
return gojs.Err(err)
}
if !strings.EqualFold(tagName.Value.String(), "FORM") {
return gojs.Err(fmt.Errorf("Submit can only be called on a FORM element"))
}
_, err = el.element.Eval("form => form.submit()")
return gojs.Err(err)
}
// Check 勾选复选框
func (el *Element) Check() error {
checked, err := el.IsChecked()
if err != nil {
return gojs.Err(err)
}
if !checked {
return gojs.Err(el.element.Click(proto.InputMouseButtonLeft, 1))
}
return nil
}
// Uncheck 取消勾选
func (el *Element) Uncheck() error {
checked, err := el.IsChecked()
if err != nil {
return gojs.Err(err)
}
if checked {
return gojs.Err(el.element.Click(proto.InputMouseButtonLeft, 1))
}
return nil
}
func (el *Element) GetBoundingRect() (*Rect, error) {
shape, err := el.element.Shape()
if err != nil {
return nil, gojs.Err(err)
}
if box := shape.Box(); box != nil {
return &Rect{X: box.X, Y: box.Y, Width: box.Width, Height: box.Height}, nil
}
return &Rect{}, nil
}
func (el *Element) GetCenter() (*Position, error) {
rect, err := el.GetBoundingRect()
if err != nil {
return nil, gojs.Err(err)
}
return &Position{X: rect.X + rect.Width/2, Y: rect.Y + rect.Height/2}, nil
}
// GetName 获取元素名称(如<input name="username">
func (el *Element) GetName() (*string, error) {
return el.GetAttribute("name")
}
// GetID 获取元素ID
func (el *Element) GetID() (*string, error) {
return el.GetAttribute("id")
}
// GetType 获取元素类型(如"text"/"checkbox"等)
func (el *Element) GetType() (*string, error) {
return el.GetAttribute("type")
}
func (el *Element) AddClass(className string) error {
_, err := el.element.Eval(`(el, className) => {
if (!el) return;
if (el.classList) {
el.classList.add(className);
} else if (el.className) {
const classes = el.className.split(' ');
if (!classes.includes(className)) {
classes.push(className);
el.className = classes.join(' ').trim();
}
}
}`, className)
return gojs.Err(err)
}
func (el *Element) GetClass() ([]string, error) {
r, err := el.element.Property("className")
if err != nil {
return nil, gojs.Err(err)
}
return u.SplitWithoutNone(r.Str(), " "), nil
}
func (el *Element) RemoveClass(className string) error {
_, err := el.element.Eval(`(el, className) => {
if (!el) return;
if (el.classList) {
el.classList.remove(className);
} else if (el.className) {
const classes = el.className.split(' ');
const index = classes.indexOf(className);
if (index !== -1) {
classes.splice(index, 1);
el.className = classes.join(' ').trim();
}
}
}`, className)
return gojs.Err(err)
}
// GetStyle 获取指定CSS样式值
func (el *Element) GetStyle(property string) (string, error) {
r, err := el.element.Eval(`(el, prop) =>
window.getComputedStyle(el).getPropertyValue(prop)`,
property)
if err != nil {
return "", gojs.Err(err)
}
return r.Value.Str(), nil
}
// SetStyle 设置CSS样式
func (el *Element) SetStyle(property, value string) error {
_, err := el.element.Eval(`(el, prop, val) => el.style.setProperty(prop, val)`, property, value)
return gojs.Err(err)
}
// SetAttribute 设置元素属性值
// name: 属性名称 value: 属性值
func (el *Element) SetAttribute(name, value string) error {
_, err := el.element.Eval(`(e, n, v) => e.setAttribute(n, v)`, name, value)
return gojs.Err(err)
}
// HasAttribute 检查属性是否存在
func (el *Element) HasAttribute(name string) (bool, error) {
r, err := el.element.Eval(`(el, name) => el.hasAttribute(name)`, name)
if err != nil {
return false, gojs.Err(err)
}
return r.Value.Bool(), nil
}
// GetAttribute 获取元素属性值
// name: 属性名称
// 返回值: 属性值字符串,若不存在则返回空字符串
func (el *Element) GetAttribute(name string) (*string, error) {
val, err := el.element.Attribute(name)
return val, gojs.Err(err)
}
// RemoveAttribute 移除元素属性
// name: 要移除的属性名称
func (el *Element) RemoveAttribute(name string) error {
_, err := el.element.Eval(`(e, n) => e.removeAttribute(n)`, name)
return gojs.Err(err)
}
// SetProperty 设置 JavaScript 属性
// name: 属性名称 value: 属性值(可以是任意类型)
func (el *Element) SetProperty(name string, value interface{}) error {
_, err := el.element.Eval(`(el, n, v) => el[n] = v`, name, value)
return gojs.Err(err)
}
// GetProperty 获取 JavaScript 属性值
// name: 属性名称
// 返回值: 属性值interface{} 类型)
func (el *Element) GetProperty(name string) (interface{}, error) {
prop, err := el.element.Property(name)
if err != nil {
return nil, gojs.Err(err)
}
return prop.Val(), nil
}
// HasProperty 检查属性是否存在
func (el *Element) HasProperty(name string) (bool, error) {
r, err := el.element.Eval(`(el, name) => el.hasOwnProperty(name)`, name)
if err != nil {
return false, gojs.Err(err)
}
return r.Value.Bool(), nil
}
func (el *Element) RemoveProperty(name string) error {
_, err := el.element.Eval(`(el, name) => delete el[name]`, name)
return gojs.Err(err)
}
// IsInteractable 元素是否可交互
func (el *Element) IsInteractable() (bool, error) {
_, err := el.element.Interactable()
2025-07-22 20:45:03 +08:00
if errors.Is(err, &rod.NotInteractableError{}) {
2025-07-21 00:09:21 +08:00
return false, nil
}
if err != nil {
return false, gojs.Err(err)
}
return true, nil
}
func (el *Element) WaitStable(ms *int) error {
if ms == nil {
ms = u.IntPtr(100)
}
return gojs.Err(el.element.Timeout(time.Second).WaitStable(time.Millisecond * time.Duration(*ms)))
}
// 元素级
func (el *Element) KeyPress(keys ...string) error {
if err := el.element.Focus(); err != nil {
return gojs.Err(err)
}
return keyAction(el.page, "press", keys...)
}
func (el *Element) KeyRelease(keys ...string) error {
if err := el.element.Focus(); err != nil {
return gojs.Err(err)
}
return keyAction(el.page, "release", keys...)
}
func (el *Element) KeyType(keys ...string) error {
if err := el.element.Focus(); err != nil {
return gojs.Err(err)
}
return keyAction(el.page, "type", keys...)
}
func (el *Element) Key(keys ...string) error {
2025-07-21 00:09:21 +08:00
if err := el.element.Focus(); err != nil {
return gojs.Err(err)
}
controlKeys := keyFilter(true, keys...)
// 按下所有控制键
err := keyAction(el.page, "press", controlKeys...)
if err == nil {
// 输入所有非控制键
err = keyAction(el.page, "type", keyFilter(false, keys...)...)
}
if err == nil {
// 释放所有控制键
err = keyAction(el.page, "release", controlKeys...)
}
return err
2025-07-21 00:09:21 +08:00
}
// IsVisible 判断元素是否可见
func (el *Element) IsVisible() (bool, error) {
visible, err := el.element.Visible()
if err != nil {
return false, gojs.Err(err)
}
return visible, nil
}
// GetPosition 获取元素位置(中心点)
func (el *Element) GetPosition() (*Position, error) {
shape, err := el.element.Shape()
if err != nil {
return nil, gojs.Err(err)
}
rect := shape.Box()
return &Position{
X: rect.X + rect.Width/2,
Y: rect.Y + rect.Height/2,
}, nil
}
// GetComputedStyle 获取元素所有计算样式
func (el *Element) GetComputedStyle() (map[string]string, error) {
r, err := el.element.Eval(`(el) => {
const styles = window.getComputedStyle(el);
const result = {};
for (let i = 0; i < styles.length; i++) {
const prop = styles[i];
result[prop] = styles.getPropertyValue(prop);
}
return result;
}`)
if err != nil {
return nil, gojs.Err(err)
}
style := r.Value.Map()
result := make(map[string]string)
for key, value := range style {
result[key] = value.String()
}
return result, nil
}