封装fetch请求后端api
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function request(options, temp) { let opts = temp if (typeof options != 'string') { opts = options } let { url, method = 'GET', params = {}, data = null } = opts || {}; if (typeof options == 'string') url = options
// 将查询参数转换为URL编码字符串 const queryString = Object.keys(params).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');
// 构建完整的请求URL let finalUrl = url + (url.includes('?') ? '&' : '?') + queryString; finalUrl = finalUrl.includes('http') ? finalUrl : `${baseApi}${finalUrl}`
// 设置请求头部 const headers = {}; if (data) headers['Content-Type'] = 'application/json'
// 发起Fetch请求 return new Promise((resolve, reject) => { fetch(finalUrl, { method, headers, body: data ? JSON.stringify(data) : null}).then(res => { if (!res.ok) throw new Error(`HTTP error! status: ${response.status}`); return res.json(); }).then(r => resolve(r)).catch(e => reject(e)); }); } |
websocket初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
websocket = new WebSocket(`ws://${location.host}/ws`); websocket.onmessage = function(evt) { if(evt.data=="reload"){ window.location.pathname = "/"; window.location.reload(true); } }; websocket.onopen = function() { console.log('socket open....'); document.getElementById('touch').style.display = 'block'; }; websocket.onclose = function() { console.log('socket close....'); document.getElementById('touch').style.display = 'none'; }; let startTime = 0; function sendData(data, force) { const curr = new Date().getTime(); if (curr - startTime > 60 || force) { console.log('socket send....', data); websocket.send(data); startTime = curr; } }; |
按键布局
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<div id="keyboard"> <div class="f2"> <i k="CTRL,A">全选</i><i k="CTRL,C">复制</i><i k="CTRL,V">粘贴</i><i k="CTRL,X">剪切</i><i k="CTRL,Z">撤销</i><i k="CTRL,S">保存</i> </div> <div class="f2"> <i k="CTRL,SHIFT">输入法</i><i k="ALT,F4">关闭</i><i k="WIN,D">桌面</i><i k="MEDIA_PREV_TRACK">上曲</i><i k="MEDIA_NEXT_TRACK">下曲</i> <i k="MEDIA_PLAY_PAUSE">播放</i><i k="VOLUME_DOWN">音量-</i><i k="VOLUME_UP">音量+</i><i k="VOLUME_MUTE">静音</i> </div> <div class="f1"><i>ESC</i><i>F1</i><i>F2</i><i>F3</i><i>F4</i><i>F5</i><i>F6</i><i>F7</i><i>F8</i><i>F9</i><i>F10</i><i>F11</i><i>F12</i></div> <div><i k="OEM_3">`</i><i>1</i><i>2</i><i>3</i><i>4</i><i>5</i><i>6</i><i>7</i><i>8</i><i>9</i><i>0</i><i>BACK</i></div> <div><i>TAB</i><i>Q</i><i>W</i><i>E</i><i>R</i><i>T</i><i>Y</i><i>U</i><i>I</i><i>O</i><i>P</i></div> <div><i k="CAPITAL">CAPS</i><i>A</i><i>S</i><i>D</i><i>F</i><i>G</i><i>H</i><i>J</i><i>K</i><i>L</i><i k="ENTER">回车</i></div> <div><i>SHFT</i><i>Z</i><i>X</i><i>C</i><i>V</i><i>B</i><i>N</i><i>M</i><i k="HOME">HM</i><i k="UP">↑</i><i k="END">ED</i></div> <div> <i k="OEM_COMMA">,</i><i k="OEM_PERIOD">.</i><i k="OEM_2">/ </i><i k="OEM_4"> { </i><i k="OEM_6"> } </i><i k="SEMICOLON"> ; </i> <i k="OEM_7"> ' </i><i k="OEM_MINU"> - </i><i k="OEM_PLUS"> + </i><i k="LEFT">←</i><i k="DOWN">↓</i><i k="RIGHT">→</i> </div> <div><span onclick="toggle('#keyboard')">隐藏</span><i style="flex: 1; line-height: 46px; margin-left: 10px;">SPACE</i></div> </div> |
其他说明
鼠标移动使用websocket实时通信后端, 做了防抖处理, 避免请求太多, 灵敏度高的时候鼠标会有卡顿, 还待优化
初始化项目
1 |
go mod init dcontrol |
下载依赖
1 2 3 4 |
go get github.com/getlantern/systray go get github.com/spf13/viper go get github.com/gorilla/websocket ... |
编写代码
1.主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//go:embed webapp var f embed.FS
func main() {
port := flag.Int("p", 0, "server port") base.RunPort = *port filePath := flag.String("f", "./config.yml", "server config file") // dir := flag.String("d", "./webapp", "server static dir") flag.Parse() //1.加载配置 setting.Init(*filePath) base.RunPort = setting.Conf.Port if *port != 0 { base.RunPort = *port } addr := fmt.Sprintf(":%d", base.RunPort)
http.HandleFunc("/control-api/monitor/", monitor.HandleApi) http.HandleFunc("/ws", ws.ServeWs)
// 注册静态资源 st, _ := fs.Sub(f, "webapp") http.Handle("/", http.StripPrefix("/", http.FileServer(http.FS(st))))
err := http.ListenAndServe(addr, nil) if err != nil { fmt.Println("start http error: ", err) } fmt.Println("start http success ", base.RunPort) } |
2.HandleApi 处理子路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
func HandleApi(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") //允许访问所有域 w.Header().Add("Access-Control-Allow-Headers", "Content-Type") //header的类型 w.Header().Set("Content-Type", "application/json")
// 获取请求路径 strings.HasSuffix path := r.URL.Path fmt.Println("redis HandleApi path:", path)
switch { case strings.Contains(path, "/getKeyMap"): getKeyMap(w, r) case strings.Contains(path, "/getIp"): getIp(w, r) case strings.Contains(path, "/getApps"): getApps(w, r) case strings.Contains(path, "/sendkey"): sendkey(w, r) case strings.Contains(path, "/open"): open(w, r) default: http.NotFound(w, r) } } |
3.windows任务栏添加应用小图标和菜单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
func GenTaskBarIcon() { if runtime.GOOS == "windows" { systray.Run(onReady, onExit) } }
func onReady() { systray.SetIcon(iconData) systray.SetTitle("D-Control") systray.SetTooltip("D-Control 右键点击打开菜单!") menuOpen := systray.AddMenuItem("打开网页", "打开系统网页") systray.AddSeparator() menuQuit := systray.AddMenuItem("退出", "退出程序")
go func() { for { select { case <-menuOpen.ClickedCh: OpenBrowser(fmt.Sprintf("http://localhost:%d/", base.RunPort)) case <-menuQuit.ClickedCh: systray.Quit() os.Exit(0) } } }()
}
func onExit() {} |
4.调用user32.dll, 实现模拟键盘输入和鼠标移动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
var dll = syscall.NewLazyDLL("user32.dll") var procKeyBd = dll.NewProc("keybd_event") var procSetCursorPos = dll.NewProc("SetCursorPos") var procGetCursorPos = dll.NewProc("GetCursorPos") var procMouseEvent = dll.NewProc("mouse_event")
func SetMouse(x int, y int, isDiff bool) { if isDiff { procGetCursorPos.Call(uintptr(unsafe.Pointer(&CursorPos))) fmt.Println("cursorPos: ", CursorPos.X, CursorPos.Y) procSetCursorPos.Call(uintptr(CursorPos.X+int32(x)), uintptr(CursorPos.Y+int32(y))) } else { procSetCursorPos.Call(uintptr(int32(x)), uintptr(int32(y))) } }
func ClickMouse(str string) { if str == "L" { procMouseEvent.Call(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) time.Sleep(50 * time.Millisecond) // 短暂延迟 procMouseEvent.Call(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) } else if str == "R" { procMouseEvent.Call(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0) time.Sleep(50 * time.Millisecond) // 短暂延迟 procMouseEvent.Call(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0) } else if str == "M" { procMouseEvent.Call(MOUSEEVENTF_MIDDLEDOWN, 0, 0, 0, 0) time.Sleep(50 * time.Millisecond) // 短暂延迟 procMouseEvent.Call(MOUSEEVENTF_MIDDLEUP, 0, 0, 0, 0) } }
func downKey(key int) { flag := 0 if key < 0xFFF { // Detect if the key code is virtual or no flag |= _KEYEVENTF_SCANCODE } else { key -= 0xFFF } vkey := key + 0x80 procKeyBd.Call(uintptr(key), uintptr(vkey), uintptr(flag), 0) }
func upKey(key int) { flag := _KEYEVENTF_KEYUP if key < 0xFFF { flag |= _KEYEVENTF_SCANCODE } else { key -= 0xFFF } vkey := key + 0x80 procKeyBd.Call(uintptr(key), uintptr(vkey), uintptr(flag), 0) }
// 按键映射Map var KeyMap = map[string]int{ "SHIFT": 0x10 + 0xFFF, "CTRL": 0x11 + 0xFFF, "ALT": 0x12 + 0xFFF, "LSHIFT": 0xA0 + 0xFFF, "RSHIFT": 0xA1 + 0xFFF, "LCONTROL": 0xA2 + 0xFFF, "RCONTROL": 0xA3 + 0xFFF, "WIN": 0x5B + 0xFFF, ... } |
5.websocket服务监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
func ServeWs(w http.ResponseWriter, r *http.Request) { ws, err := upgrader.Upgrade(w, r, nil) if err != nil { fmt.Println("upgrade:", err) return } fmt.Println("ServeWs connected......")
defer ws.Close()
for { // 读取消息 messageType, msg, err := ws.ReadMessage() if err != nil { fmt.Println("Error while reading message:", err) break }
// 打印接收到的消息 fmt.Printf("ws Received: %s\n", msg) wsdata := string(msg) if wsdata == "pos,click" { // go keys.RunKeys(keys.KeyMap["LBUTTON"]) keys.ClickMouse("L") } else if wsdata == "pos,longclick" { keys.ClickMouse("R") } else if strings.HasPrefix(wsdata, "pos,start") { parts := strings.Split(wsdata, ",") if len(parts) == 4 { fx, _ := strconv.ParseFloat(parts[2], 64) fy, _ := strconv.ParseFloat(parts[3], 64) keys.SetMouse(int(fx), int(fy), true) } }
// 可以选择回送消息给客户端 err = ws.WriteMessage(messageType, msg) if err != nil { fmt.Println("Error while writing message:", err) break } } } |
搭配macast开源投屏神器, 躺在床上手机随时投屏视频到电脑上, 手机再遥控电脑音量和简易操作, 美滋滋了
源码和程序截图详见github.com/dhjz/dcontrol
页面效果图见appimg目录