diff --git a/src/api/ios.js b/src/api/ios.js index 2222a93..c7bad98 100644 --- a/src/api/ios.js +++ b/src/api/ios.js @@ -118,3 +118,7 @@ export function restartTikTok(data) { return postAxios({ url: 'restartTikTok', data }) } +//获取设备网络状态 +export function getDeviceNetStatus(data) { + return postAxios({ url: 'getDeviceNetStatus', data }) +} \ No newline at end of file diff --git a/src/static/css/video.less b/src/static/css/video.less index b4f07bc..d9312b0 100644 --- a/src/static/css/video.less +++ b/src/static/css/video.less @@ -481,4 +481,66 @@ video { .video-canvas .overlay { z-index: 2; +} + + +/* 容器要能放伪元素 */ +.video-canvas.net-bad { + position: relative; + outline: 2px solid rgba(239, 68, 68, 0.6); + /* 常亮的细红框作底线 */ + border-radius: 16px; +} + +/* 闪烁的红色光圈边框(不占据布局、不挡点击) */ +.video-canvas.net-bad::after { + content: ''; + position: absolute; + inset: -4px; + /* 往外扩一点,避免吃掉内容 */ + border-radius: 20px; + pointer-events: none; + z-index: 3; + /* 盖在内部 img/canvas 之上 */ + box-shadow: + 0 0 0 3px rgba(239, 68, 68, 0.95), + /* 实心外圈 */ + 0 0 14px rgba(239, 68, 68, 0.85), + /* 强发光 */ + 0 0 36px rgba(239, 68, 68, 0.65); + /* 远发光 */ + opacity: 0.2; + animation: alertPulse 0.9s infinite cubic-bezier(.65, .05, .36, 1); + will-change: opacity, box-shadow, transform; +} + +/* 强烈的“脉冲闪烁”效果 */ +@keyframes alertPulse { + 0% { + opacity: 0.2; + box-shadow: + 0 0 0 2px rgba(239, 68, 68, 0.6), + 0 0 10px rgba(239, 68, 68, 0.55), + 0 0 24px rgba(239, 68, 68, 0.45); + transform: scale(1); + } + + 40% { + opacity: 1; + box-shadow: + 0 0 0 4px rgba(239, 68, 68, 1), + 0 0 18px rgba(239, 68, 68, 0.95), + 0 0 42px rgba(239, 68, 68, 0.85); + transform: scale(1.01); + /* 轻微放大,增强“警报感” */ + } + + 100% { + opacity: 0.2; + box-shadow: + 0 0 0 2px rgba(239, 68, 68, 0.6), + 0 0 10px rgba(239, 68, 68, 0.55), + 0 0 24px rgba(239, 68, 68, 0.45); + transform: scale(1); + } } \ No newline at end of file diff --git a/src/views/VideoStream.vue b/src/views/VideoStream.vue index d0ab3aa..482ca9a 100644 --- a/src/views/VideoStream.vue +++ b/src/views/VideoStream.vue @@ -4,15 +4,6 @@
人设编辑 - - -
@@ -35,8 +26,10 @@
-
+
@@ -171,7 +164,8 @@ import { changeAccount, stopAllTask, anchorList, - restartTikTok + restartTikTok, + getDeviceNetStatus } from '@/api/ios'; import ding from '@/assets/mes.wav' import { set } from "lodash"; @@ -229,6 +223,23 @@ let deviceInformation = ref([]) //停止中 let stopLoading = null +// 每台设备的网络状态:true=正常,false=异常 +const netStatus = reactive({}) // { [deviceId]: boolean } + +// —— 网络波动联动(每设备暂停/恢复)—— +const offlineFlags = reactive({}) // { [deviceId]: true/false } 是否因断网而暂停过 +const lastNet = reactive({}) // { [deviceId]: true/false } 上一次看到的网络状态 +const resumeTimers = new Map() // deviceId -> setTimeout id +const NET_RESUME_STABLE_MS = 6000 // 断网恢复后,等待网络稳定的毫秒数 + +function clearResumeTimer(id) { + const t = resumeTimers.get(id) + if (t) { + clearTimeout(t) + resumeTimers.delete(id) + } +} + // 当前是否被其它模式占用(四个互斥按钮专用) const isLocked = (type) => !!runType.value && runType.value !== type @@ -777,11 +788,6 @@ const onCanvasUp = async (udid, e, index) => { if (isTap) { await tapAction({ udid, x: endDevXY.x, y: endDevXY.y }) } else { - // //通过方向判断code 1234 - // const rotation = Number((deviceInformation.value[index] || {}).rotation || 0) - // const code = getSwipeCodeWithRotation(dx, dy, rotation) - // await swipeAction({ udid, direction: code }) - //通过自定义滑动坐标和时间传参 await swipeAction({ udid, sx: startDevXY.x, sy: startDevXY.y, ex: endDevXY.x, ey: endDevXY.y, duration: elapsed / 1000 }) } @@ -922,60 +928,9 @@ function onDialogConfirm(result, type, index, data) { comonList = result setContentList(result) common.value = data.common - // dialogTitle.value = '私信'; - // setTimeout(() => { - // showDialog.value = true; - // initialTextStr.value = getContentpriListMultiline(); - // }, 500) transDlgType.value = '私信' showtransDlg.value = true - } else if (type == '私信') { - // runType.value = 'follow' - // setContentpriList(result) - - // if (isAlliance.value) { - // followAndGreetUnion( - // { - // deviceList: deviceInformation.value.map(item => item.deviceId), - // anchorList: hostList.map(item => ({ - // anchorId: item.id, - // country: item.country, - // invitationType: item.invitationType, - // state: stateByInvType(item.invitationType), - // })), - // prologueList: result, - // needReply: data.auto, - // needTranslate: data.needTranslate, - // } - // ).then((res) => { - // ElMessage({ type: 'success', message: '任务开启成功' }); - - // hostList = [] - // }) - // } else { - // passAnchorData( - // { - // deviceList: deviceInformation.value.map(item => item.deviceId), - // anchorList: hostList.map(item => ({ - // anchorId: item.id, - // country: item.country, - // invitationType: item.invitationType, - // state: stateByInvType(item.invitationType), - // })), - // prologueList: result, - // comment: comonList, - // needReply: data.auto, - // needTranslate: data.needTranslate, - // isComment: data.common - // } - // ).then((res) => { - // ElMessage({ type: 'success', message: '任务开启成功' }); - - // hostList = [] - // }) - - // } } else if (type == '视频评论') { runType.value = 'like' deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId, comment: result, isComment: data.common })) @@ -1016,11 +971,10 @@ onMounted(async () => { window.electronAPI.startMq(userdata.tenantId, userdata.id) // 初始化时获取设备列表 - //每3秒获取一次设备列表 消息列表 和 查询主播列表是否还有主播 getListtimer = setInterval(async () => { - getDeviceListFun() - selectLastFun() + getDeviceListFun() //获取设备列表 + selectLastFun() //获取手机网络状态 const hostsList = await getStoredHostList() // console.log(hostsList.length) //当私信主播时,主播列表没有数据了,提示列表空了 并且关闭私信 @@ -1042,8 +996,10 @@ onMounted(async () => { setInterval(async () => { await checkVPN() + await refreshNetStatus() }, 1000 * 20) + if (!await isAiConfig()) { showMyInfo.value = true } @@ -1114,20 +1070,14 @@ onUnmounted(() => { let isStartLac = false const getDeviceListFun = () => { getDeviceList().then((res) => { - // console.log('返回', res.length) if (res && res.length > 0 && deviceInformation.value.length !== res.length) { console.log("设备变更") deviceInformation.value = res - // refreshAllStopImgs() - // reloadImg() } if (res.length == 0) { deviceInformation.value = [] - // refreshAllStopImgs() - // reloadImg() } - // deviceInformation.value = ['', '', '', '', '', '',] }).catch((err) => { if (isStartLac) { ElMessage.error(`IOSAI服务错误`) @@ -1210,37 +1160,68 @@ function runTask(key, deviceId, type) { console.log("进入follow", scheduleEnabled.value) if (scheduleEnabled.value) { if (!deviceId) { - await stopAll(100) + await stopAll(5000) } else { - passAnchorData( - { - deviceList: [deviceId], - anchorList: [], - prologueList: getContentpriList(), - comment: comonList, - needReply: auto.value - } - ).then((res) => { - hostList = [] - }) + if (isAlliance.value) { + followAndGreetUnion( + { + deviceList: [deviceId], + anchorList: [], + prologueList: getContentpriList(), + needReply: auto.value, + } + ).then((res) => { + hostList = [] + }) + } else { + passAnchorData( + { + deviceList: [deviceId], + anchorList: [], + prologueList: getContentpriList(), + comment: comonList, + needReply: auto.value, + isComment: common.value //是否评论 + + } + ).then((res) => { + hostList = [] + }) + } + return } //第一个小时结束后,第二轮开始的时候,直接进入follow setTimeout(() => { runType.value = 'follow' + if (isAlliance.value) { + followAndGreetUnion( + { + deviceList: deviceInformation.value.map(item => item.deviceId), + anchorList: [], + prologueList: getContentpriList(), + needReply: auto.value + } + ).then((res) => { + hostList = [] + }) + } else { + passAnchorData( + { + deviceList: deviceInformation.value.map(item => item.deviceId), + anchorList: [], + prologueList: getContentpriList(), + comment: comonList, + needReply: auto.value, + isComment: common.value //是否评论 + + } + ).then((res) => { + hostList = [] + }) + } - passAnchorData( - { - deviceList: deviceInformation.value.map(item => item.deviceId), - anchorList: [], - prologueList: getContentpriList(), - comment: comonList, - needReply: auto.value - } - ).then((res) => { - hostList = [] - }) }, 1000) return @@ -1254,7 +1235,7 @@ function runTask(key, deviceId, type) { } else if (key === 'like') { if (!deviceId) { - await stopAll(100) + await stopAll(5000) } else { growAccount({ udid: deviceId }) return @@ -1268,7 +1249,7 @@ function runTask(key, deviceId, type) { } else if (key === 'brushLive') { if (!deviceId) { - await stopAll(100) + await stopAll(5000) } else { watchLiveForGrowth({ udid: deviceId }) return @@ -1282,7 +1263,7 @@ function runTask(key, deviceId, type) { runType.value = 'brushLive' } else if (key === 'listen') { if (!deviceId) { - await stopAll(100) + await stopAll(5000) } else { monitorMessages({ udid: deviceId }) return @@ -1402,7 +1383,7 @@ function startScheduleLoop() { pauseSnapshot = { index: scheduleState.index, elapsedBeforePause } // 停掉当前片段 - await stopAll(100) + await stopAll(5000) // 执行中断任务(带重试) @@ -1590,13 +1571,6 @@ const checkVPN = async () => { } }; -// 语言选项(也可以使用内置默认) -// const languages = [ -// { label: '简体中文 (zh-CN)', value: 'zh-CN' }, -// { label: 'English (en)', value: 'en' }, -// { label: '日本語 (ja)', value: 'ja' }, -// ] - // 模拟翻译函数:你可以接入自己后端或第三方接口 sentences文本 targetLang语言 async function doTranslate(sentences, targetLang) { const str = arrayToString(sentences) @@ -1680,6 +1654,83 @@ function arrayToString(arr) { // 过滤空项并用 \n 连接 return arr.filter(Boolean).join(' \n') } + + +// —— 每 20s 刷新网络状态 —— +// 写一个小函数专门刷新网络状态,方便复用 +async function refreshNetStatus() { + const list = [...(deviceInformation.value || [])] + + // 并发请求每台设备网络状态(安全处理错误) + const settled = await Promise.allSettled( + list.map(d => getDeviceNetStatus({ udid: d.deviceId })) + ) + + settled.forEach((s, i) => { + const id = list[i].deviceId + netStatus[id] = (s.status === 'fulfilled' && s.value === true) + }) + console.log("设备在线状态", settled) + + // 清理已下线设备的状态,避免残留 + const aliveIds = new Set(list.map(d => d.deviceId)) + Object.keys(netStatus).forEach(id => { + if (!aliveIds.has(id)) { + delete netStatus[id] + delete lastNet[id] + delete offlineFlags[id] + clearResumeTimer(id) + } + }) + + // —— 在这里做“网络→任务联动”(只在有运行模式时触发)—— + if (runType.value) { + list.forEach(({ deviceId: id }) => { + const prev = lastNet[id] // 之前的网络状态 + const curr = netStatus[id] // 当前的网络状态(true/false/undefined) + if (typeof curr === 'undefined') return + + // 边沿触发:true -> false 断网,false -> true 恢复 + if (prev !== curr) { + // 断网:立刻停止该设备任务(只停这一台) + if (curr === false) { + clearResumeTimer(id) // 避免有遗留恢复定时器 + if (!offlineFlags[id]) { + console.log(`[NET] ${id} 离线,停止该设备任务`) + offlineFlags[id] = true + // 这里调用你已有的“只停一台”的方法 + stopOne(id) + } + } + + // 恢复:等待网络稳定后再恢复该设备任务 + if (curr === true && offlineFlags[id]) { + clearResumeTimer(id) + const timer = setTimeout(() => { + // 二次确认:还在运行模式里、该设备仍在线 + if (runType.value && netStatus[id] === true) { + console.log(`[NET] ${id} 恢复在线,重新启动当前模式: ${runType.value}`) + // 仅启动这一台 + runTask(runType.value, id) + // 标记已恢复 + offlineFlags[id] = false + } + resumeTimers.delete(id) + }, NET_RESUME_STABLE_MS) + resumeTimers.set(id, timer) + } + } + + // 更新 last + lastNet[id] = curr + }) + } else { + // 不在运行模式时,清理“暂停标记”和恢复定时器,保持干净 + Object.keys(offlineFlags).forEach(id => { offlineFlags[id] = false }) + Array.from(resumeTimers.keys()).forEach(id => clearResumeTimer(id)) + } +} +