http/page.go
2025-07-23 20:30:29 +08:00

1436 lines
36 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
originPage *rod.Page
// 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)
}
pg := &Page{originPage: page}
pg.ResetTimeout()
pg.WaitPageLoad()
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)
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)
}
pg.WaitPageLoad()
return nil
}
// WaitLoad 等待页面加载完成
func (pg *Page) WaitPageLoad() error {
time.Sleep(100 * time.Millisecond)
err := pg.page.WaitLoad()
if err != nil {
return gojs.Err(err)
}
pg.ResetTimeout()
return nil
}
// 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)))
}
// 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)
}
// 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
}
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)
}
pg.WaitPageLoad()
return nil
}
func (pg *Page) Forward() error {
err := pg.page.NavigateForward()
if err != nil {
return gojs.Err(err)
}
pg.WaitPageLoad()
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) {
r, err := el.element.Eval(`el => {return el.innerHTML}`)
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()
if errors.Is(err, &rod.NotInteractableError{}) {
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 {
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
}
// 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
}