开发者

基于Golang+Vue编写一个手机远程控制电脑的懒人工具

目录
  • 前言
  • 思路
  • 使用技术
  • 开始操作
    • 前端
    • 后端
  • 后言
    • 源码地址

      前言

      躺在床上投屏到电脑的时候, 调节音量和一些简单的操作还需要起身操作, 觉得麻烦, 就开发了这么一个小工具。

      思路

      • Go语言负责后端,负责模拟键盘输入和鼠标移动
      • vue负责页面编写,调用后端接口,使用petite-vue单个页面开发, 够轻量
      • go直接调用user32.dll完成键盘和鼠标的操作, 不依赖三方框架
      • 前端完全基于浏览器, 有微信有扫一扫就能直接打开控制页
      • 按键传输采用http请求, 鼠标移动采用websocket

      使用技术

      • 前端:petite-vue、qrcode
      • 后端:go1.20、systray、websocket

      开始操作

      前端

      封装fetch请求后端api

      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初始化

      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;
      		}
      };
      

      按键布局

      <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>www.devze.com<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&gphpt;<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>SPACE</i></div>
      </div>
      

      其他说明

      鼠标移动使用websocket实时通信后端, 做了防抖处理, 避免请求太多, 灵敏度高的时候鼠标会有卡顿, 还待优化

      后端

      初始化项目

      go mod init dcontrol
      

      下载依赖

      go get github.com/getlantern/systray
      go get github.com/spf13/viper
      go get github.com/gorilla/websocket
      ...
      

      编写代码

      1.主函数

      • 通过go:embed 指定静态资源目录, 可以直接将前端打包的资源封装入exe中
      • http.HandleFunc 将api前缀交给函数处理, 在函数里面具体处理子路由
      • 配置文件通过config.yml加载, 可以指定快捷应用和启动端口
      //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 处理子路由

      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任务栏添加应用小图标和菜单

      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, 实现模拟键盘输入和鼠标移动

      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_M编程IDDLEDOWN, 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服务监听

      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)
      			}
      		编程}
      
      		// 可以选择回送消息给客户端
      qHqdt		err = ws.WriteMessage(messageType, msg)
      		if err != nil {
      			fmt.Println("Error while writing message:", err)
      			break
      		}
      	}
      }
      

      后言

      搭配MACast开源投屏神器, 躺在床上手机随时投屏视频到电脑上, 手机再遥控电脑音量和简易操作, 美滋滋了

      源码地址

      源码和程序截图详见github.com/dhjz/dcontrol

      页面效果图见appimg目录

      基于Golang+Vue编写一个手机远程控制电脑的懒人工具

      基于Golang+Vue编写一个手机远程控制电脑的懒人工具

      以上就是基于golang+Vue编写一个手机远程控制电脑的懒人工具的详细内容,更多关于Go Vue手机远程控制电脑的资料请关注编程客栈(www.devze.com)其它相关文章!

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新开发

      开发排行榜