2025-09-08 20:53:24 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="main">
|
|
|
|
|
|
<el-scrollbar class="left"> <!-- 左边栏 -->
|
|
|
|
|
|
<div class="center-line"> <!-- 左边栏按钮 -->
|
|
|
|
|
|
<div v-for="(btn, index) in buttons" :key="index" style="width: 100%;">
|
|
|
|
|
|
<div v-if="btn.show?.()" class="left-button" :style="btn.style ? btn.style() : {}" @click="btn.onClick"
|
|
|
|
|
|
@mouseenter="hoverIndex = index" @mouseleave="hoverIndex = null">
|
|
|
|
|
|
<img :src="hoverIndex === index ? btn.img.hover : btn.img.normal" alt="">
|
|
|
|
|
|
{{ btn.label }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="position: absolute;left: 20px; bottom: 20px;">
|
|
|
|
|
|
<el-button @click="showHostDlg = true">执行主播库</el-button>
|
|
|
|
|
|
<el-button type="info" @click="uploadLogFile">上传日志</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-scrollbar>
|
|
|
|
|
|
<!-- 中间手机区域 -->
|
|
|
|
|
|
<div class="content" @click.self="selectedDevice = 999">
|
|
|
|
|
|
<div v-if="isImg" class="video-container" v-for="(device, index) in deviceInformation" :key="device.deviceId">
|
2025-09-08 23:12:00 +08:00
|
|
|
|
<div class="video-canvas" :class="{ active: selectedDevice === index }" :style="getCanvasStyle(index)"
|
2025-09-08 20:53:24 +08:00
|
|
|
|
@click="selectDevice(index)">
|
2025-09-08 23:12:00 +08:00
|
|
|
|
<img class="stream" :src="'http://localhost:' + device.screenPort" />
|
|
|
|
|
|
<canvas v-show="selectedDevice === index" class="overlay"
|
2025-09-08 20:53:24 +08:00
|
|
|
|
@mousedown.stop="(e) => onCanvasDown(device.deviceId, e, index)"
|
|
|
|
|
|
@mouseup.stop="(e) => onCanvasUp(device.deviceId, e, index)"
|
2025-09-08 23:12:00 +08:00
|
|
|
|
@mousemove.stop="(e) => onCanvasMove(device.deviceId, e, index)" />
|
2025-09-08 20:53:24 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="input-info" v-show="selectedDevice == index">
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-button" @click="getMesList(device.deviceId)">获取当前聊天记录</div>
|
|
|
|
|
|
<!-- <div class="app-button" @click="mqSend()">mq</div> -->
|
|
|
|
|
|
<!-- <div class="app-button"
|
|
|
|
|
|
@click="wsActions.clickCopyList(device.deviceId, index); openShowChat = true; istranslate = true">
|
|
|
|
|
|
翻译本页对话</div>
|
|
|
|
|
|
<div class="app-button" @click="wsActions.clickCopyList(device.deviceId, index); openShowChat = true;">
|
|
|
|
|
|
回复消息</div>
|
|
|
|
|
|
<div class="app-button" @click="wsActions.test(device.deviceId, index)">打印ui节点树</div>
|
|
|
|
|
|
<div class="app-button" @click="wsActions.isOneLive(device.deviceId, index)">判断单人还是双人</div>
|
|
|
|
|
|
<div class="app-button" @click="wsActions.slideDown(device.deviceId, index)">下滑</div>
|
|
|
|
|
|
<div class="app-button" @click="wsActions.killNow(device.deviceId, index)">关闭当前应用</div>
|
|
|
|
|
|
<div class="app-button" @click="chooseFile(device.deviceId, index, 1, wsActions)">安装 APK
|
|
|
|
|
|
文件</div>
|
|
|
|
|
|
<div class="app-button" @click="chooseFile(device.deviceId, index, 2, wsActions)">
|
|
|
|
|
|
传送文件</div> -->
|
|
|
|
|
|
<!-- <div style="display: flex;">
|
|
|
|
|
|
<input style="border: 1px solid #000;margin:0px 14px;" v-model="textContent[index]" type="text"></input>
|
|
|
|
|
|
<div class="app-button" style="margin: 0px;height: 40px;width: 60px;font-size: 14px;"
|
|
|
|
|
|
@click="setComText(index)">发送
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div> -->
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="right center-justify" @click.self="selectedDevice = 999">
|
|
|
|
|
|
<!-- <div style="margin: 30px;"></div> -->
|
|
|
|
|
|
<ChatDialog :visible="openShowChat" :messages="chatList" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<MultiLineInputDialog v-model:visible="showDialog" :initialText='""' :title="dialogTitle" :index="selectedDevice"
|
|
|
|
|
|
@confirm="onDialogConfirm" @cancel="stopAll" />
|
|
|
|
|
|
<HostListManagerDialog v-model:visible="showHostDlg" @save="onHostSaved" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 定时调度配置弹窗 -->
|
|
|
|
|
|
<el-dialog v-model="showScheduleDlg" title="定时调度(每小时)" width="420px">
|
|
|
|
|
|
<div style="display:grid;grid-template-columns: 100px 1fr; gap:12px; align-items:center;">
|
|
|
|
|
|
<div>片段 A</div>
|
|
|
|
|
|
<div style="display:flex; gap:8px; align-items:center;">
|
|
|
|
|
|
<el-select v-model="schedAKey" style="width:140px;">
|
|
|
|
|
|
<el-option label="一键关注" value="follow" />
|
|
|
|
|
|
<el-option label="刷视频(养号)" value="like" />
|
|
|
|
|
|
<el-option label="刷直播" value="brushLive" />
|
|
|
|
|
|
<el-option label="监测消息" value="listen" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-input-number v-model="schedAMin" :min="1" :max="59" />
|
|
|
|
|
|
<span>分钟</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>片段 B</div>
|
|
|
|
|
|
<div style="display:flex; gap:8px; align-items:center;">
|
|
|
|
|
|
<el-select v-model="schedBKey" style="width:140px;">
|
|
|
|
|
|
<!-- <el-option label="一键关注" value="follow" /> -->
|
|
|
|
|
|
<el-option label="刷视频(养号)" value="like" />
|
|
|
|
|
|
<el-option label="刷直播" value="brushLive" />
|
|
|
|
|
|
<el-option label="监测消息" value="listen" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-input-number v-model="schedBMin" :min="1" :max="59" />
|
|
|
|
|
|
<span>分钟</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>总时长</div>
|
|
|
|
|
|
<div><b>{{ schedAMin + schedBMin }}</b> 分钟(必须等于 60)</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- <div>启用调度</div>
|
|
|
|
|
|
<div><el-switch v-model="scheduleEnabled" /></div> -->
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="showScheduleDlg = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="saveSchedule">开启</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-09-08 23:12:00 +08:00
|
|
|
|
import { ref, onMounted, onUnmounted, onBeforeUnmount, watch, inject, computed } from "vue";
|
2025-09-08 20:53:24 +08:00
|
|
|
|
import { useRouter } from 'vue-router';
|
|
|
|
|
|
import {
|
|
|
|
|
|
setphoneXYinfo, getphoneXYinfo, getUser,
|
|
|
|
|
|
getHostList, setHostList, getContentpriList,
|
|
|
|
|
|
setContentpriList, getContentList, setContentList,
|
|
|
|
|
|
setsessionId, getsessionId
|
|
|
|
|
|
} from '@/stores/storage'
|
|
|
|
|
|
import { connectSSE } from '@/utils/sseUtils'
|
|
|
|
|
|
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
|
|
|
|
|
|
import { chat, translationToChinese, translation } from "@/api/chat";
|
|
|
|
|
|
import HostListManagerDialog from '@/components/HostListManagerDialog.vue'
|
|
|
|
|
|
import MultiLineInputDialog from '@/components/MultiLineInputDialog.vue'; // 根据实际路径修改
|
|
|
|
|
|
import ChatDialog from '@/components/ChatDialog.vue'
|
|
|
|
|
|
import { pickTikTokBundleId } from '@/utils/arrUtils'
|
|
|
|
|
|
import { logout } from '@/api/account';
|
|
|
|
|
|
import {
|
|
|
|
|
|
getDeviceList,
|
|
|
|
|
|
toHome,
|
|
|
|
|
|
swipeAction,
|
|
|
|
|
|
tapAction,
|
|
|
|
|
|
growAccount,
|
|
|
|
|
|
stopScript,
|
|
|
|
|
|
watchLiveForGrowth,
|
|
|
|
|
|
monitorMessages,
|
|
|
|
|
|
passAnchorData,
|
|
|
|
|
|
addTempAnchorData,
|
|
|
|
|
|
deviceAppList,
|
|
|
|
|
|
launchApp,
|
|
|
|
|
|
getChatTextInfo,
|
|
|
|
|
|
setLoginInfo,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} from '@/api/ios';
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
const openShowChat = ref([true])
|
|
|
|
|
|
//主播库
|
|
|
|
|
|
const showHostDlg = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
let hostList = []
|
|
|
|
|
|
//查询列表轮询
|
|
|
|
|
|
let getListtimer = null;
|
|
|
|
|
|
let userdata = getUser();
|
|
|
|
|
|
let chatList = ref([])
|
|
|
|
|
|
let isImg = ref(true)
|
|
|
|
|
|
// 引入刷新方法
|
|
|
|
|
|
const reload = inject("reload")
|
|
|
|
|
|
const reloadImg = () => {
|
|
|
|
|
|
isImg.value = false
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
isImg.value = true
|
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
}
|
|
|
|
|
|
//start弹窗
|
|
|
|
|
|
let isMsgPop = ref(false)
|
|
|
|
|
|
//缓存主播联动数组
|
|
|
|
|
|
let stroageHost = ref([])
|
|
|
|
|
|
let runType = ref('')
|
|
|
|
|
|
let isMonitorOn = ref(false)
|
|
|
|
|
|
const hoverIndex = ref(null) //选中
|
|
|
|
|
|
let showDialog = ref(false);//弹窗是否显示
|
|
|
|
|
|
let dialogTitle = ref('');//当前弹窗类型
|
|
|
|
|
|
let deviceInformation = ref([])
|
|
|
|
|
|
// 你可以用这种方式声明按钮们
|
|
|
|
|
|
//联动用作标记
|
|
|
|
|
|
let batchMode = ref('init'); // 'init' | 'follow'(仅作标记)
|
|
|
|
|
|
|
|
|
|
|
|
// 当前是否被其它模式占用(四个互斥按钮专用)
|
|
|
|
|
|
const isLocked = (type) => !!runType.value && runType.value !== type
|
|
|
|
|
|
|
|
|
|
|
|
// 互斥按钮的样式:激活=红,锁定=半透明且禁点
|
|
|
|
|
|
const ctrlStyle = (type) => ({
|
|
|
|
|
|
backgroundColor: runType.value === type ? 'red' : '',
|
|
|
|
|
|
opacity: isLocked(type) ? 0.5 : 1,
|
|
|
|
|
|
pointerEvents: isLocked(type) ? 'none' : 'auto',
|
|
|
|
|
|
cursor: isLocked(type) ? 'not-allowed' : 'pointer',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const buttons = [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '刷新',
|
|
|
|
|
|
onClick: () => reloadImg(),
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '打开tiktok',
|
|
|
|
|
|
onClick: () => openTk(),
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn2.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn2-2.png', import.meta.url).href
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '返回主页',
|
|
|
|
|
|
onClick: () => {
|
|
|
|
|
|
deviceInformation.value.forEach((item) => {
|
|
|
|
|
|
toHome({ udid: item.deviceId })
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn3.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn3-3.png', import.meta.url).href
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '刷直播',
|
|
|
|
|
|
onClick: () => {
|
|
|
|
|
|
if (runType.value == 'brushLive') {
|
|
|
|
|
|
deviceInformation.value.forEach((item) => {
|
|
|
|
|
|
stopScript({ udid: item.deviceId })
|
|
|
|
|
|
})
|
|
|
|
|
|
runType.value = ''
|
|
|
|
|
|
return
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 若被其它模式占用:直接返回(已在样式层禁点,这里双保险)
|
|
|
|
|
|
if (isLocked('brushLive')) return
|
|
|
|
|
|
|
|
|
|
|
|
runType.value = 'brushLive'
|
|
|
|
|
|
deviceInformation.value.forEach((item) => watchLiveForGrowth({ udid: item.deviceId }))
|
|
|
|
|
|
},
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn4.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn4-4.png', import.meta.url).href
|
|
|
|
|
|
},
|
|
|
|
|
|
style: () => ctrlStyle('brushLive')
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '刷视频',
|
|
|
|
|
|
onClick: () => {
|
|
|
|
|
|
if (runType.value == 'like') {
|
|
|
|
|
|
deviceInformation.value.forEach((item) => {
|
|
|
|
|
|
stopScript({ udid: item.deviceId })
|
|
|
|
|
|
})
|
|
|
|
|
|
runType.value = ''
|
|
|
|
|
|
return
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (isLocked('like')) return
|
|
|
|
|
|
|
|
|
|
|
|
runType.value = 'like'
|
|
|
|
|
|
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
|
|
|
|
|
|
},
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn5.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn5-5.png', import.meta.url).href
|
|
|
|
|
|
},
|
|
|
|
|
|
style: () => ctrlStyle('like')
|
|
|
|
|
|
},
|
|
|
|
|
|
// {
|
|
|
|
|
|
// label: '一键关注并打招呼',
|
|
|
|
|
|
// onClick: () => {
|
|
|
|
|
|
|
|
|
|
|
|
// if (runType.value == 'follow') {
|
|
|
|
|
|
// deviceInformation.value.forEach((item) => {
|
|
|
|
|
|
// stopScript({ udid: item.deviceId })
|
|
|
|
|
|
// })
|
|
|
|
|
|
// runType.value = ''
|
|
|
|
|
|
// return
|
|
|
|
|
|
// };
|
|
|
|
|
|
|
|
|
|
|
|
// if (isLocked('follow')) return
|
|
|
|
|
|
// showDialog.value = true;
|
|
|
|
|
|
// dialogTitle.value = '主播ID';
|
|
|
|
|
|
|
|
|
|
|
|
// },
|
|
|
|
|
|
// show: () => true,
|
|
|
|
|
|
// img: {
|
|
|
|
|
|
// normal: new URL('@/assets/video/leftBtn6.png', import.meta.url).href,
|
|
|
|
|
|
// hover: new URL('@/assets/video/leftBtn6-6.png', import.meta.url).href
|
|
|
|
|
|
// },
|
|
|
|
|
|
// style: () => ctrlStyle('follow')
|
|
|
|
|
|
|
|
|
|
|
|
// },
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '启动调度任务',
|
|
|
|
|
|
onClick: () => openScheduleDialog(),
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn6.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn6-6.png', import.meta.url).href
|
|
|
|
|
|
},
|
|
|
|
|
|
style: () => ctrlStyle('follow')
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '监测消息',
|
|
|
|
|
|
onClick: () => {
|
|
|
|
|
|
if (runType.value == 'lisen') {
|
|
|
|
|
|
deviceInformation.value.forEach((item) => {
|
|
|
|
|
|
stopScript({ udid: item.deviceId })
|
|
|
|
|
|
})
|
|
|
|
|
|
runType.value = ''
|
|
|
|
|
|
return
|
|
|
|
|
|
};
|
|
|
|
|
|
if (isLocked('lisen')) return
|
|
|
|
|
|
|
|
|
|
|
|
runType.value = 'lisen'
|
|
|
|
|
|
deviceInformation.value.forEach((item) => monitorMessages({ udid: item.deviceId }))
|
|
|
|
|
|
},
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href
|
|
|
|
|
|
},
|
|
|
|
|
|
style: () => ctrlStyle('lisen')
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
// {
|
|
|
|
|
|
// label: '定时调度',
|
|
|
|
|
|
// onClick: () => openScheduleDialog(),
|
|
|
|
|
|
// show: () => true,
|
|
|
|
|
|
// img: {
|
|
|
|
|
|
// normal: new URL('@/assets/video/leftBtn1.png', import.meta.url).href,
|
|
|
|
|
|
// hover: new URL('@/assets/video/leftBtn1-1.png', import.meta.url).href
|
|
|
|
|
|
// }
|
|
|
|
|
|
// },
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '全部停止',
|
|
|
|
|
|
onClick: () => stopAll(),
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn8.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn8-8.png', import.meta.url).href
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '登出',
|
|
|
|
|
|
onClick: () => {
|
|
|
|
|
|
logout({ userId: userdata.id, tenantId: userdata.tenantId })
|
|
|
|
|
|
router.push('/')
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
show: () => true,
|
|
|
|
|
|
img: {
|
|
|
|
|
|
normal: new URL('@/assets/video/leftBtn9.png', import.meta.url).href,
|
|
|
|
|
|
hover: new URL('@/assets/video/leftBtn9-9.png', import.meta.url).href
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
const schedulePlan = [
|
|
|
|
|
|
{ key: 'follow', duration: 40 * 60 * 1000 },
|
|
|
|
|
|
{ key: 'like', duration: 20 * 60 * 1000 },
|
|
|
|
|
|
]
|
|
|
|
|
|
// 调度状态(持久化一下,避免刷新丢失)
|
|
|
|
|
|
let scheduleState = (() => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const saved = JSON.parse(localStorage.getItem('SCHEDULE_STATE') || '{}')
|
|
|
|
|
|
if (saved && typeof saved.index === 'number' && typeof saved.startTime === 'number') {
|
|
|
|
|
|
return saved
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch { }
|
|
|
|
|
|
return { index: 0, startTime: Date.now() }
|
|
|
|
|
|
})()
|
|
|
|
|
|
|
|
|
|
|
|
let scheduleTimer = null // 轮询定时器句柄
|
|
|
|
|
|
const scheduleTickMs = 30_000 // 每 30s 检查一次是否到切换点
|
2025-09-08 23:12:00 +08:00
|
|
|
|
let scheduleEnabled = ref(false) // 需要时可手动关闭调度(例如“全部停止”)
|
2025-09-08 20:53:24 +08:00
|
|
|
|
// 弹窗
|
|
|
|
|
|
const showScheduleDlg = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 两个时间片(默认 A=follow 40min,B=like 20min)
|
|
|
|
|
|
const schedAKey = ref('follow')
|
|
|
|
|
|
const schedAMin = ref(40)
|
|
|
|
|
|
const schedBKey = ref('like')
|
|
|
|
|
|
const schedBMin = ref(20)
|
|
|
|
|
|
// 打开弹窗:把当前 schedulePlan 映射到 UI
|
|
|
|
|
|
function openScheduleDialog() {
|
|
|
|
|
|
// 把当前计划读出来(只支持两个片段的简易版)
|
|
|
|
|
|
if (Array.isArray(schedulePlan) && schedulePlan.length >= 2) {
|
|
|
|
|
|
const a = schedulePlan[0], b = schedulePlan[1]
|
|
|
|
|
|
schedAKey.value = a?.key || 'follow'
|
|
|
|
|
|
schedAMin.value = Math.max(1, Math.round((a?.duration || 40 * 60_000) / 60_000))
|
|
|
|
|
|
schedBKey.value = b?.key || 'like'
|
|
|
|
|
|
schedBMin.value = Math.max(1, Math.round((b?.duration || 20 * 60_000) / 60_000))
|
|
|
|
|
|
}
|
|
|
|
|
|
showScheduleDlg.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存:校验=60 分钟 → 更新 schedulePlan → 持久化 → 重启轮询
|
|
|
|
|
|
function saveSchedule() {
|
2025-09-08 23:12:00 +08:00
|
|
|
|
|
2025-09-08 20:53:24 +08:00
|
|
|
|
const total = schedAMin.value + schedBMin.value
|
|
|
|
|
|
if (total !== 60) {
|
|
|
|
|
|
ElMessage.error('两个片段相加必须等于 60 分钟')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
schedulePlan.splice(0, schedulePlan.length,
|
|
|
|
|
|
{ key: schedAKey.value, duration: schedAMin.value * 60_000 },
|
|
|
|
|
|
{ key: schedBKey.value, duration: schedBMin.value * 60_000 },
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 存 localStorage
|
|
|
|
|
|
localStorage.setItem('SCHEDULE_PLAN', JSON.stringify(schedulePlan))
|
|
|
|
|
|
localStorage.setItem('SCHEDULE_ENABLED', JSON.stringify(!!scheduleEnabled.value))
|
|
|
|
|
|
|
|
|
|
|
|
// 重置时间片起点并立即生效
|
|
|
|
|
|
scheduleState.index = 0
|
|
|
|
|
|
scheduleState.startTime = Date.now()
|
|
|
|
|
|
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
|
|
|
|
|
|
|
|
|
|
|
|
// 若启用则重启轮询
|
2025-09-08 23:12:00 +08:00
|
|
|
|
startScheduleLoop()
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
|
|
|
|
|
showScheduleDlg.value = false
|
|
|
|
|
|
ElMessage.success('已保存定时调度')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const selectedDevice = ref(null)
|
|
|
|
|
|
|
|
|
|
|
|
// —— 显示尺寸固定为 320x720;未选中缩略为 THUMB_SCALE 倍 ——
|
2025-09-08 23:12:00 +08:00
|
|
|
|
// 尺寸与排布
|
2025-09-08 20:53:24 +08:00
|
|
|
|
const BASE_W = 320
|
|
|
|
|
|
const BASE_H = 720
|
|
|
|
|
|
const THUMB_SCALE = 0.6
|
2025-09-08 23:12:00 +08:00
|
|
|
|
const PER_ROW = 3
|
|
|
|
|
|
|
|
|
|
|
|
// 底行上移的位移量:720*(1-0.6)=288
|
|
|
|
|
|
const BOTTOM_SHIFT = Math.round(BASE_H * (1 - THUMB_SCALE)) // 288
|
2025-09-08 20:53:24 +08:00
|
|
|
|
|
2025-09-08 23:12:00 +08:00
|
|
|
|
// 是否至少有两行
|
|
|
|
|
|
const hasTwoRows = computed(() => deviceInformation.value.length > PER_ROW)
|
|
|
|
|
|
|
|
|
|
|
|
// 真正的“底行”判定:必须有两行以上才成立
|
|
|
|
|
|
const isBottomRow = (index) => {
|
|
|
|
|
|
if (!hasTwoRows.value) return false
|
|
|
|
|
|
const lastRow = Math.floor((deviceInformation.value.length - 1) / PER_ROW)
|
|
|
|
|
|
return Math.floor(index / PER_ROW) === lastRow
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 统一给 .video-canvas 返回 transform(缩略/放大/底行上移)
|
|
|
|
|
|
function getCanvasStyle(index) {
|
|
|
|
|
|
const isSelected = selectedDevice.value === index
|
|
|
|
|
|
if (!isSelected) {
|
|
|
|
|
|
return { transform: `scale(${THUMB_SCALE})` }
|
|
|
|
|
|
}
|
|
|
|
|
|
// 选中:默认正常放大;若在底行且至少两行 -> 先上移再放大
|
|
|
|
|
|
return isBottomRow(index)
|
|
|
|
|
|
? { transform: `translateY(-${BOTTOM_SHIFT}px) scale(1)` }
|
|
|
|
|
|
: { transform: 'scale(1)' }
|
|
|
|
|
|
}
|
2025-09-08 20:53:24 +08:00
|
|
|
|
// 当前选中的卡片:选中=1倍,未选中=缩略比例
|
|
|
|
|
|
const imgWH = (index) => {
|
|
|
|
|
|
const scale = (selectedDevice.value === index) ? 1 : THUMB_SCALE
|
|
|
|
|
|
return {
|
|
|
|
|
|
width: `${BASE_W * scale}px`,
|
|
|
|
|
|
height: `${BASE_H * scale}px`,
|
|
|
|
|
|
transition: 'all 0.3s ease',
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算某索引当前展示宽高(用于坐标换算)
|
|
|
|
|
|
const displaySize = (index) => {
|
|
|
|
|
|
const scale = (selectedDevice.value === index) ? 1 : THUMB_SCALE
|
|
|
|
|
|
return { w: BASE_W * scale, h: BASE_H * scale }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从 Canvas offset 坐标 → 真实手机分辨率坐标
|
|
|
|
|
|
const mapToDeviceXY = (index, offsetX, offsetY) => {
|
|
|
|
|
|
const dev = deviceInformation.value[index] || {}
|
|
|
|
|
|
const realW = Number(dev.width) || BASE_W // 后端返回的真实分辨率
|
|
|
|
|
|
const realH = Number(dev.height) || BASE_H
|
|
|
|
|
|
const rotation = Number(dev.rotation || 0) // 若后端有提供旋转角,可用 0/90/180/270
|
|
|
|
|
|
|
|
|
|
|
|
const { w: dispW, h: dispH } = displaySize(index)
|
|
|
|
|
|
|
|
|
|
|
|
// 归一化到 0~1
|
|
|
|
|
|
let nx = Math.min(Math.max(offsetX / dispW, 0), 1)
|
|
|
|
|
|
let ny = Math.min(Math.max(offsetY / dispH, 0), 1)
|
|
|
|
|
|
|
|
|
|
|
|
// 处理旋转(如果你的服务器坐标基于设备原生朝向)
|
|
|
|
|
|
// 0: 直接映射;90: 顺时针;180、270 同理
|
|
|
|
|
|
let x, y
|
|
|
|
|
|
switch (rotation % 360) {
|
|
|
|
|
|
case 90:
|
|
|
|
|
|
case -270:
|
|
|
|
|
|
x = Math.round(ny * realW)
|
|
|
|
|
|
y = Math.round((1 - nx) * realH)
|
|
|
|
|
|
break
|
|
|
|
|
|
case 180:
|
|
|
|
|
|
case -180:
|
|
|
|
|
|
x = Math.round((1 - nx) * realW)
|
|
|
|
|
|
y = Math.round((1 - ny) * realH)
|
|
|
|
|
|
break
|
|
|
|
|
|
case 270:
|
|
|
|
|
|
case -90:
|
|
|
|
|
|
x = Math.round((1 - ny) * realW)
|
|
|
|
|
|
y = Math.round(nx * realH)
|
|
|
|
|
|
break
|
|
|
|
|
|
default: // 0°
|
|
|
|
|
|
x = Math.round(nx * realW)
|
|
|
|
|
|
y = Math.round(ny * realH)
|
|
|
|
|
|
}
|
|
|
|
|
|
return { x, y }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 选中:恢复到 320x720 并显示盖层
|
|
|
|
|
|
const selectDevice = (index) => {
|
|
|
|
|
|
selectedDevice.value = index
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ——— 鼠标交互:按下/移动/抬起 ———
|
|
|
|
|
|
const dragState = ref({}) // 以 index 作为 key 保存 {ox, oy, t}
|
|
|
|
|
|
|
|
|
|
|
|
const onCanvasDown = (udid, e, index) => {
|
|
|
|
|
|
// 记录起点(Canvas 内的 offset)
|
|
|
|
|
|
dragState.value[index] = { ox: e.offsetX, oy: e.offsetY, t: Date.now(), udid }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const onCanvasMove = (udid, e, index) => {
|
|
|
|
|
|
// 如需在 overlay 上画指示、十字线等,可在这里使用 e.offsetX/e.offsetY
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const onCanvasUp = async (udid, e, index) => {
|
|
|
|
|
|
const st = dragState.value[index]
|
|
|
|
|
|
if (!st) return
|
|
|
|
|
|
|
|
|
|
|
|
const { ox, oy, t } = st
|
|
|
|
|
|
const dx = e.offsetX - ox
|
|
|
|
|
|
const dy = e.offsetY - oy
|
|
|
|
|
|
const elapsed = Date.now() - t
|
|
|
|
|
|
delete dragState.value[index]
|
|
|
|
|
|
|
|
|
|
|
|
// 映射到真实分辨率
|
|
|
|
|
|
const p0 = mapToDeviceXY(index, ox, oy)
|
|
|
|
|
|
const p1 = mapToDeviceXY(index, e.offsetX, e.offsetY)
|
|
|
|
|
|
console.log(" x, y", p0, p1)
|
|
|
|
|
|
// 判断是“点按”还是“滑动”
|
|
|
|
|
|
const MOVE_THR = 5 // 像素阈值(可按需调整)
|
|
|
|
|
|
const isTap = Math.hypot(dx, dy) < MOVE_THR && elapsed < 500
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (isTap) {
|
|
|
|
|
|
await tapAction({ udid, x: p1.x, y: p1.y })
|
|
|
|
|
|
// console.log('tap', p1)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 只需要方向:1上/2左/3下/4右
|
|
|
|
|
|
const rotation = Number((deviceInformation.value[index] || {}).rotation || 0)
|
|
|
|
|
|
|
|
|
|
|
|
const code = getSwipeCodeWithRotation(dx, dy, rotation) // 必定得到1~4
|
|
|
|
|
|
console.log("code", code)
|
|
|
|
|
|
await swipeAction({ udid, direction: code })
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 方向码:1=上, 2=左, 3=下, 4=右(不返回0,始终给出一个方向) */
|
|
|
|
|
|
function getSwipeCode(dx, dy) {
|
|
|
|
|
|
// 哪个轴位移更大就取哪个轴;边界≈45°
|
|
|
|
|
|
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
|
|
|
|
return dx < 0 ? 2 : 4 // 左/右
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return dy < 0 ? 1 : 3 // 上/下(DOM坐标里向上是负)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 带设备旋转(0/90/180/270):先把画布向量(dx,dy)旋回设备坐标系再判方向 */
|
|
|
|
|
|
function getSwipeCodeWithRotation(dx, dy, rotation = 0) {
|
|
|
|
|
|
let dxD = dx, dyD = dy
|
|
|
|
|
|
switch ((rotation % 360 + 360) % 360) {
|
|
|
|
|
|
case 90: dxD = dy; dyD = -dx; break
|
|
|
|
|
|
case 180: dxD = -dx; dyD = -dy; break
|
|
|
|
|
|
case 270: dxD = -dy; dyD = dx; break
|
|
|
|
|
|
default: break
|
|
|
|
|
|
}
|
|
|
|
|
|
return getSwipeCode(dxD, dyD)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function openTk() {
|
|
|
|
|
|
if (!deviceInformation.value?.length) {
|
|
|
|
|
|
ElMessage.warning('暂无在线设备')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const loading = ElLoading.service({ text: '正在打开 TikTok …', background: 'rgba(0,0,0,.35)' })
|
|
|
|
|
|
const results = []
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 为了稳妥,逐台串行(如果你希望更快,可改 Promise.all 并注意并发数)
|
|
|
|
|
|
for (const dev of deviceInformation.value) {
|
|
|
|
|
|
const udid = dev.deviceId
|
|
|
|
|
|
try {
|
|
|
|
|
|
const apps = await deviceAppList({ udid }) // 期望返回示例中的数组
|
|
|
|
|
|
const bundleId = pickTikTokBundleId(apps)
|
|
|
|
|
|
|
|
|
|
|
|
if (!bundleId) {
|
|
|
|
|
|
results.push({ udid, ok: false, msg: '未找到 TikTok' })
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await launchApp({ udid, bundleId })
|
|
|
|
|
|
results.push({ udid, ok: true, msg: `已启动 TikTok (${bundleId})` })
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('openTk error', udid, e)
|
|
|
|
|
|
results.push({ udid, ok: false, msg: '请求失败' })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.close()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 汇总提示(成功/失败各一条)
|
|
|
|
|
|
const okCount = results.filter(r => r.ok).length
|
|
|
|
|
|
const fail = results.filter(r => !r.ok)
|
|
|
|
|
|
if (okCount) ElMessage.success(`已在 ${okCount} 台设备启动 TikTok`)
|
|
|
|
|
|
if (fail.length) {
|
|
|
|
|
|
const udids = fail.map(f => f.udid).join(', ')
|
|
|
|
|
|
ElMessage.error(`以下设备未能启动:${udids}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getMesList(deviceId) {
|
|
|
|
|
|
getChatTextInfo({ udid: deviceId }).then((res) => {
|
|
|
|
|
|
if (res) {
|
|
|
|
|
|
chatList.value = res
|
|
|
|
|
|
console.log(chatList.value)
|
|
|
|
|
|
getTranslation(chatList.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stopAll() {
|
|
|
|
|
|
// if (!runType.value) return
|
|
|
|
|
|
deviceInformation.value.forEach((item) => {
|
|
|
|
|
|
stopScript({ udid: item.deviceId }).then((res) => {
|
|
|
|
|
|
console.log(`停止成功:${item.deviceId}`, res, printCurrentTime())
|
|
|
|
|
|
ElMessage.success(`停止成功:${item.deviceId}`)
|
|
|
|
|
|
}).catch((res) => {
|
|
|
|
|
|
console.log(`停止失败`, printCurrentTime())
|
|
|
|
|
|
ElMessage.error(`脚本已停止`)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
scheduleEnabled.value = false
|
|
|
|
|
|
runType.value = ''
|
|
|
|
|
|
batchMode.value = 'init';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//确认多行文本框内容
|
|
|
|
|
|
function onDialogConfirm(result, type, index, isMon) {
|
|
|
|
|
|
// console.log(type, result, isMon);
|
|
|
|
|
|
if (type == '主播ID') {
|
|
|
|
|
|
hostList = (result || []).map(id => ({ id, country: '' }))
|
|
|
|
|
|
|
|
|
|
|
|
dialogTitle.value = '私信';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
showDialog.value = true;
|
|
|
|
|
|
}, 600)
|
|
|
|
|
|
} else if (type == '私信') {
|
|
|
|
|
|
runType.value = 'follow'
|
|
|
|
|
|
setContentpriList(result)
|
|
|
|
|
|
passAnchorData(
|
|
|
|
|
|
{
|
|
|
|
|
|
deviceList: deviceInformation.value.map(item => item.deviceId),
|
|
|
|
|
|
anchorList: hostList.map(item => ({
|
|
|
|
|
|
anchorId: item.id,
|
|
|
|
|
|
country: item.country
|
|
|
|
|
|
})),
|
|
|
|
|
|
prologueList: result,
|
|
|
|
|
|
needReply: isMon
|
|
|
|
|
|
}
|
|
|
|
|
|
).then((res) => {
|
|
|
|
|
|
hostList = []
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
const loading = ElLoading.service({
|
|
|
|
|
|
lock: true,
|
|
|
|
|
|
text: 'Loading',
|
|
|
|
|
|
background: 'rgba(0, 0, 0, 0.7)',
|
|
|
|
|
|
});
|
|
|
|
|
|
getDeviceListFun()
|
|
|
|
|
|
window.electronAPI.startMq(userdata.tenantId, userdata.id)
|
|
|
|
|
|
// 初始化时获取设备列表
|
|
|
|
|
|
getListtimer = setInterval(() => {
|
|
|
|
|
|
loading.close();
|
|
|
|
|
|
getDeviceListFun()
|
|
|
|
|
|
}, 3000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 批次缓冲(仅用于当前“波”)
|
|
|
|
|
|
let batch = []; // [{ country, text }]
|
|
|
|
|
|
let flushTimer = null;
|
|
|
|
|
|
|
|
|
|
|
|
function scheduleFlush(handler, delay = 400) {
|
|
|
|
|
|
if (flushTimer) clearTimeout(flushTimer);
|
|
|
|
|
|
flushTimer = setTimeout(() => {
|
|
|
|
|
|
if (batch.length) {
|
|
|
|
|
|
const items = batch.slice(); // 拷贝一份
|
|
|
|
|
|
batch.length = 0; // 清空批次
|
|
|
|
|
|
try {
|
|
|
|
|
|
handler(items);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('[SSE flush error]', e);
|
|
|
|
|
|
// 出错不回灌,避免重复提交;必要时可根据需要 batch.push(...items)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, delay);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// —— SSE 接收 ——
|
|
|
|
|
|
const es = connectSSE('http://localhost:3312/events', (data) => {
|
|
|
|
|
|
console.log('来自服务端:', data);
|
|
|
|
|
|
|
|
|
|
|
|
if (data === 'start') {
|
|
|
|
|
|
// 新一波开始:根据当前状态决定“本波 flush 用谁”
|
|
|
|
|
|
if (!isMsgPop.value) {
|
|
|
|
|
|
// 还没开始过 -> 首次弹框,确认后使用处理本波
|
|
|
|
|
|
isMsgPop.value = true;
|
|
|
|
|
|
batchMode.value = 'init';
|
|
|
|
|
|
ElMessageBox.confirm(
|
|
|
|
|
|
'检测到YOLO助手正在爬取主播,是否进行操作?',
|
|
|
|
|
|
'消息提醒',
|
|
|
|
|
|
{ confirmButtonText: '开始', cancelButtonText: '取消', type: 'success' }
|
|
|
|
|
|
)
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
ElMessage({ type: 'success', message: '任务开启成功' });
|
|
|
|
|
|
// runType.value = 'follow';
|
|
|
|
|
|
batchMode.value = 'follow';
|
|
|
|
|
|
|
|
|
|
|
|
// 不在这里立刻提交;让后续主播数据先进 batch,再由防抖统一 flush
|
|
|
|
|
|
// 不直接发;把这“一波”的主播先塞进 hostList,然后弹出“私信”输入框
|
|
|
|
|
|
scheduleFlush((items) => {
|
|
|
|
|
|
hostList = (items || []).map(h => ({ id: h.text, country: h.country || '' }))
|
|
|
|
|
|
showScheduleDlg.value = true
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
// 取消:清理状态,丢弃批次
|
|
|
|
|
|
batch.length = 0;
|
|
|
|
|
|
isMsgPop.value = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 已经在运行 follow:本波用 addTempAnchorData 追加
|
|
|
|
|
|
batchMode.value = 'follow';
|
|
|
|
|
|
// 立刻安排一次“尾随防抖”flush,等本波数据齐了再送
|
|
|
|
|
|
scheduleFlush((items) => {
|
|
|
|
|
|
addTempAnchorData(items.map(h => ({ anchorId: h.text, country: h.country || '' })));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 非 start:本波主播数据进入批次
|
|
|
|
|
|
const country = data && data.country != null ? data.country : '';
|
|
|
|
|
|
const text = data && (data.hostsId != null ? data.hostsId : data.text);
|
|
|
|
|
|
if (text == null) {
|
|
|
|
|
|
// 数据格式不对,丢弃或打印
|
|
|
|
|
|
console.warn('[SSE] 非法数据,缺少 hostsId/text:', data);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
batch.push({ country, text });
|
|
|
|
|
|
|
|
|
|
|
|
// 根据当前模式,刷新防抖(让“最后一条到来后”延迟几百毫秒再统一提交)
|
|
|
|
|
|
if (batchMode.value === 'init') {
|
|
|
|
|
|
// 首次确认前:等用户点“开始”后由上面的 scheduleFlush 执行
|
|
|
|
|
|
scheduleFlush((items) => {
|
|
|
|
|
|
// 安全起见:只有在 runType 已经 follow 时才真正提交
|
|
|
|
|
|
if (runType.value === 'follow') {
|
|
|
|
|
|
passAnchorData({
|
|
|
|
|
|
deviceList: deviceInformation.value.map(item => item.deviceId),
|
|
|
|
|
|
anchorList: items.map(h => ({ anchorId: h.text, country: h.country || '' })),
|
|
|
|
|
|
needReply: false
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 还没开始就来了数据:把它们留回批次,等待上面 then 里的 scheduleFlush 再处理
|
|
|
|
|
|
batch.push(...items);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 已在关注:走追加逻辑
|
|
|
|
|
|
scheduleFlush((items) => {
|
|
|
|
|
|
addTempAnchorData(items.map(h => ({ anchorId: h.text, country: h.country || '' })));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
clearInterval(getListtimer)
|
|
|
|
|
|
getListtimer = null
|
|
|
|
|
|
})
|
|
|
|
|
|
const getDeviceListFun = () => {
|
|
|
|
|
|
getDeviceList().then((res) => {
|
2025-09-08 23:12:00 +08:00
|
|
|
|
console.log('返回', res.length)
|
2025-09-08 20:53:24 +08:00
|
|
|
|
if (res && res.length > 0 && deviceInformation.value.length !== res.length) {
|
|
|
|
|
|
console.log("设备变更")
|
|
|
|
|
|
deviceInformation.value = res
|
|
|
|
|
|
reloadImg()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (res.length == 0) {
|
|
|
|
|
|
deviceInformation.value = []
|
|
|
|
|
|
reloadImg()
|
|
|
|
|
|
}
|
2025-09-08 23:12:00 +08:00
|
|
|
|
// deviceInformation.value = ['', '', '', '', '', '',]
|
2025-09-08 20:53:24 +08:00
|
|
|
|
}).catch((err) => {
|
|
|
|
|
|
ElMessage.error(`IOSAI服务错误`)
|
|
|
|
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function uploadLogFile() {
|
|
|
|
|
|
let loading = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 先弹出确认框
|
|
|
|
|
|
await ElMessageBox.confirm(
|
|
|
|
|
|
'确定要上传日志文件吗?',
|
|
|
|
|
|
'提示',
|
|
|
|
|
|
{
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
// 如果点了确定,就会走到这里
|
|
|
|
|
|
loading = ElLoading.service({
|
|
|
|
|
|
lock: true,
|
|
|
|
|
|
text: '上传中...',
|
|
|
|
|
|
background: 'rgba(0, 0, 0, 0.7)',
|
|
|
|
|
|
})
|
|
|
|
|
|
const res = await setLoginInfo({
|
|
|
|
|
|
"tenantId": userdata.tenantId,
|
|
|
|
|
|
"userId": userdata.id,
|
|
|
|
|
|
"token": userdata.tokenValue
|
|
|
|
|
|
})
|
|
|
|
|
|
loading.close()
|
|
|
|
|
|
console.log("上传文件返回", res)
|
|
|
|
|
|
if (res) {
|
|
|
|
|
|
console.log("✅ 上传成功:", res)
|
|
|
|
|
|
ElMessage.success('✅ 上传成功')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error("❌ 上传失败:", res.msg)
|
|
|
|
|
|
ElMessage.error('❌ 上传失败: ' + (res.msg || '未知错误'))
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
loading.close()
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果用户点了取消,会进入这里
|
|
|
|
|
|
if (err === 'cancel' || err === 'close') {
|
|
|
|
|
|
ElMessage.info('已取消上传')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error("❌ 上传异常:", err)
|
|
|
|
|
|
ElMessage.error('❌ 上传异常: ' + err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
function runTask(key) {
|
|
|
|
|
|
console.log('[schedule] 切换到任务:', key, printCurrentTime())
|
|
|
|
|
|
|
|
|
|
|
|
forceActivate(key, () => {
|
|
|
|
|
|
if (key === 'follow') {
|
2025-09-08 23:12:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (scheduleEnabled.value) {
|
|
|
|
|
|
stopAll()
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
runType.value = 'follow'
|
|
|
|
|
|
|
|
|
|
|
|
passAnchorData(
|
|
|
|
|
|
{
|
|
|
|
|
|
deviceList: deviceInformation.value.map(item => item.deviceId),
|
|
|
|
|
|
anchorList: [],
|
|
|
|
|
|
prologueList: getContentpriList(),
|
|
|
|
|
|
needReply: false
|
|
|
|
|
|
}
|
|
|
|
|
|
).then((res) => {
|
|
|
|
|
|
hostList = []
|
|
|
|
|
|
})
|
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
scheduleEnabled.value = true
|
2025-09-08 20:53:24 +08:00
|
|
|
|
if (hostList.length <= 0) {
|
|
|
|
|
|
dialogTitle.value = '主播ID';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dialogTitle.value = '私信';
|
|
|
|
|
|
}
|
|
|
|
|
|
showDialog.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
} else if (key === 'like') {
|
|
|
|
|
|
|
|
|
|
|
|
stopAll()
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
2025-09-08 23:12:00 +08:00
|
|
|
|
scheduleEnabled.value = true
|
2025-09-08 20:53:24 +08:00
|
|
|
|
runType.value = 'like'
|
|
|
|
|
|
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
|
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
|
|
|
|
|
|
} else if (key === 'brushLive') {
|
|
|
|
|
|
stopAll()
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
2025-09-08 23:12:00 +08:00
|
|
|
|
scheduleEnabled.value = true
|
2025-09-08 20:53:24 +08:00
|
|
|
|
runType.value = 'brushLive'
|
|
|
|
|
|
deviceInformation.value.forEach((item) => watchLiveForGrowth({ udid: item.deviceId }))
|
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
runType.value = 'brushLive'
|
|
|
|
|
|
} else if (key === 'listen') {
|
|
|
|
|
|
stopAll()
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
runType.value = 'listen'
|
2025-09-08 23:12:00 +08:00
|
|
|
|
scheduleEnabled.value = true
|
2025-09-08 20:53:24 +08:00
|
|
|
|
isMonitorOn.value = true
|
|
|
|
|
|
deviceInformation.value.forEach((item) => monitorMessages({ udid: item.deviceId }))
|
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startScheduleLoop() {
|
|
|
|
|
|
// 先按照当前 index 跑一次,保证“即刻对齐”
|
|
|
|
|
|
runTask(schedulePlan[scheduleState.index].key)
|
|
|
|
|
|
|
|
|
|
|
|
// 清理旧轮询,防止重复
|
|
|
|
|
|
if (scheduleTimer) clearInterval(scheduleTimer)
|
|
|
|
|
|
|
|
|
|
|
|
scheduleTimer = setInterval(() => {
|
|
|
|
|
|
if (!scheduleEnabled.value) return
|
|
|
|
|
|
|
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
const cur = schedulePlan[scheduleState.index]
|
|
|
|
|
|
const elapsed = now - scheduleState.startTime
|
|
|
|
|
|
|
|
|
|
|
|
if (elapsed >= cur.duration) {
|
|
|
|
|
|
// 进入下一个时间片
|
|
|
|
|
|
scheduleState.index = (scheduleState.index + 1) % schedulePlan.length
|
|
|
|
|
|
scheduleState.startTime = now
|
|
|
|
|
|
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
|
|
|
|
|
|
|
|
|
|
|
|
runTask(schedulePlan[scheduleState.index].key)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, scheduleTickMs)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function forceActivate(key, runner) {
|
|
|
|
|
|
// 跳过互斥逻辑,直接切换
|
|
|
|
|
|
runType.value = key;
|
|
|
|
|
|
if (typeof runner === 'function') runner();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function mqSend() {
|
|
|
|
|
|
window.electronAPI.mqSend("start")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getTranslation(list) {
|
|
|
|
|
|
list.forEach((item, index) => {
|
|
|
|
|
|
translationToChinese({ msg: item.text }).then(res => {
|
|
|
|
|
|
console.log(res);
|
|
|
|
|
|
chatList.value[index].text = res
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onHostSaved(list) {
|
|
|
|
|
|
console.log('保存后的主播id:', list)
|
|
|
|
|
|
}
|
|
|
|
|
|
// //sse接收爬虫发送的消息
|
|
|
|
|
|
// const es = connectSSE(`http://localhost:3311/events`, (data) => {
|
|
|
|
|
|
// // connectSSE(`http://192.168.1.155:19665/api/sse/connect/${userdata.tenantId}/${userdata.id}`, (data) => {
|
|
|
|
|
|
// // 处理服务端推送的数据
|
|
|
|
|
|
// console.log('来自服务端:', data)
|
|
|
|
|
|
|
|
|
|
|
|
// //接收到start
|
|
|
|
|
|
// if (data === 'start') {
|
|
|
|
|
|
|
|
|
|
|
|
// } else {
|
|
|
|
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// })
|
|
|
|
|
|
|
|
|
|
|
|
//当前时间获取
|
|
|
|
|
|
function printCurrentTime() {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
return now.toLocaleString()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
|
|
@import '../static/css/video.less';
|
|
|
|
|
|
</style>
|