基于Golang+Vue編寫一個手機遠程控制電腦的懶人工具
前言
躺在床上投屏到電腦的時候, 調(diào)節(jié)音量和一些簡單的操作還需要起身操作, 覺得麻煩, 就開發(fā)了這么一個小工具。
思路
- Go語言負責后端,負責模擬鍵盤輸入和鼠標移動
- Vue負責頁面編寫,調(diào)用后端接口,使用petite-vue單個頁面開發(fā), 夠輕量
- go直接調(diào)用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
// 將查詢參數(shù)轉(zhuǎn)換為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'
// 發(fā)起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><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實時通信后端, 做了防抖處理, 避免請求太多, 靈敏度高的時候鼠標會有卡頓, 還待優(yōu)化
后端
初始化項目
go mod init dcontrol
下載依賴
go get github.com/getlantern/systray go get github.com/spf13/viper go get github.com/gorilla/websocket ...
編寫代碼
1.主函數(shù)
- 通過
go:embed指定靜態(tài)資源目錄, 可以直接將前端打包的資源封裝入exe中 http.HandleFunc將api前綴交給函數(shù)處理, 在函數(shù)里面具體處理子路由- 配置文件通過
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)
// 注冊靜態(tài)資源
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("打開網(wǎng)頁", "打開系統(tǒng)網(wǎng)頁")
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.調(diào)用user32.dll, 實現(xiàn)模擬鍵盤輸入和鼠標移動
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服務監(jiān)聽
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目錄


以上就是基于Golang+Vue編寫一個手機遠程控制電腦的懶人工具的詳細內(nèi)容,更多關于Go Vue手機遠程控制電腦的資料請關注腳本之家其它相關文章!
相關文章
go強制類型轉(zhuǎn)換type(a)以及范圍引起的數(shù)據(jù)差異
這篇文章主要為大家介紹了go強制類型轉(zhuǎn)換type(a)以及范圍引起的數(shù)據(jù)差異,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-10-10
gtoken替換jwt實現(xiàn)sso登錄的問題小結(jié)
這篇文章主要介紹了gtoken替換jwt實現(xiàn)sso登錄,主要介紹了替換jwt的原因分析及gtoken的優(yōu)勢,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-05-05
Go?io/fs.FileMode文件系統(tǒng)基本操作和權限管理深入理解
這篇文章主要為大家介紹了Go?io/fs.FileMode文件系統(tǒng)基本操作和權限管理深入理解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2024-01-01

