2.9.1最终稳定版
This commit is contained in:
@@ -31,3 +31,8 @@ export function comment() {
|
||||
export function logout(data) {
|
||||
return postAxios({ url: 'api/user/aiChat-logout', data })
|
||||
}
|
||||
|
||||
//获取账号状态
|
||||
export function health(data) {
|
||||
return getAxios({ url: 'api/common/health' })
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
:show-close="false" @close="onClose">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="text-lg font-semibold">多语翻译</div>
|
||||
<div class="text-lg font-semibold">打招呼内容</div>
|
||||
<!-- <div class="text-xs text-gray-500">可输入多句文本 → 选择语种 → 一键翻译 → 可编辑&持久化 → 确定返回某语种结果</div> -->
|
||||
</div>
|
||||
</template>
|
||||
@@ -21,6 +21,12 @@
|
||||
<el-button type="success" @click="exportPrologue()">
|
||||
导入私信
|
||||
</el-button>
|
||||
<el-tooltip class="box-item" effect="dark" content="关闭后,招呼内容将不再翻译" placement="bottom">
|
||||
<el-switch v-model="isTranslation" class="fancy-switch" :width="70" inline-prompt
|
||||
active-text="翻译" inactive-text="翻译" style="margin-left:16px" />
|
||||
</el-tooltip>
|
||||
|
||||
<el-text style="margin-left: 10px;" type="primary"> </el-text>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -43,7 +49,7 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 语种与翻译控制区 -->
|
||||
<el-card shadow="never" class="!border rounded-xl">
|
||||
<el-card v-show="isTranslation" shadow="never" class="!border rounded-xl">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium">语种与翻译</span>
|
||||
@@ -90,7 +96,7 @@
|
||||
|
||||
<div class="col-span-12 lg:col-span-6">
|
||||
<el-alert title="说明" type="info" :closable="false" show-icon class="mb-3"
|
||||
description="选择语种后点击『翻译』。翻译结果可在下方标签页逐条手动调整,自动保存开启时会同步到本地。" />
|
||||
description="选择语种后点击『翻译』。翻译结果可在下方标签页逐条手动调整,选择对应地区国家所有语言语种。" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -136,16 +142,17 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="text-xs text-gray-500">
|
||||
<div class="text-xs text-gray-500" v-show="isTranslation">
|
||||
共 {{ sentences.length }} 句 · 选择 {{ selectedLangs.length }} 种语言
|
||||
<!-- <template v-if="activeTab"> · 当前返回:{{ getLangLabel(activeTab) }}</template> -->
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
自动回复
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="开启后,检测到新私信将自动回复" placement="bottom">
|
||||
<el-tooltip class="box-item" effect="dark"
|
||||
:content="userdata.aiReplay ? '开启后,检测到新私信将自动回复' : '未开通,请联系管理员开通'" placement="bottom">
|
||||
|
||||
<el-switch style="margin-right: 20px;" v-model="auto" />
|
||||
<el-switch :disabled="!userdata.aiReplay" style="margin-right: 20px;" v-model="auto" />
|
||||
</el-tooltip>
|
||||
<el-button @click="onClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!canConfirm" @click="onConfirm">确定</el-button>
|
||||
@@ -159,6 +166,10 @@
|
||||
import { reactive, ref, watch, computed, onMounted } from 'vue'
|
||||
import { prologue } from '@/api/account';
|
||||
import { Loading } from '@element-plus/icons-vue' // ✅ 旋转图标
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getUser } from '@/stores/storage'
|
||||
let userdata = ref(getUser());
|
||||
|
||||
const suppressCancelNext = ref(false) // ✅ 下次关闭是否屏蔽 cancel 事件
|
||||
/**
|
||||
* Props & Emits(JS 版)
|
||||
@@ -179,7 +190,7 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
let auto = ref(true);
|
||||
let auto = ref(false);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
@@ -198,6 +209,21 @@ const autoSave = ref(true)
|
||||
const activeTab = ref('')
|
||||
const engine = ref('custom') // 'custom' | 'local'
|
||||
|
||||
const MAX_SENTENCES = 30
|
||||
|
||||
function clampSentences(list) {
|
||||
const trimmed = list.slice(0, MAX_SENTENCES)
|
||||
if (list.length > MAX_SENTENCES) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: `最多支持 ${MAX_SENTENCES} 条,已保留前 ${MAX_SENTENCES} 条。`,
|
||||
duration: 2500,
|
||||
})
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
const isTranslation = ref(false)
|
||||
watch(selectedLangs, (langs) => {
|
||||
const keep = new Set(langs)
|
||||
|
||||
@@ -220,8 +246,8 @@ watch(selectedLangs, (langs) => {
|
||||
/** 默认语言选项 */
|
||||
const defaultLanguages = [
|
||||
{ label: '英语', value: 'en' },
|
||||
{ label: '简体中文', value: 'zh-CN' },
|
||||
{ label: '繁体中文', value: 'zh-TW' },
|
||||
{ label: '简体中文', value: 'zh' },
|
||||
{ label: '繁体中文', value: 'zh_tw' },
|
||||
{ label: '俄语', value: 'ru' },
|
||||
{ label: '日语', value: 'ja' },
|
||||
{ label: '韩语', value: 'ko' },
|
||||
@@ -329,16 +355,28 @@ function importFromTextarea() {
|
||||
.split(/\r?\n/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
sentences.splice(0, sentences.length, ...lines)
|
||||
|
||||
const limited = clampSentences(lines)
|
||||
sentences.splice(0, sentences.length, ...limited)
|
||||
syncLengths()
|
||||
if (autoSave.value) saveToLocal()
|
||||
}
|
||||
|
||||
//新增一行时限制
|
||||
function addSentence() {
|
||||
if (sentences.length >= MAX_SENTENCES) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: `最多只可添加 ${MAX_SENTENCES} 条。`,
|
||||
})
|
||||
return
|
||||
}
|
||||
sentences.push('')
|
||||
syncLengths()
|
||||
if (autoSave.value) saveToLocal()
|
||||
}
|
||||
|
||||
|
||||
function removeSentence(i) {
|
||||
sentences.splice(i, 1)
|
||||
syncLengths()
|
||||
@@ -432,11 +470,12 @@ function loadFromLocal() {
|
||||
const key = (k) => `${storagePrefix.value}:${k}`
|
||||
try {
|
||||
const s = JSON.parse(localStorage.getItem(key('sentences')) || '[]')
|
||||
const limited = clampSentences(Array.isArray(s) ? s : [])
|
||||
const langs = JSON.parse(localStorage.getItem(key('selectedLangs')) || '[]')
|
||||
const a = JSON.parse(localStorage.getItem(key('activeTab')) || '""')
|
||||
const t = JSON.parse(localStorage.getItem(key('translations')) || '{}')
|
||||
|
||||
sentences.splice(0, sentences.length, ...(Array.isArray(s) ? s : []))
|
||||
sentences.splice(0, sentences.length, ...limited)
|
||||
selectedLangs.value = Array.isArray(langs) ? langs : []
|
||||
activeTab.value = typeof a === 'string' ? a : ''
|
||||
|
||||
@@ -460,14 +499,46 @@ watch([sentences, selectedLangs, translations, activeTab], () => {
|
||||
if (autoSave.value) saveToLocal()
|
||||
}, { deep: true })
|
||||
|
||||
function onConfirm() {
|
||||
async function onConfirm() {
|
||||
if (!activeTab.value) return
|
||||
suppressCancelNext.value = true // ✅ 告诉下次关闭不要触发 cancel
|
||||
// 拷贝成普通对象,避免把响应式引用直接抛给外部
|
||||
|
||||
// 如果关闭了翻译功能,则先提示
|
||||
if (!isTranslation.value) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'当前「翻译」开关已关闭,打招呼内容将不会被翻译,只会使用原文发送。是否继续?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '继续',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
customClass: 'confirm-box-sm',
|
||||
}
|
||||
)
|
||||
// 用户点击“继续” -> 继续执行提交逻辑
|
||||
} catch {
|
||||
// 用户点击“取消” -> 直接退出
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '已取消发送',
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// === 以下是原逻辑 ===
|
||||
suppressCancelNext.value = true
|
||||
const out = JSON.parse(JSON.stringify(translations))
|
||||
// 追加 yolo 为原始内容(源句子数组)
|
||||
|
||||
// 追加原始内容
|
||||
out.yolo = sentences.slice()
|
||||
emit('confirm', { type: props.type, strings: out, autoBlo: auto.value })
|
||||
|
||||
emit('confirm', {
|
||||
type: props.type,
|
||||
strings: out,
|
||||
autoBlo: auto.value,
|
||||
needTranslate: isTranslation.value,
|
||||
})
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
@@ -536,4 +607,73 @@ onMounted(() => {
|
||||
.rounded-xl {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
/* === 紧凑版 fancy-switch,不依赖外部库 === */
|
||||
:deep(.fancy-switch) {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 轨道部分(略大于默认) */
|
||||
:deep(.fancy-switch .el-switch__core) {
|
||||
height: 28px;
|
||||
/* 比默认略高 */
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
transition: all 0.25s ease;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 滑块部分(略缩小) */
|
||||
:deep(.fancy-switch .el-switch__action) {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
/* 关闭态:暗色背景,灰色文字 */
|
||||
:deep(.fancy-switch:not(.is-checked) .el-switch__core) {
|
||||
background: linear-gradient(180deg, #1f2937, #111827) !important;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
/* 开启态:蓝绿渐变高亮 */
|
||||
:deep(.fancy-switch.is-checked .el-switch__core) {
|
||||
background: linear-gradient(90deg, #60a5fa, #34d399) !important;
|
||||
color: #0b1220;
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(96, 165, 250, .18),
|
||||
0 4px 18px rgba(52, 211, 153, .25);
|
||||
}
|
||||
|
||||
/* 滑块开启态:更亮一点 */
|
||||
:deep(.fancy-switch.is-checked .el-switch__action) {
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 16px rgba(52, 211, 153, .3);
|
||||
}
|
||||
|
||||
/* 呼吸动画(更柔和版本) */
|
||||
@keyframes glowPulseSmall {
|
||||
0% {
|
||||
box-shadow: 0 0 0 2px rgba(96, 165, 250, .16), 0 4px 18px rgba(52, 211, 153, .25);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 0 5px rgba(96, 165, 250, .08), 0 4px 22px rgba(52, 211, 153, .30);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 2px rgba(96, 165, 250, .16), 0 4px 18px rgba(52, 211, 153, .25);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.fancy-switch.is-checked .el-switch__core) {
|
||||
animation: glowPulseSmall 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 键盘焦点高亮 */
|
||||
:deep(.fancy-switch .el-switch__input:focus-visible + .el-switch__core) {
|
||||
outline: 2px solid rgba(96, 165, 250, .55);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -231,6 +231,11 @@ video {
|
||||
width: 30px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
span {
|
||||
size: 10px;
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.left-button:hover {
|
||||
|
||||
@@ -20,11 +20,11 @@ function attachInterceptors(instance) {
|
||||
instance.interceptors.request.use((config) => {
|
||||
// 登录/换租户接口可能不需要 token,根据你的需求放行
|
||||
const urlLast = sliceUrl(config.url || '')
|
||||
if ((urlLast === 'prologue' || urlLast === 'comment' || urlLast === 'aiChat-logout' || urlLast === 'updates')) {
|
||||
if ((urlLast === 'prologue' || urlLast === 'comment' || urlLast === 'aiChat-logout' || urlLast === 'updates' || urlLast === 'health')) {
|
||||
config.headers['vvtoken'] = getToken()
|
||||
}
|
||||
// 超时 & 通用头
|
||||
config.timeout = 600000
|
||||
config.timeout = 180000
|
||||
if (!config.headers) config.headers = {}
|
||||
// 大多数 POST 走 x-www-form-urlencoded(保持你原来的行为)
|
||||
// console.log(config.method)
|
||||
@@ -38,7 +38,8 @@ function attachInterceptors(instance) {
|
||||
// 响应拦截器
|
||||
instance.interceptors.response.use(
|
||||
(response) => {
|
||||
const data = response.data
|
||||
const data = response.data // 请求的 返回数据
|
||||
const url = response.config.url // 请求的 url
|
||||
if (data?.code === 0 || data?.code === 200) {
|
||||
// 成功:返回业务数据(没有就回传原 data)
|
||||
return (data?.data !== undefined) ? data.data : data
|
||||
@@ -62,7 +63,12 @@ function attachInterceptors(instance) {
|
||||
if (error.code === 'ERR_NETWORK') {
|
||||
// 你原来的 isStart 逻辑如果要保留,只控制是否弹 toast;但**不要 return**
|
||||
if (!isStart) {
|
||||
ElMessage.error('网络请求失败')
|
||||
if (url === 'deviceList') {
|
||||
|
||||
} else {
|
||||
ElMessage.error('网络请求失败')
|
||||
}
|
||||
|
||||
}
|
||||
isStart = false
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
|
||||
@@ -79,7 +79,7 @@ import { getToken, setToken, setUser, setUserPass, getUserPass } from '@/stores/
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
import { passToken } from '@/api/ios';
|
||||
|
||||
let version = ref('2.7.0');
|
||||
let version = ref('2.8.0');
|
||||
onMounted(() => {
|
||||
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
|
||||
<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">
|
||||
<div v-if="btn.show?.()" class="left-button" :style="btn.style ? btn.style() : {}"
|
||||
:title="btn.tooltip ? btn.tooltip() : ''" @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>
|
||||
@@ -42,7 +44,9 @@
|
||||
<div class="app-button" @click="restartTikTok({ udid: device.deviceId })">重置tiktok</div>
|
||||
<div class="app-button" @click="getMesList(device.deviceId)">获取当前聊天记录</div>
|
||||
<div class="app-button" @click="stopOne(device.deviceId)">停止任务</div>
|
||||
<div class="app-button" @click="runTask(runType, device.deviceId)">开启</div>
|
||||
<div class="app-button" @click="scheduleEnabled = true; runTask(runType, device.deviceId);">
|
||||
开启
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,11 +59,11 @@
|
||||
</div>
|
||||
<img v-if="isWifi" style="position: absolute; right: 20px; top: 10px; height: 30px;" src="@/assets/wifi.png"></img>
|
||||
<MultiLineInputDialog v-model:visible="showDialog" :initialText='initialTextStr' :title="dialogTitle"
|
||||
:index="selectedDevice" @confirm="onDialogConfirm" @cancel="stopAll(100)" />
|
||||
:index="selectedDevice" @confirm="onDialogConfirm" @cancel="stopAll(2000)" />
|
||||
<HostListManagerDialog v-model:visible="showHostDlg" @save="onHostSaved" @invitType="invitTypeFun" />
|
||||
|
||||
<TranslationDialog v-model="showtransDlg" :type="transDlgType" :translateFn="doTranslate"
|
||||
storage-key-prefix="demo-translation" @confirm="onConfirm" @cancel="stopAll(100)" />
|
||||
storage-key-prefix="demo-translation" @confirm="onConfirm" @cancel="stopAll(2000)" />
|
||||
</div>
|
||||
<!-- <AgentGuildDialog v-model="showMyInfo" :model="formInit" @save="handleSave" /> -->
|
||||
<AgentGuildDialog v-model="showMyInfo" :model="{
|
||||
@@ -135,13 +139,13 @@ import { chat, translationToChinese, translation, customTranslation } from "@/ap
|
||||
import HostListManagerDialog from '@/components/HostListManagerDialog.vue'
|
||||
import AgentGuildDialog from '@/components/AgentGuildDialog.vue'
|
||||
|
||||
import MultiLineInputDialog from '@/components/MultiLineInputDialog.vue'; // 根据实际路径修改
|
||||
import TranslationDialog from '@/components/translationDialog.vue'; // 根据实际路径修改
|
||||
import MultiLineInputDialog from '@/components/MultiLineInputDialog.vue';
|
||||
import TranslationDialog from '@/components/translationDialog.vue';
|
||||
|
||||
import ChatDialog from '@/components/ChatDialog.vue'
|
||||
import MessageDialogd from '@/components/MessageDialogd.vue'
|
||||
import { pickTikTokBundleId } from '@/utils/arrUtils'
|
||||
import { logout, updates } from '@/api/account';
|
||||
import { logout, updates, health } from '@/api/account';
|
||||
import {
|
||||
getDeviceList,
|
||||
toHome,
|
||||
@@ -191,7 +195,7 @@ const borkerConfig = reactive({
|
||||
let common = ref(true);
|
||||
// 自动化
|
||||
let auto = ref(true);
|
||||
|
||||
let isTranslate = ref(false)
|
||||
let initialTextStr = ref('') // 初始文本字符串
|
||||
// 批次缓冲(仅用于当前“波”)
|
||||
let batch = []; // [{ country, text }]
|
||||
@@ -199,8 +203,9 @@ let flushTimer = null;
|
||||
|
||||
let hostList = [] // 主播列表
|
||||
let comonList = [] //评论列表
|
||||
//查询列表轮询
|
||||
//查询 列表 新消息轮询
|
||||
let getListtimer = null;
|
||||
let getNetworkListtimer = null;
|
||||
let userdata = getUser();
|
||||
let chatList = ref([])
|
||||
|
||||
@@ -223,6 +228,10 @@ let deviceInformation = ref([])
|
||||
//停止中
|
||||
let stopLoading = null
|
||||
|
||||
// 是否未开通 AI 自动回复(= 监测消息不可用)
|
||||
const isListenLockedByPlan = computed(() => String(userdata?.aiReplay ?? '0') === '0')
|
||||
|
||||
|
||||
// 每台设备的网络状态:true=正常,false=异常
|
||||
const netStatus = reactive({}) // { [deviceId]: boolean }
|
||||
|
||||
@@ -240,6 +249,11 @@ function clearResumeTimer(id) {
|
||||
}
|
||||
}
|
||||
|
||||
// —— 在线判定/过滤 ——
|
||||
// 红框=netStatus[id] === false 视为离线;其它(true/undefined)都当作可用
|
||||
const isOnline = (id) => netStatus[id] !== false
|
||||
const onlineOnly = (ids) => ids.filter(isOnline)
|
||||
|
||||
// 当前是否被其它模式占用(四个互斥按钮专用)
|
||||
const isLocked = (type) => !!runType.value && runType.value !== type
|
||||
|
||||
@@ -251,12 +265,21 @@ watch(sseEnabled, v => {
|
||||
})
|
||||
|
||||
// 互斥按钮的样式:激活=红,锁定=半透明且禁点
|
||||
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 ctrlStyle = (type) => {
|
||||
const lockedByMode = isLocked(type)
|
||||
const disableListen = (type === 'listen') && isListenLockedByPlan.value
|
||||
|
||||
const style = {
|
||||
backgroundColor: runType.value === type ? 'red' : '',
|
||||
opacity: (lockedByMode || disableListen) ? 0.5 : 1,
|
||||
// 允许事件,以便显示浏览器原生 title 提示 & 点击弹出 ElMessage
|
||||
pointerEvents: lockedByMode ? 'none' : 'auto',
|
||||
cursor: (lockedByMode || disableListen) ? 'not-allowed' : 'pointer',
|
||||
filter: disableListen ? 'grayscale(1)' : ''
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
@@ -358,33 +381,31 @@ const buttons = [
|
||||
},
|
||||
{
|
||||
label: '监测消息',
|
||||
// 点击前先判断是否未开通,未开通只提示不执行
|
||||
onClick: () => {
|
||||
if (isListenLockedByPlan.value) {
|
||||
ElMessage.warning('未开通 AI 自动回复功能,请联系管理员开通');
|
||||
return
|
||||
}
|
||||
if (runType.value == 'listen') {
|
||||
deviceInformation.value.forEach((item) => {
|
||||
stopScript({ udid: item.deviceId })
|
||||
})
|
||||
runType.value = ''
|
||||
return
|
||||
};
|
||||
}
|
||||
if (isLocked('listen')) return
|
||||
//如果传评论就注释一下两行代码,解开后面代码
|
||||
runType.value = 'listen'
|
||||
deviceInformation.value.forEach((item) => monitorMessages({ udid: item.deviceId }))
|
||||
// dialogTitle.value = '评论(无消息将刷视频)';
|
||||
// setTimeout(() => {
|
||||
// showDialog.value = true;
|
||||
|
||||
// initialTextStr.value = getContentListMultiline();
|
||||
// }, 500)
|
||||
|
||||
},
|
||||
// 悬停提示文案
|
||||
tooltip: () => (isListenLockedByPlan.value ? '未开通:AI 自动回复' : ''),
|
||||
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('listen')
|
||||
|
||||
},
|
||||
// {
|
||||
// label: '定时调度',
|
||||
@@ -397,7 +418,7 @@ const buttons = [
|
||||
// },
|
||||
{
|
||||
label: '全部停止',
|
||||
onClick: () => stopAll(100),
|
||||
onClick: () => stopAll(2000),
|
||||
show: () => true,
|
||||
img: {
|
||||
normal: new URL('@/assets/video/leftBtn8.png', import.meta.url).href,
|
||||
@@ -521,6 +542,7 @@ const loops = new Map(); // deviceId -> { timer, stopped, lastUrl }
|
||||
|
||||
/** 生成一次地址 */
|
||||
const makeUrl = (port) => `http://localhost:${port}/?t=${Date.now()}`;
|
||||
// const makeUrl = (port) => `http://192.168.1.209:${port}/?t=${Date.now()}`;
|
||||
|
||||
/** 真正断开某台设备当前连接并清理 */
|
||||
function hardCloseImg(deviceId) {
|
||||
@@ -880,31 +902,59 @@ async function stopAll(time) {
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
});
|
||||
|
||||
// 🔒 先强制关闭所有提示弹窗
|
||||
ElMessageBox.close();
|
||||
|
||||
scheduleEnabled.value = false;
|
||||
runType.value = '';
|
||||
isMsgPop.value = false;
|
||||
dropCurrentWave();
|
||||
|
||||
// ========== ⏰ 新增超时逻辑 ==========
|
||||
const STOP_TASK_TIMEOUT_MS = 120000; // 120 秒超时,可调整
|
||||
|
||||
// 封装一个带超时保护的 Promise
|
||||
const withTimeout = (p, ms) => {
|
||||
return Promise.race([
|
||||
p,
|
||||
new Promise((resolve) =>
|
||||
setTimeout(() => {
|
||||
console.warn(`[stopAllTask] 超时 ${ms}ms`);
|
||||
resolve('timeout');
|
||||
}, ms)
|
||||
),
|
||||
]);
|
||||
};
|
||||
|
||||
try {
|
||||
// 1) 等待接口完成
|
||||
await stopAllTask(deviceInformation.value.map(item => item.deviceId));
|
||||
// 2) 等待 2 秒(和你原逻辑一致)
|
||||
// 1️⃣ 带超时保护地调用接口
|
||||
const res = await withTimeout(
|
||||
stopAllTask(deviceInformation.value.map(item => item.deviceId)),
|
||||
STOP_TASK_TIMEOUT_MS
|
||||
);
|
||||
|
||||
if (res === 'timeout') {
|
||||
console.warn('stopAllTask 请求超时,自动继续后续逻辑');
|
||||
}
|
||||
|
||||
// 2️⃣ 等待指定时间(原逻辑保留)
|
||||
await new Promise(r => setTimeout(r, time));
|
||||
|
||||
stopLoading.close();
|
||||
console.log('全部停止成功', printCurrentTime());
|
||||
ElMessage.success('全部停止成功');
|
||||
|
||||
// 3) 明确返回(可选)
|
||||
// 3️⃣ 无论接口成功或超时都返回 true
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log('停止失败', printCurrentTime(), e);
|
||||
console.error('停止失败', printCurrentTime(), e);
|
||||
ElMessage.error('脚本已停止');
|
||||
stopLoading.close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//确认多行文本框内容
|
||||
function onDialogConfirm(result, type, index, data) {
|
||||
console.log(type, result, data);
|
||||
@@ -967,37 +1017,54 @@ onMounted(async () => {
|
||||
ElMessage.error(`未检测到设备`)
|
||||
|
||||
}
|
||||
|
||||
|
||||
//MQ链接
|
||||
window.electronAPI.startMq(userdata.tenantId, userdata.id)
|
||||
|
||||
|
||||
// 初始化时获取设备列表
|
||||
//每3秒获取一次设备列表 消息列表 和 查询主播列表是否还有主播
|
||||
getListtimer = setInterval(async () => {
|
||||
getDeviceListFun() //获取设备列表
|
||||
selectLastFun() //获取手机网络状态
|
||||
const hostsList = await getStoredHostList()
|
||||
selectLastFun() //获取新消息
|
||||
const hostsList = await getStoredHostList() //获取主播列表
|
||||
// console.log(hostsList.length)
|
||||
//当私信主播时,主播列表没有数据了,提示列表空了 并且关闭私信
|
||||
if (runType.value == 'follow') {
|
||||
if (hostsList.length <= 0) {
|
||||
await stopAll(5000)
|
||||
await stopAll(2000)
|
||||
runType.value = 'like'
|
||||
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
|
||||
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId, comment: getContentList(), isComment: common.value }))
|
||||
|
||||
ElMessageBox.alert('私信全部完成!(刷视频中)', '提示', {
|
||||
confirmButtonText: 'OK',
|
||||
callback: (action) => {
|
||||
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}, 3000)
|
||||
|
||||
setInterval(async () => {
|
||||
getNetworkListtimer = setInterval(async () => {
|
||||
await checkVPN()
|
||||
await refreshNetStatus()
|
||||
}, 1000 * 20)
|
||||
health().then((res) => {
|
||||
|
||||
}).catch((err) => {
|
||||
if (err.code === 40400) {
|
||||
//关闭获取设备列表的方法
|
||||
clearInterval(getListtimer)
|
||||
getListtimer = null
|
||||
//关闭获取网络状态的方法
|
||||
clearInterval(getNetworkListtimer)
|
||||
getNetworkListtimer = null
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
}, 1000 * 60 * 3)
|
||||
|
||||
|
||||
if (!await isAiConfig()) {
|
||||
@@ -1063,8 +1130,13 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
//关闭获取设备列表的方法
|
||||
clearInterval(getListtimer)
|
||||
getListtimer = null
|
||||
//关闭获取网络状态的方法
|
||||
clearInterval(getNetworkListtimer)
|
||||
getNetworkListtimer = null
|
||||
|
||||
})
|
||||
|
||||
let isStartLac = false
|
||||
@@ -1081,7 +1153,7 @@ const getDeviceListFun = () => {
|
||||
}).catch((err) => {
|
||||
if (isStartLac) {
|
||||
ElMessage.error(`IOSAI服务错误`)
|
||||
isStartLac = true
|
||||
// isStartLac = true
|
||||
} else {
|
||||
|
||||
}
|
||||
@@ -1153,15 +1225,21 @@ async function uploadLogFile() {
|
||||
}
|
||||
}
|
||||
function runTask(key, deviceId, type) {
|
||||
|
||||
// —— 若指定单设备,且该设备当前离线,则直接跳过 ——
|
||||
if (deviceId && !isOnline(deviceId)) {
|
||||
markPendingForOffline([deviceId]); // ⭐ 新增:让它后续来网能自动跟上
|
||||
ElMessage.warning('该设备当前网络不可用,已跳过');
|
||||
return;
|
||||
}
|
||||
console.log('[schedule] 切换到任务:', key, printCurrentTime())
|
||||
|
||||
forceActivate(key, async () => {
|
||||
if (key === 'follow') {
|
||||
console.log("进入follow", scheduleEnabled.value)
|
||||
if (scheduleEnabled.value) {
|
||||
if (!deviceId) {
|
||||
await stopAll(5000)
|
||||
} else {
|
||||
//如果有id 就只执行一个 并且return出去
|
||||
if (deviceId) {
|
||||
if (isAlliance.value) {
|
||||
followAndGreetUnion(
|
||||
{
|
||||
@@ -1169,9 +1247,11 @@ function runTask(key, deviceId, type) {
|
||||
anchorList: [],
|
||||
prologueList: getContentpriList(),
|
||||
needReply: auto.value,
|
||||
needTranslate: isTranslate.value,
|
||||
}
|
||||
).then((res) => {
|
||||
hostList = []
|
||||
return
|
||||
})
|
||||
} else {
|
||||
passAnchorData(
|
||||
@@ -1181,75 +1261,103 @@ function runTask(key, deviceId, type) {
|
||||
prologueList: getContentpriList(),
|
||||
comment: comonList,
|
||||
needReply: auto.value,
|
||||
needTranslate: isTranslate.value,
|
||||
isComment: common.value //是否评论
|
||||
|
||||
}
|
||||
).then((res) => {
|
||||
hostList = []
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
} else {
|
||||
//没有id 直接停止所有设备
|
||||
await stopAll(2000)
|
||||
|
||||
//第一个小时结束后,第二轮开始的时候,直接进入follow
|
||||
setTimeout(() => {
|
||||
runType.value = 'follow'
|
||||
//过滤无网络设备
|
||||
const allIds = deviceInformation.value.map(item => item.deviceId)
|
||||
// 在线要启动的
|
||||
const deviceIds = onlineOnly(allIds)
|
||||
if (!deviceIds.length) {
|
||||
ElMessage.warning('没有在线设备可执行任务');
|
||||
// 也别忘了把离线的标成待恢复
|
||||
markPendingForOffline(allIds.filter(id => netStatus[id] === false))
|
||||
return
|
||||
}
|
||||
// ⭐ 给这次被跳过的离线设备打标记(后续来网自动补上)
|
||||
markPendingForOffline(allIds.filter(id => !deviceIds.includes(id)))
|
||||
|
||||
if (isAlliance.value) {
|
||||
|
||||
followAndGreetUnion(
|
||||
{
|
||||
deviceList: deviceIds,
|
||||
anchorList: [],
|
||||
prologueList: getContentpriList(),
|
||||
needReply: auto.value,
|
||||
needTranslate: isTranslate.value,
|
||||
}
|
||||
).then((res) => {
|
||||
hostList = []
|
||||
return
|
||||
})
|
||||
} else {
|
||||
passAnchorData(
|
||||
{
|
||||
deviceList: deviceIds,
|
||||
anchorList: [],
|
||||
prologueList: getContentpriList(),
|
||||
comment: comonList,
|
||||
needReply: auto.value,
|
||||
needTranslate: isTranslate.value,
|
||||
isComment: common.value //是否评论
|
||||
|
||||
}
|
||||
).then((res) => {
|
||||
hostList = []
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
}, 1000)
|
||||
|
||||
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 = []
|
||||
})
|
||||
}
|
||||
|
||||
}, 1000)
|
||||
|
||||
return
|
||||
} else {
|
||||
// 如果是暂停状态,则打开弹窗进行开启第一轮任务
|
||||
scheduleEnabled.value = true
|
||||
initialTextStr.value = '';
|
||||
dialogTitle.value = '主播ID';
|
||||
showDialog.value = true;
|
||||
}
|
||||
|
||||
scheduleEnabled.value = true
|
||||
initialTextStr.value = '';
|
||||
dialogTitle.value = '主播ID';
|
||||
showDialog.value = true;
|
||||
|
||||
|
||||
} else if (key === 'like') {
|
||||
|
||||
if (!deviceId) {
|
||||
await stopAll(5000)
|
||||
await stopAll(2000)
|
||||
} else {
|
||||
growAccount({ udid: deviceId })
|
||||
growAccount({ udid: deviceId, comment: getContentList(), isComment: common.value })
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
scheduleEnabled.value = true
|
||||
runType.value = 'like'
|
||||
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
|
||||
deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId, comment: getContentList(), isComment: common.value }))
|
||||
}, 1000)
|
||||
|
||||
} else if (key === 'brushLive') {
|
||||
if (!deviceId) {
|
||||
await stopAll(5000)
|
||||
await stopAll(2000)
|
||||
} else {
|
||||
watchLiveForGrowth({ udid: deviceId })
|
||||
return
|
||||
@@ -1263,7 +1371,7 @@ function runTask(key, deviceId, type) {
|
||||
runType.value = 'brushLive'
|
||||
} else if (key === 'listen') {
|
||||
if (!deviceId) {
|
||||
await stopAll(5000)
|
||||
await stopAll(2000)
|
||||
} else {
|
||||
monitorMessages({ udid: deviceId })
|
||||
return
|
||||
@@ -1282,7 +1390,7 @@ function runTask(key, deviceId, type) {
|
||||
|
||||
async function stopCurrentMode() {
|
||||
// 如果你希望“只停当前片段的设备”,也可以用 stopScript 针对设备循环
|
||||
await stopAll(100)
|
||||
await stopAll(2000)
|
||||
}
|
||||
|
||||
/** 恢复:回到 scheduleState.index 对应片段,并让 startTime 回到“暂停前进度” */
|
||||
@@ -1296,6 +1404,7 @@ function resumeAfterInterrupt() {
|
||||
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
|
||||
|
||||
// 确保任务是该片段
|
||||
scheduleEnabled.value = true
|
||||
runTask(schedulePlan[scheduleState.index].key)
|
||||
pauseSnapshot = null
|
||||
}
|
||||
@@ -1383,7 +1492,7 @@ function startScheduleLoop() {
|
||||
pauseSnapshot = { index: scheduleState.index, elapsedBeforePause }
|
||||
|
||||
// 停掉当前片段
|
||||
await stopAll(5000)
|
||||
await stopAll(2000)
|
||||
|
||||
|
||||
// 执行中断任务(带重试)
|
||||
@@ -1421,6 +1530,7 @@ function startScheduleLoop() {
|
||||
scheduleState.index = (scheduleState.index + 1) % schedulePlan.length
|
||||
scheduleState.startTime = now
|
||||
localStorage.setItem('SCHEDULE_STATE', JSON.stringify(scheduleState))
|
||||
scheduleEnabled.value = true
|
||||
runTask(schedulePlan[scheduleState.index].key)
|
||||
}
|
||||
}, scheduleTickMs)
|
||||
@@ -1519,18 +1629,9 @@ async function doLogout() {
|
||||
}
|
||||
}
|
||||
function stopOne(deviceId) {
|
||||
stopLoading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '停止中',
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
});
|
||||
stopScript({ udid: deviceId }).then((res) => {
|
||||
stopLoading.close()
|
||||
}).catch((err) => {
|
||||
stopLoading.close()
|
||||
|
||||
}).finally((err) => {
|
||||
stopLoading.close()
|
||||
stopScript({ udid: deviceId }).then((res) => {
|
||||
// ElMessage.success('停止成功');
|
||||
|
||||
})
|
||||
}
|
||||
@@ -1599,17 +1700,31 @@ async function doTranslate(sentences, targetLang) {
|
||||
}
|
||||
|
||||
// 接收“确定”事件返回结果
|
||||
function onConfirm({ type, strings, autoBlo }) {
|
||||
console.log('✅ 确认返回:', type, strings, autoBlo)
|
||||
function onConfirm({ type, strings, autoBlo, needTranslate }) {
|
||||
console.log('✅ 确认返回:', type, strings, autoBlo, needTranslate)
|
||||
auto.value = autoBlo
|
||||
isTranslate.value = needTranslate
|
||||
showtransDlg.value = false
|
||||
runType.value = 'follow'
|
||||
setContentpriList(strings)
|
||||
|
||||
//过滤无网络设备
|
||||
const allIds = deviceInformation.value.map(item => item.deviceId)
|
||||
// 在线要启动的
|
||||
const deviceIds = onlineOnly(allIds)
|
||||
if (!deviceIds.length) {
|
||||
ElMessage.warning('没有在线设备可执行任务');
|
||||
// 也别忘了把离线的标成待恢复
|
||||
markPendingForOffline(allIds.filter(id => netStatus[id] === false))
|
||||
return
|
||||
}
|
||||
// ⭐ 给这次被跳过的离线设备打标记(后续来网自动补上)
|
||||
markPendingForOffline(allIds.filter(id => !deviceIds.includes(id)))
|
||||
|
||||
if (isAlliance.value) {
|
||||
followAndGreetUnion(
|
||||
{
|
||||
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||||
deviceList: deviceIds,
|
||||
anchorList: hostList.map(item => ({
|
||||
anchorId: item.id,
|
||||
country: item.country,
|
||||
@@ -1618,7 +1733,7 @@ function onConfirm({ type, strings, autoBlo }) {
|
||||
})),
|
||||
prologueList: strings,
|
||||
needReply: autoBlo,
|
||||
// needTranslate: data.needTranslate,
|
||||
needTranslate: needTranslate,
|
||||
}
|
||||
).then((res) => {
|
||||
ElMessage({ type: 'success', message: '任务开启成功' });
|
||||
@@ -1628,7 +1743,7 @@ function onConfirm({ type, strings, autoBlo }) {
|
||||
} else {
|
||||
passAnchorData(
|
||||
{
|
||||
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||||
deviceList: deviceIds,
|
||||
anchorList: hostList.map(item => ({
|
||||
anchorId: item.id,
|
||||
country: item.country,
|
||||
@@ -1638,7 +1753,7 @@ function onConfirm({ type, strings, autoBlo }) {
|
||||
prologueList: strings, //私信对象
|
||||
comment: comonList, //评论列表
|
||||
needReply: autoBlo, //自动回复
|
||||
// needTranslate: data.needTranslate,
|
||||
needTranslate: needTranslate, //翻译
|
||||
isComment: common.value //是否评论
|
||||
}
|
||||
).then((res) => {
|
||||
@@ -1656,23 +1771,50 @@ function arrayToString(arr) {
|
||||
}
|
||||
|
||||
|
||||
// —— 每 20s 刷新网络状态 ——
|
||||
// 写一个小函数专门刷新网络状态,方便复用
|
||||
// 每台设备请求间隔(毫秒)
|
||||
const REQ_GAP_MS = 5000
|
||||
|
||||
async function refreshNetStatus() {
|
||||
const list = [...(deviceInformation.value || [])]
|
||||
if (!list.length) return
|
||||
|
||||
// 并发请求每台设备网络状态(安全处理错误)
|
||||
const settled = await Promise.allSettled(
|
||||
list.map(d => getDeviceNetStatus({ udid: d.deviceId }))
|
||||
const firstPass = {}
|
||||
|
||||
// ✅ 串行请求:每台设备间隔 5 秒
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const d = list[i]
|
||||
try {
|
||||
const res = await getDeviceNetStatus({ udid: d.deviceId })
|
||||
firstPass[d.deviceId] = (res === true)
|
||||
} catch (err) {
|
||||
firstPass[d.deviceId] = false
|
||||
}
|
||||
|
||||
// 不是最后一台才等待 5s
|
||||
if (i < list.length - 1) {
|
||||
await new Promise(r => setTimeout(r, REQ_GAP_MS))
|
||||
}
|
||||
}
|
||||
|
||||
// 二次复核
|
||||
const falseIds = Object.keys(firstPass).filter(id => firstPass[id] === false)
|
||||
const confirmResults = await Promise.all(
|
||||
falseIds.map(async id => {
|
||||
const stableOffline = await confirmStableOffline(id)
|
||||
return { id, stableOffline }
|
||||
})
|
||||
)
|
||||
|
||||
settled.forEach((s, i) => {
|
||||
const id = list[i].deviceId
|
||||
netStatus[id] = (s.status === 'fulfilled' && s.value === true)
|
||||
const effective = { ...firstPass }
|
||||
confirmResults.forEach(({ id, stableOffline }) => {
|
||||
effective[id] = !stableOffline
|
||||
})
|
||||
console.log("设备在线状态", settled)
|
||||
|
||||
// 清理已下线设备的状态,避免残留
|
||||
// 写回 UI
|
||||
Object.keys(effective).forEach(id => { netStatus[id] = effective[id] })
|
||||
console.log('设备在线状态(有效值)', effective)
|
||||
|
||||
// 清理已下线设备
|
||||
const aliveIds = new Set(list.map(d => d.deviceId))
|
||||
Object.keys(netStatus).forEach(id => {
|
||||
if (!aliveIds.has(id)) {
|
||||
@@ -1683,54 +1825,91 @@ async function refreshNetStatus() {
|
||||
}
|
||||
})
|
||||
|
||||
// —— 在这里做“网络→任务联动”(只在有运行模式时触发)——
|
||||
// 任务联动(保持原逻辑)
|
||||
if (runType.value) {
|
||||
list.forEach(({ deviceId: id }) => {
|
||||
const prev = lastNet[id] // 之前的网络状态
|
||||
const curr = netStatus[id] // 当前的网络状态(true/false/undefined)
|
||||
const prev = lastNet[id]
|
||||
const curr = netStatus[id]
|
||||
if (typeof curr === 'undefined') return
|
||||
|
||||
// 边沿触发:true -> false 断网,false -> true 恢复
|
||||
if (prev !== curr) {
|
||||
// 断网:立刻停止该设备任务(只停这一台)
|
||||
if (curr === false) {
|
||||
clearResumeTimer(id) // 避免有遗留恢复定时器
|
||||
clearResumeTimer(id)
|
||||
if (!offlineFlags[id]) {
|
||||
console.log(`[NET] ${id} 离线,停止该设备任务`)
|
||||
console.log(`[NET] ${id} 稳定离线,停止该设备任务`)
|
||||
offlineFlags[id] = true
|
||||
// 这里调用你已有的“只停一台”的方法
|
||||
stopOne(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复:等待网络稳定后再恢复该设备任务
|
||||
if (curr === true && offlineFlags[id]) {
|
||||
clearResumeTimer(id)
|
||||
const timer = setTimeout(() => {
|
||||
// 二次确认:还在运行模式里、该设备仍在线
|
||||
const t = setTimeout(() => {
|
||||
if (runType.value && netStatus[id] === true) {
|
||||
console.log(`[NET] ${id} 恢复在线,重新启动当前模式: ${runType.value}`)
|
||||
// 仅启动这一台
|
||||
scheduleEnabled.value = true
|
||||
runTask(runType.value, id)
|
||||
// 标记已恢复
|
||||
offlineFlags[id] = false
|
||||
}
|
||||
resumeTimers.delete(id)
|
||||
}, NET_RESUME_STABLE_MS)
|
||||
resumeTimers.set(id, timer)
|
||||
resumeTimers.set(id, t)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 last
|
||||
lastNet[id] = curr
|
||||
})
|
||||
} else {
|
||||
// 不在运行模式时,清理“暂停标记”和恢复定时器,保持干净
|
||||
Object.keys(offlineFlags).forEach(id => { offlineFlags[id] = false })
|
||||
Array.from(resumeTimers.keys()).forEach(id => clearResumeTimer(id))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ===== 新增:二次复核断网所需的小工具 =====
|
||||
const verifyingOffline = {} // { [deviceId]: Promise<boolean> } 去重并发
|
||||
|
||||
const RETRY_COUNT = 2 // 额外再查几次
|
||||
const RETRY_GAP_MS = 1000 // 每次间隔(可调)
|
||||
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
|
||||
|
||||
async function safeGetStatus(udid) {
|
||||
try {
|
||||
return await getDeviceNetStatus({ udid }) === true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回 true=稳定离线;false=误报/抖动(有一次为 true 就当没离线) */
|
||||
async function confirmStableOffline(udid) {
|
||||
if (verifyingOffline[udid]) return verifyingOffline[udid]
|
||||
|
||||
verifyingOffline[udid] = (async () => {
|
||||
for (let i = 0; i < RETRY_COUNT; i++) {
|
||||
await sleep(RETRY_GAP_MS)
|
||||
const ok = await safeGetStatus(udid) // true=在线
|
||||
if (ok) return false // 有一次在线 => 抖动
|
||||
}
|
||||
return true // 全部都是 false => 稳定离线
|
||||
})().finally(() => { delete verifyingOffline[udid] })
|
||||
|
||||
return verifyingOffline[udid]
|
||||
}
|
||||
|
||||
//小工具:标记离线待恢复
|
||||
const markPendingForOffline = (ids) => {
|
||||
ids.forEach(id => {
|
||||
if (netStatus[id] === false) {
|
||||
offlineFlags[id] = true; // 标记成“暂停/待恢复”
|
||||
// 可选:初始化上次状态,便于日志判断
|
||||
if (typeof lastNet[id] === 'undefined') lastNet[id] = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
Reference in New Issue
Block a user