分包+主播库
This commit is contained in:
381
src/components/HostListManagerDialog.vue
Normal file
381
src/components/HostListManagerDialog.vue
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="show" width="70vw" :close-on-click-modal="false" :destroy-on-close="true" @open="onOpen">
|
||||||
|
<template #header>
|
||||||
|
<div class="dlg-title">
|
||||||
|
<span>主播管理</span>
|
||||||
|
<span class="muted">(已选 {{ selectedCount }} / 共 {{ hosts.length }})</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-button size="small" @click="selectAll">全选</el-button>
|
||||||
|
<el-button size="small" @click="selectNone">全不选</el-button>
|
||||||
|
<el-button size="small" @click="invertSelect">反选</el-button>
|
||||||
|
<el-button size="small" type="danger" :disabled="!selectedCount" @click="deleteSelected">删除选中</el-button>
|
||||||
|
|
||||||
|
<!-- ✅ 新增:一键删除已处理 -->
|
||||||
|
<el-button size="small" type="warning" :disabled="!processedCount" @click="deleteProcessed">
|
||||||
|
删除已处理
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button size="small" type="primary" @click="saveAll">保存</el-button>
|
||||||
|
<el-tooltip placement="bottom" effect="dark">
|
||||||
|
<template #content>
|
||||||
|
在空白区域按下左键拖拽进行框选<br />
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<el-icon class="hint">i</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 列表区域 -->
|
||||||
|
<div ref="gridRef" class="grid" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp"
|
||||||
|
@mouseleave="onMouseUp" @scroll="recalcRectsSoon">
|
||||||
|
<div v-for="it in hosts" :key="it.text" class="item-card" :class="{ selected: isSelected(it.text) }"
|
||||||
|
:ref="el => setCardRef(it.text, el)" @click.stop="toggleSelect(it.text)">
|
||||||
|
<div class="row top">
|
||||||
|
<span class="id" :title="it.text">{{ it.text }}</span>
|
||||||
|
<button class="x" title="删除此项" @click.stop="deleteOne(it.text)">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="row meta">
|
||||||
|
<span class="country" :title="it.country">{{ it.country || '—' }}</span>
|
||||||
|
<span class="state" :class="{ done: !!it.state }">{{ it.state ? '已处理' : '未处理' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 框选矩形 -->
|
||||||
|
<div v-if="selecting" class="selection-rect" :style="selectionStyle"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="foot">
|
||||||
|
<el-button @click="show = false">关闭</el-button>
|
||||||
|
<el-button type="primary" @click="saveAll">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { getHostList, setHostList } from '@/stores/storage'
|
||||||
|
|
||||||
|
// v-model:visible 接口
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:visible', 'save'])
|
||||||
|
const show = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (v) => emit('update:visible', v)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 数据
|
||||||
|
const hosts = ref([]) // {country, text, state}
|
||||||
|
const selected = reactive(new Set()) // 选中的 text 集合
|
||||||
|
|
||||||
|
// 卡片 DOM 引用与位置缓存
|
||||||
|
const gridRef = ref(null)
|
||||||
|
const cardRefs = reactive({}) // text -> el
|
||||||
|
const rectCache = reactive({}) // text -> DOMRect
|
||||||
|
let rectRecalcTimer = null
|
||||||
|
|
||||||
|
|
||||||
|
const processedCount = computed(() => hosts.value.filter(it => !!it?.state).length)
|
||||||
|
function setCardRef(key, el) {
|
||||||
|
if (el) {
|
||||||
|
cardRefs[key] = el
|
||||||
|
} else {
|
||||||
|
delete cardRefs[key]
|
||||||
|
delete rectCache[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoredHostList() {
|
||||||
|
const v = getHostList()
|
||||||
|
return Array.isArray(v) ? v : []
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onOpen() {
|
||||||
|
hosts.value = getStoredHostList()
|
||||||
|
selected.clear()
|
||||||
|
await nextTick()
|
||||||
|
recalcRects()
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAll() {
|
||||||
|
setHostList(hosts.value)
|
||||||
|
ElMessage.success('已保存')
|
||||||
|
emit('save', hosts.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择相关
|
||||||
|
const selectedCount = computed(() => selected.size)
|
||||||
|
function isSelected(id) { return selected.has(id) }
|
||||||
|
function toggleSelect(id) {
|
||||||
|
if (selected.has(id)) selected.delete(id)
|
||||||
|
else selected.add(id)
|
||||||
|
}
|
||||||
|
function selectAll() { selected.clear(); hosts.value.forEach(it => selected.add(it.text)) }
|
||||||
|
function selectNone() { selected.clear() }
|
||||||
|
function invertSelect() {
|
||||||
|
const next = new Set()
|
||||||
|
hosts.value.forEach(it => { if (!selected.has(it.text)) next.add(it.text) })
|
||||||
|
selected.clear(); next.forEach(id => selected.add(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelected() {
|
||||||
|
if (!selected.size) return
|
||||||
|
ElMessageBox.confirm(`确认删除选中的 ${selected.size} 项吗?`, '提示', { type: 'warning' })
|
||||||
|
.then(() => {
|
||||||
|
const keep = []
|
||||||
|
for (const it of hosts.value) if (!selected.has(it.text)) keep.push(it)
|
||||||
|
hosts.value = keep
|
||||||
|
selected.clear()
|
||||||
|
setHostList(keep)
|
||||||
|
recalcRectsSoon()
|
||||||
|
ElMessage.success('已删除选中')
|
||||||
|
})
|
||||||
|
.catch(() => { })
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteOne(id) {
|
||||||
|
const idx = hosts.value.findIndex(it => it.text === id)
|
||||||
|
if (idx !== -1) {
|
||||||
|
hosts.value.splice(idx, 1)
|
||||||
|
selected.delete(id)
|
||||||
|
// setHostList(hosts.value)
|
||||||
|
recalcRectsSoon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// —— 框选逻辑 ——
|
||||||
|
const selecting = ref(false)
|
||||||
|
const anchor = ref({ x: 0, y: 0 })
|
||||||
|
const cursor = ref({ x: 0, y: 0 })
|
||||||
|
const baseSelection = ref(new Set()) // 框选开始时的已有选择(支持累加)
|
||||||
|
|
||||||
|
const selectionStyle = computed(() => {
|
||||||
|
const root = gridRef.value
|
||||||
|
if (!root) return {}
|
||||||
|
// 使用容器坐标系定位矩形
|
||||||
|
const box = root.getBoundingClientRect()
|
||||||
|
const x1 = Math.min(anchor.value.x, cursor.value.x) - box.left + root.scrollLeft
|
||||||
|
const y1 = Math.min(anchor.value.y, cursor.value.y) - box.top + root.scrollTop
|
||||||
|
const x2 = Math.max(anchor.value.x, cursor.value.x) - box.left + root.scrollLeft
|
||||||
|
const y2 = Math.max(anchor.value.y, cursor.value.y) - box.top + root.scrollTop
|
||||||
|
return { left: x1 + 'px', top: y1 + 'px', width: (x2 - x1) + 'px', height: (y2 - y1) + 'px' }
|
||||||
|
})
|
||||||
|
|
||||||
|
function onMouseDown(e) {
|
||||||
|
if (e.button !== 0) return
|
||||||
|
const root = gridRef.value
|
||||||
|
if (!root) return
|
||||||
|
// 只在空白处拖拽,或按着 Alt 任意处拖拽
|
||||||
|
const onItem = e.target && e.target.closest && e.target.closest('.item-card')
|
||||||
|
if (onItem && !e.altKey) return
|
||||||
|
|
||||||
|
// 记录锚点(client 坐标,方便和 DOMRect 比较)
|
||||||
|
anchor.value = { x: e.clientX, y: e.clientY }
|
||||||
|
cursor.value = { x: e.clientX, y: e.clientY }
|
||||||
|
selecting.value = true
|
||||||
|
// 是否保留原选择
|
||||||
|
baseSelection.value = (e.ctrlKey || e.metaKey) ? new Set(Array.from(selected)) : new Set()
|
||||||
|
// 防止选中文本
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e) {
|
||||||
|
if (!selecting.value) return
|
||||||
|
cursor.value = { x: e.clientX, y: e.clientY }
|
||||||
|
updateSelectionByRect()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp() {
|
||||||
|
if (!selecting.value) return
|
||||||
|
selecting.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectionByRect() {
|
||||||
|
const x1 = Math.min(anchor.value.x, cursor.value.x)
|
||||||
|
const y1 = Math.min(anchor.value.y, cursor.value.y)
|
||||||
|
const x2 = Math.max(anchor.value.x, cursor.value.x)
|
||||||
|
const y2 = Math.max(anchor.value.y, cursor.value.y)
|
||||||
|
|
||||||
|
// 实时选择集合
|
||||||
|
const current = new Set(baseSelection.value)
|
||||||
|
for (const it of hosts.value) {
|
||||||
|
const r = rectCache[it.text]
|
||||||
|
if (!r) continue
|
||||||
|
const hit = !(r.left > x2 || r.right < x1 || r.top > y2 || r.bottom < y1)
|
||||||
|
if (hit) current.add(it.text)
|
||||||
|
}
|
||||||
|
// 覆盖 selected
|
||||||
|
selected.clear(); current.forEach(id => selected.add(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalcRects() {
|
||||||
|
for (const [key, el] of Object.entries(cardRefs)) {
|
||||||
|
try { rectCache[key] = el.getBoundingClientRect() } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalcRectsSoon() {
|
||||||
|
clearTimeout(rectRecalcTimer)
|
||||||
|
rectRecalcTimer = setTimeout(() => nextTick().then(recalcRects), 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function deleteProcessed() {
|
||||||
|
if (!processedCount.value) return
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确认删除所有“已处理”项(${processedCount.value} 个)吗?此操作不可撤销。`,
|
||||||
|
'提示',
|
||||||
|
{ type: 'warning' }
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
// 仅保留未处理
|
||||||
|
const keep = hosts.value.filter(it => !it?.state)
|
||||||
|
hosts.value = keep
|
||||||
|
|
||||||
|
// 清理已不存在的选中项
|
||||||
|
for (const id of Array.from(selected)) {
|
||||||
|
if (!keep.find(it => it.text === id)) selected.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
setHostList(keep) // 同步回缓存
|
||||||
|
recalcRectsSoon() // 重新计算卡片矩形,保证框选正常
|
||||||
|
ElMessage.success('已删除已处理项')
|
||||||
|
})
|
||||||
|
.catch(() => { })
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(hosts, () => nextTick().then(recalcRects))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dlg-title {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dlg-title .muted {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar .hint {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
position: relative;
|
||||||
|
height: 60vh;
|
||||||
|
overflow: auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px;
|
||||||
|
user-select: none;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card {
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: box-shadow .15s, border-color .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, .06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card.selected {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--el-color-primary) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .row.top {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .id {
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .x {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #f56c6c;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .x:hover {
|
||||||
|
color: #f33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .meta {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .state {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .state.done {
|
||||||
|
color: #67c23a;
|
||||||
|
border-color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-rect {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 1px dashed var(--el-color-primary);
|
||||||
|
background: color-mix(in srgb, var(--el-color-primary) 12%, transparent);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foot {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
85
src/components/LeftToolbar.vue
Normal file
85
src/components/LeftToolbar.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="center-line">
|
||||||
|
<div v-for="(btn, i) in buttons" :key="i" style="width: 100%;">
|
||||||
|
<div v-if="btn.show?.()" class="left-button"
|
||||||
|
:class="[{ active: btn.key && activeKey === btn.key, disabled: btn.key && isLocked && activeKey !== btn.key }]"
|
||||||
|
:style="btn.style ? btn.style() : {}" @click="$emit('click', btn)" @mouseenter="hoverIndex = i"
|
||||||
|
@mouseleave="hoverIndex = null">
|
||||||
|
<img :src="hoverIndex === i ? btn.img.hover : btn.img.normal" alt="">
|
||||||
|
{{ btn.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
buttons: { type: Array, required: true },
|
||||||
|
activeKey: { type: String, default: '' },
|
||||||
|
isLocked: { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(['click'])
|
||||||
|
|
||||||
|
const hoverIndex = ref(null)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- 注意:不要 scoped,直接引入全局样式,避免父组件 scoped 导致样式穿透问题 -->
|
||||||
|
<style scoped lang="less">
|
||||||
|
.center-line {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
// justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-button {
|
||||||
|
position: relative;
|
||||||
|
z-index: 999;
|
||||||
|
margin-bottom: 19px;
|
||||||
|
width: 100%;
|
||||||
|
height: 72px;
|
||||||
|
// background: #32C9CD;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
font-family: Source Han Sans SC;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #7A8B97;
|
||||||
|
line-height: 16px;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 30px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-button:hover {
|
||||||
|
background: #32C9CD;
|
||||||
|
color: #F9FAFE;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 鼠标按下时效果 */
|
||||||
|
.left-button:active {
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-button.active {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 互斥期间的其它三枚按钮置灰禁点 */
|
||||||
|
.left-button.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
121
src/composables/useMonitor.js
Normal file
121
src/composables/useMonitor.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// src/composables/useMonitor.js
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备消息监测(含“有消息时暂停,此设备私信发送成功后恢复”)
|
||||||
|
* - 每 10s 轮询 getmesNum,但跳过 paused 的设备
|
||||||
|
* - 暴露 pause/resume 方法,供外部在 onmessage 分支中调用
|
||||||
|
* - 提供 openMonitor()/closeMonitor()/stop() 以适配你现有按钮逻辑
|
||||||
|
*
|
||||||
|
* @param {object} options
|
||||||
|
* - deviceInformation: ref([]) 设备列表
|
||||||
|
* - runType: ref('') 当前运行模式
|
||||||
|
* - isStop: ref(false) 全局停止标志
|
||||||
|
* - isMonitorOn: ref(false) 监测开关(UI 显示用)
|
||||||
|
* - isShowMes: ref() 定时器引用(外面已有)
|
||||||
|
* - wsActionsRef: () => wsActions 一个函数,返回你动态创建的 wsActions 对象
|
||||||
|
* - onKickOnce?: (udid, index) => void 在恢复后,立刻补一次检测的 hook(默认调用 getmesNum)
|
||||||
|
*/
|
||||||
|
export function useMonitor({
|
||||||
|
deviceInformation,
|
||||||
|
runType,
|
||||||
|
isStop,
|
||||||
|
isMonitorOn,
|
||||||
|
isShowMes,
|
||||||
|
wsActionsRef,
|
||||||
|
onKickOnce
|
||||||
|
}) {
|
||||||
|
// 用 UDID 做键,避免 index 变化错位
|
||||||
|
const pausedDevices = new Set();
|
||||||
|
|
||||||
|
const isPausedByIndex = (index) => {
|
||||||
|
const udid = deviceInformation.value[index]?.udid;
|
||||||
|
return udid ? pausedDevices.has(udid) : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pauseMonitorByIndex = (index) => {
|
||||||
|
const udid = deviceInformation.value[index]?.udid;
|
||||||
|
if (udid) pausedDevices.add(udid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resumeMonitorByIndex = (index) => {
|
||||||
|
const udid = deviceInformation.value[index]?.udid;
|
||||||
|
if (udid) pausedDevices.delete(udid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearPaused = () => pausedDevices.clear();
|
||||||
|
|
||||||
|
const openMonitor = () => {
|
||||||
|
isStop.value = false;
|
||||||
|
// 立即跑一轮
|
||||||
|
const wsActions = wsActionsRef?.();
|
||||||
|
deviceInformation.value.forEach((device, index) => {
|
||||||
|
if (!isPausedByIndex(index)) {
|
||||||
|
wsActions?.getmesNum(device.udid, index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runType.value = 'listen';
|
||||||
|
isMonitorOn.value = true;
|
||||||
|
|
||||||
|
// 每 10s 轮询未暂停设备
|
||||||
|
isShowMes.value = setInterval(() => {
|
||||||
|
const ws = wsActionsRef?.();
|
||||||
|
deviceInformation.value.forEach((device, index) => {
|
||||||
|
if (!isPausedByIndex(index)) {
|
||||||
|
ws?.getmesNum(device.udid, index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 10000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeMonitor = () => {
|
||||||
|
isMonitorOn.value = false;
|
||||||
|
runType.value = '';
|
||||||
|
if (isShowMes.value) {
|
||||||
|
clearInterval(isShowMes.value);
|
||||||
|
isShowMes.value = '';
|
||||||
|
}
|
||||||
|
clearPaused();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 兼容你原来的 stop() 会顺手关监测
|
||||||
|
const stopAll = () => {
|
||||||
|
closeMonitor();
|
||||||
|
clearPaused();
|
||||||
|
isStop.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 用于“私信成功后立刻恢复并补一次检测”
|
||||||
|
const resumeAndKick = (index) => {
|
||||||
|
resumeMonitorByIndex(index);
|
||||||
|
setTimeout(() => {
|
||||||
|
const udid = deviceInformation.value[index]?.udid;
|
||||||
|
if (!udid) return;
|
||||||
|
|
||||||
|
if (typeof onKickOnce === 'function') {
|
||||||
|
onKickOnce(udid, index);
|
||||||
|
} else {
|
||||||
|
const ws = wsActionsRef?.();
|
||||||
|
ws?.getmesNum(udid, index);
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// state
|
||||||
|
pausedDevices,
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
isPausedByIndex,
|
||||||
|
pauseMonitorByIndex,
|
||||||
|
resumeMonitorByIndex,
|
||||||
|
clearPaused,
|
||||||
|
|
||||||
|
// controls
|
||||||
|
openMonitor,
|
||||||
|
closeMonitor,
|
||||||
|
stopAll,
|
||||||
|
resumeAndKick,
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/composables/useStreams.js
Normal file
82
src/composables/useStreams.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// composables/useStreams.js
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
|
||||||
|
export function useStreams({ instanceList, videoElement, wslist, openStr, VideoConverter }) {
|
||||||
|
// 用 Map 做延迟队列,避免固定 8 个的限制
|
||||||
|
const feedState = new Map()
|
||||||
|
|
||||||
|
function ensureState(index) {
|
||||||
|
if (!feedState.has(index)) {
|
||||||
|
feedState.set(index, { processing: false, pending: null })
|
||||||
|
}
|
||||||
|
return feedState.get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushFrame(index, buf) {
|
||||||
|
const st = ensureState(index)
|
||||||
|
if (st.processing) {
|
||||||
|
// 覆盖旧的等待帧,保留最新
|
||||||
|
st.pending = buf
|
||||||
|
return
|
||||||
|
}
|
||||||
|
st.processing = true
|
||||||
|
try {
|
||||||
|
instanceList[index].converter.appendRawData(new Uint8Array(buf))
|
||||||
|
} finally {
|
||||||
|
st.processing = false
|
||||||
|
if (st.pending) {
|
||||||
|
const next = st.pending
|
||||||
|
st.pending = null
|
||||||
|
queueMicrotask(() => pushFrame(index, next))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFeedState(index) {
|
||||||
|
const st = feedState.get(index)
|
||||||
|
if (!st) return
|
||||||
|
st.processing = false
|
||||||
|
st.pending = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForVideoEl(udid, tries = 20, delay = 16) {
|
||||||
|
for (let i = 0; i < tries; i++) {
|
||||||
|
const el = videoElement.value?.[udid]
|
||||||
|
if (el) return el
|
||||||
|
await nextTick()
|
||||||
|
await new Promise(r => setTimeout(r, delay))
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshStream(index, hard = true) {
|
||||||
|
const devices = Object.keys(videoElement.value || {})
|
||||||
|
const udid = devices[index]
|
||||||
|
const video = videoElement.value?.[udid]
|
||||||
|
if (!video || !instanceList[index]) return
|
||||||
|
|
||||||
|
resetFeedState(index)
|
||||||
|
try { instanceList[index].converter?.destroy?.() } catch { }
|
||||||
|
instanceList[index].converter = null
|
||||||
|
|
||||||
|
if (hard) {
|
||||||
|
try { video.pause?.() } catch { }
|
||||||
|
try { video.removeAttribute?.('src') } catch { }
|
||||||
|
try { video.load?.() } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新挂新的 converter
|
||||||
|
instanceList[index].converter = new VideoConverter(video, 60, 1)
|
||||||
|
|
||||||
|
// 让后端尽快推关键帧
|
||||||
|
try { wslist[index]?.send?.(openStr) } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
feedState,
|
||||||
|
pushFrame,
|
||||||
|
resetFeedState,
|
||||||
|
waitForVideoEl,
|
||||||
|
refreshStream,
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/composables/useTeardown.js
Normal file
108
src/composables/useTeardown.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// composables/useTeardown.js
|
||||||
|
|
||||||
|
export function useTeardown(deps) {
|
||||||
|
let sseRef = null;
|
||||||
|
let multiplexWS = null;
|
||||||
|
|
||||||
|
function setSSE(es) {
|
||||||
|
try { sseRef?.close?.(); } catch { }
|
||||||
|
sseRef = es;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMultiplexWS(ws) {
|
||||||
|
try { multiplexWS?.close?.(); } catch { }
|
||||||
|
multiplexWS = ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
function disposeDevice(index) {
|
||||||
|
try {
|
||||||
|
const di = deps.deviceInformation.value[index];
|
||||||
|
const udid = di?.udid;
|
||||||
|
|
||||||
|
// 停止定时器
|
||||||
|
if (deps.playTimer.value[index]) {
|
||||||
|
clearTimeout(deps.playTimer.value[index]);
|
||||||
|
deps.playTimer.value[index] = null;
|
||||||
|
}
|
||||||
|
const inst = deps.instanceList[index];
|
||||||
|
if (inst?.timer) {
|
||||||
|
clearInterval(inst.timer);
|
||||||
|
inst.timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭 WS
|
||||||
|
const ws = deps.wslist[index];
|
||||||
|
if (ws) {
|
||||||
|
ws.onopen = ws.onmessage = ws.onerror = ws.onclose = null;
|
||||||
|
try { ws.close(); } catch { }
|
||||||
|
deps.wslist[index] = undefined;
|
||||||
|
}
|
||||||
|
if (udid) deps.wsCache.delete(udid);
|
||||||
|
|
||||||
|
// 销毁解码器
|
||||||
|
try {
|
||||||
|
inst?.converter?.destroy?.();
|
||||||
|
} catch { }
|
||||||
|
if (deps.instanceList[index]) deps.instanceList[index].converter = undefined;
|
||||||
|
|
||||||
|
// 释放 <video>
|
||||||
|
if (udid) {
|
||||||
|
const video = deps.videoElement.value?.[udid];
|
||||||
|
if (video) {
|
||||||
|
try { video.pause?.(); } catch { }
|
||||||
|
try { video.removeAttribute?.('src'); } catch { }
|
||||||
|
try { video.load?.(); } catch { }
|
||||||
|
delete deps.videoElement.value[udid];
|
||||||
|
}
|
||||||
|
if (deps.canvasRef.value?.[udid]) {
|
||||||
|
delete deps.canvasRef.value[udid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空任务队列
|
||||||
|
try { deps.createTaskQueue(index).clear(); } catch { }
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[disposeDevice] error:', index, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disposeAll(reason = 'manual') {
|
||||||
|
console.log('[teardown] disposeAll:', reason);
|
||||||
|
|
||||||
|
// 停止全局监控
|
||||||
|
try { deps.stopAll?.(); } catch { }
|
||||||
|
|
||||||
|
// 释放所有设备
|
||||||
|
const n = deps.deviceInformation.value.length;
|
||||||
|
for (let i = 0; i < n; i++) disposeDevice(i);
|
||||||
|
deps.deviceInformation.value = [];
|
||||||
|
|
||||||
|
// 关闭 multiplex WS
|
||||||
|
try { multiplexWS?.close?.(); } catch { }
|
||||||
|
multiplexWS = null;
|
||||||
|
|
||||||
|
// 关闭 SSE
|
||||||
|
try { sseRef?.close?.(); } catch { }
|
||||||
|
sseRef = null;
|
||||||
|
|
||||||
|
// 页级定时器
|
||||||
|
if (deps.isShowMes.value) {
|
||||||
|
clearInterval(deps.isShowMes.value);
|
||||||
|
deps.isShowMes.value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空缓存
|
||||||
|
deps.wsCache.clear?.();
|
||||||
|
|
||||||
|
// 全局事件监听
|
||||||
|
try { deps.removeDocListeners?.(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
setSSE,
|
||||||
|
setMultiplexWS,
|
||||||
|
disposeDevice,
|
||||||
|
disposeAll,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -74,7 +74,8 @@ body {
|
|||||||
|
|
||||||
video {
|
video {
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
|
border: 13px solid rgba(84, 224, 206, 0.726);
|
||||||
|
border-radius: 20px;
|
||||||
/* 关键:让 video 不拦截鼠标事件 */
|
/* 关键:让 video 不拦截鼠标事件 */
|
||||||
|
|
||||||
/* 添加动画 */
|
/* 添加动画 */
|
||||||
@@ -106,8 +107,8 @@ video {
|
|||||||
|
|
||||||
.canvas {
|
.canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 13px;
|
||||||
left: 0;
|
left: 13px;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
/* transform: scale(0.27); */
|
/* transform: scale(0.27); */
|
||||||
/* 缩小到原始尺寸的50% */
|
/* 缩小到原始尺寸的50% */
|
||||||
@@ -168,41 +169,7 @@ video {
|
|||||||
transform: translateY(1px);
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-button {
|
|
||||||
position: relative;
|
|
||||||
z-index: 999;
|
|
||||||
margin-bottom: 19px;
|
|
||||||
width: 100%;
|
|
||||||
height: 72px;
|
|
||||||
// background: #32C9CD;
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
font-family: Source Han Sans SC;
|
|
||||||
font-weight: 800;
|
|
||||||
font-size: 20px;
|
|
||||||
color: #7A8B97;
|
|
||||||
line-height: 16px;
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 30px;
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-button:hover {
|
|
||||||
background: #32C9CD;
|
|
||||||
color: #F9FAFE;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 鼠标按下时效果 */
|
|
||||||
.left-button:active {
|
|
||||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.center-justify {
|
.center-justify {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -215,16 +182,4 @@ video {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
// justify-content: center;
|
// justify-content: center;
|
||||||
}
|
|
||||||
|
|
||||||
.left-button.active {
|
|
||||||
background-color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 互斥期间的其它三枚按钮置灰禁点 */
|
|
||||||
.left-button.disabled {
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.5;
|
|
||||||
filter: grayscale(0.4);
|
|
||||||
}
|
}
|
||||||
@@ -61,9 +61,10 @@ export function clearHostList() {
|
|||||||
export function addToHostList(newItem) {
|
export function addToHostList(newItem) {
|
||||||
// 获取当前的数组
|
// 获取当前的数组
|
||||||
const currentList = JSON.parse(localStorage.getItem('hostList') || '[]');
|
const currentList = JSON.parse(localStorage.getItem('hostList') || '[]');
|
||||||
|
newItem.forEach(element => {
|
||||||
|
currentList.push(element);
|
||||||
|
});
|
||||||
// 向数组添加新元素
|
// 向数组添加新元素
|
||||||
currentList.push(newItem);
|
|
||||||
|
|
||||||
// 更新存储的数组
|
// 更新存储的数组
|
||||||
localStorage.setItem('hostList', JSON.stringify(currentList));
|
localStorage.setItem('hostList', JSON.stringify(currentList));
|
||||||
@@ -98,4 +99,8 @@ export function setsessionId(data) {
|
|||||||
// 用于获取评论信息
|
// 用于获取评论信息
|
||||||
export function getsessionId() {
|
export function getsessionId() {
|
||||||
return JSON.parse(sessionStorage.getItem('sessionList'));
|
return JSON.parse(sessionStorage.getItem('sessionList'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ axios.interceptors.request.use((config) => {
|
|||||||
|
|
||||||
// 响应拦截器
|
// 响应拦截器
|
||||||
axios.interceptors.response.use((response) => {
|
axios.interceptors.response.use((response) => {
|
||||||
// console.log("response", response.data)
|
console.log("response", response.data)
|
||||||
if (response.data.code == 0 || response.data.code == 200) {
|
if (response.data.code == 0 || response.data.code == 200) {
|
||||||
// console.log("response", response.data.data)
|
// console.log("response", response.data.data)
|
||||||
return response.data.data
|
return response.data.data
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const mouseData = {
|
|||||||
export function createWsActions(wslist) {
|
export function createWsActions(wslist) {
|
||||||
// 通用 ws 发送方法
|
// 通用 ws 发送方法
|
||||||
function send(index, payload) {
|
function send(index, payload) {
|
||||||
|
console.log("发送任务", payload.type)
|
||||||
if (wslist[index]) {
|
if (wslist[index]) {
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ import { getToken, setToken, setUser, setUserPass, getUserPass } from '@/stores/
|
|||||||
import { ElLoading, ElMessage } from 'element-plus';
|
import { ElLoading, ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
|
||||||
let version = ref('0.0.0');
|
let version = ref('1.5.4');
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<el-scrollbar class="left"> <!-- 左边栏 -->
|
<el-scrollbar class="left"> <!-- 左边栏 -->
|
||||||
<div class="center-line"> <!-- 左边栏按钮 -->
|
<LeftToolbar :buttons="buttons" :active-key="activeKey" :is-locked="isLocked" @click="handleBtnClick" />
|
||||||
<div v-for="(btn, index) in buttons" :key="index" style="width: 100%;">
|
<el-button style="position: absolute;left: 20px; bottom: 20px;" @click="showHostDlg = true">执行主播库</el-button>
|
||||||
<div v-if="btn.show?.()" class="left-button" :class="[{ active: isActive(btn), disabled: isDisabled(btn) }]"
|
|
||||||
:style="btn.style ? btn.style() : {}" @click="handleBtnClick(btn)" @mouseenter="hoverIndex = index"
|
|
||||||
@mouseleave="hoverIndex = null">
|
|
||||||
<img :src="hoverIndex === index ? btn.img.hover : btn.img.normal" alt="">
|
|
||||||
{{ btn.label }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
<!-- 中间手机区域 -->
|
<!-- 中间手机区域 -->
|
||||||
<div class="content" @click.self="selectedDevice = 999">
|
<div class="content" @click.self="selectedDevice = 999">
|
||||||
@@ -38,7 +30,7 @@
|
|||||||
<!-- <div class="app-button" @click="wsActions.getSize(device.udid, index)">获取屏幕尺寸</div> -->
|
<!-- <div class="app-button" @click="wsActions.getSize(device.udid, index)">获取屏幕尺寸</div> -->
|
||||||
|
|
||||||
<div class="app-button" @click="wsActions.test(device.udid, index)">打印ui节点树</div>
|
<div class="app-button" @click="wsActions.test(device.udid, index)">打印ui节点树</div>
|
||||||
<div class="app-button" @click="wsActions.isOneLive(device.udid, index)">判断单人还是双人</div>
|
<!-- <div class="app-button" @click="wsActions.isOneLive(device.udid, index)">判断单人还是双人</div> -->
|
||||||
<div class="app-button" @click="wsActions.slideDown(device.udid, index)">下滑</div>
|
<div class="app-button" @click="wsActions.slideDown(device.udid, index)">下滑</div>
|
||||||
<div class="app-button" @click="wsActions.killNow(device.udid, index)">关闭当前应用</div>
|
<div class="app-button" @click="wsActions.killNow(device.udid, index)">关闭当前应用</div>
|
||||||
<div class="app-button" @click="chooseFile(device.udid, index, 1, wsActions)">安装 APK
|
<div class="app-button" @click="chooseFile(device.udid, index, 1, wsActions)">安装 APK
|
||||||
@@ -61,6 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<MultiLineInputDialog v-model:visible="showDialog" :initialText='""' :title="dialogTitle" :index="selectedDevice"
|
<MultiLineInputDialog v-model:visible="showDialog" :initialText='""' :title="dialogTitle" :index="selectedDevice"
|
||||||
@confirm="onDialogConfirm" @cancel="stop" />
|
@confirm="onDialogConfirm" @cancel="stop" />
|
||||||
|
<HostListManagerDialog v-model:visible="showHostDlg" @save="onHostSaved" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -70,7 +63,7 @@ import VideoConverter from "h264-converter";
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import {
|
import {
|
||||||
setphoneXYinfo, getphoneXYinfo, getUser,
|
setphoneXYinfo, getphoneXYinfo, getUser,
|
||||||
getHostList, setHostList, getContentpriList,
|
getHostList, setHostList, addToHostList, getContentpriList,
|
||||||
setContentpriList, getContentList, setContentList,
|
setContentpriList, getContentList, setContentList,
|
||||||
setsessionId, getsessionId
|
setsessionId, getsessionId
|
||||||
} from '@/stores/storage'
|
} from '@/stores/storage'
|
||||||
@@ -88,11 +81,17 @@ import { prologue, comment } from '@/api/account';
|
|||||||
import { createTaskQueue } from '@/composables/useTaskQueue' //创建任务
|
import { createTaskQueue } from '@/composables/useTaskQueue' //创建任务
|
||||||
import { useCanvasPointer } from '@/composables/useCanvasPointer' //canvas 初始化 点击转换
|
import { useCanvasPointer } from '@/composables/useCanvasPointer' //canvas 初始化 点击转换
|
||||||
import { attachTrimmerForIndex } from '@/composables/useVideoStream' //修剪器
|
import { attachTrimmerForIndex } from '@/composables/useVideoStream' //修剪器
|
||||||
|
import HostListManagerDialog from '@/components/HostListManagerDialog.vue'
|
||||||
|
import { useMonitor } from '@/composables/useMonitor'
|
||||||
|
import { useTeardown } from '@/composables/useTeardown' //销毁
|
||||||
|
import LeftToolbar from '@/components/LeftToolbar.vue' //左侧工具栏
|
||||||
|
import { useStreams } from '@/composables/useStreams'
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
let wsActions = null;
|
let wsActions = null;
|
||||||
let userdata = getUser();
|
let userdata = getUser();
|
||||||
// 引入刷新方法
|
// 引入刷新方法
|
||||||
// const reload = inject("reload")
|
const reloadPage = inject("reload")
|
||||||
|
|
||||||
let phone = ref({ width: 207, height: 470 });
|
let phone = ref({ width: 207, height: 470 });
|
||||||
const openStr = base64ToBinary("ZQBwAAAAAAA8CgLQAtAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA"); //开启视频流的启动命令
|
const openStr = base64ToBinary("ZQBwAAAAAAA8CgLQAtAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA"); //开启视频流的启动命令
|
||||||
@@ -155,8 +154,7 @@ const mouseData = {
|
|||||||
let openShowChat = ref(true);
|
let openShowChat = ref(true);
|
||||||
let istranslate = ref(false); //是否是翻译本页
|
let istranslate = ref(false); //是否是翻译本页
|
||||||
let phoneXYinfo = ref(getphoneXYinfo() == null ? [{}, {}, {}, {}, {}, {}, {}, {}] : getphoneXYinfo());
|
let phoneXYinfo = ref(getphoneXYinfo() == null ? [{}, {}, {}, {}, {}, {}, {}, {}] : getphoneXYinfo());
|
||||||
// 当前悬浮的按钮索引
|
|
||||||
const hoverIndex = ref(null)
|
|
||||||
const isMonitorOn = ref(false) // false 表示关闭,true 表示开启
|
const isMonitorOn = ref(false) // false 表示关闭,true 表示开启
|
||||||
// 这四个互斥模式的 key,和你的 runType 对应
|
// 这四个互斥模式的 key,和你的 runType 对应
|
||||||
const EXCLUSIVE_KEYS = ['brushLive', 'like', 'follow', 'listen'];
|
const EXCLUSIVE_KEYS = ['brushLive', 'like', 'follow', 'listen'];
|
||||||
@@ -168,7 +166,10 @@ const KEY_LABEL = {
|
|||||||
follow: '一键关注并打招呼',
|
follow: '一键关注并打招呼',
|
||||||
listen: '监测消息',
|
listen: '监测消息',
|
||||||
};
|
};
|
||||||
|
const showHostDlg = ref(false)
|
||||||
|
function onHostSaved(list) {
|
||||||
|
console.log('保存后的 HostList:', list)
|
||||||
|
}
|
||||||
// 当前激活的互斥 key(runType 里只要是这四个之一就视为锁定)
|
// 当前激活的互斥 key(runType 里只要是这四个之一就视为锁定)
|
||||||
const activeKey = computed(() => EXCLUSIVE_KEYS.includes(runType.value) ? runType.value : '');
|
const activeKey = computed(() => EXCLUSIVE_KEYS.includes(runType.value) ? runType.value : '');
|
||||||
|
|
||||||
@@ -306,7 +307,10 @@ const buttons = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '登出',
|
label: '登出',
|
||||||
onClick: () => router.push('/'),
|
onClick: () => {
|
||||||
|
td.disposeAll('logout')
|
||||||
|
router.push('/')
|
||||||
|
},
|
||||||
show: () => true,
|
show: () => true,
|
||||||
img: {
|
img: {
|
||||||
normal: new URL('@/assets/video/leftBtn9.png', import.meta.url).href,
|
normal: new URL('@/assets/video/leftBtn9.png', import.meta.url).href,
|
||||||
@@ -315,6 +319,27 @@ const buttons = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
// 建立 monitor
|
||||||
|
const {
|
||||||
|
isPausedByIndex,
|
||||||
|
pauseMonitorByIndex,
|
||||||
|
resumeMonitorByIndex,
|
||||||
|
openMonitor,
|
||||||
|
closeMonitor,
|
||||||
|
stopAll,
|
||||||
|
resumeAndKick,
|
||||||
|
} = useMonitor({
|
||||||
|
deviceInformation,
|
||||||
|
runType,
|
||||||
|
isStop,
|
||||||
|
isMonitorOn,
|
||||||
|
isShowMes,
|
||||||
|
wsActionsRef: () => wsActions, // wsActions 是你 onopen 里创建的
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 放在变量都已声明之后(要能拿到 phone、toBuffer、wslist)
|
// 放在变量都已声明之后(要能拿到 phone、toBuffer、wslist)
|
||||||
const { canvasRef, frameMeta, initCanvas, getCanvasCoordinate, sendPointer } =
|
const { canvasRef, frameMeta, initCanvas, getCanvasCoordinate, sendPointer } =
|
||||||
useCanvasPointer({
|
useCanvasPointer({
|
||||||
@@ -324,33 +349,36 @@ const { canvasRef, frameMeta, initCanvas, getCanvasCoordinate, sendPointer } =
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const feedState = Array(8).fill(null).map(() => ({
|
// —— 放在变量区(deviceInformation 已经是 ref([]))——
|
||||||
processing: false,
|
const pausedDevices = new Set(); // 用 UDID 做键
|
||||||
pending: null, // ArrayBuffer 等最新一段
|
|
||||||
}));
|
|
||||||
function pushFrame(index, buf) {
|
|
||||||
const st = feedState[index];
|
|
||||||
if (st.processing) {
|
|
||||||
// 覆盖旧的等待帧,保留最新
|
|
||||||
st.pending = buf;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
st.processing = true;
|
|
||||||
try {
|
|
||||||
//推送帧到video
|
|
||||||
instanceList[index].converter.appendRawData(new Uint8Array(buf));
|
|
||||||
} finally {
|
|
||||||
st.processing = false;
|
|
||||||
if (st.pending) {
|
|
||||||
const next = st.pending;
|
|
||||||
st.pending = null;
|
|
||||||
// 用微任务衔接,避免递归栈增长
|
|
||||||
queueMicrotask(() => pushFrame(index, next));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const wsCache = new Map();
|
const wsCache = new Map();
|
||||||
|
|
||||||
|
const td = useTeardown({
|
||||||
|
deviceInformation,
|
||||||
|
wslist,
|
||||||
|
instanceList,
|
||||||
|
videoElement,
|
||||||
|
canvasRef,
|
||||||
|
playTimer,
|
||||||
|
isShowMes,
|
||||||
|
wsCache,
|
||||||
|
createTaskQueue,
|
||||||
|
stopAll, // useMonitor 里的
|
||||||
|
removeDocListeners: () => {
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { feedState, pushFrame, resetFeedState, waitForVideoEl, refreshStream } = useStreams({
|
||||||
|
instanceList,
|
||||||
|
videoElement,
|
||||||
|
wslist,
|
||||||
|
openStr,
|
||||||
|
VideoConverter, // 传入页面已引入的构造器,避免在 composable 重复引入
|
||||||
|
})
|
||||||
//````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````
|
//````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````
|
||||||
// 初始化 手机显示WebSocket 和视频流
|
// 初始化 手机显示WebSocket 和视频流
|
||||||
const initVideoStream = async (udid, index) => {
|
const initVideoStream = async (udid, index) => {
|
||||||
@@ -428,15 +456,18 @@ const initVideoStream = async (udid, index) => {
|
|||||||
//如果检测到有新消息,会收到两条ws回复,一条message==1 一条message==成功
|
//如果检测到有新消息,会收到两条ws回复,一条message==1 一条message==成功
|
||||||
} else if (resData.message == 1) {
|
} else if (resData.message == 1) {
|
||||||
console.log('有消息')
|
console.log('有消息')
|
||||||
|
pauseMonitorByIndex(index); // 新增:暂停该设备的轮询
|
||||||
} else if (resData.message == '点击成功') {
|
} else if (resData.message == '点击成功') {
|
||||||
|
console.log('双击', resData.x, resData.y, index)
|
||||||
console.log('双击', resData.x * iponeCoefficient.value[index].width, resData.y * iponeCoefficient.value[index].height, index)
|
console.log('双击', resData.x * iponeCoefficient.value[index].width, resData.y * iponeCoefficient.value[index].height, index)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
clickxy(resData.x * iponeCoefficient.value[index].width, resData.y * iponeCoefficient.value[index].height, index)
|
clickxy(resData.x * getphoneXYinfo()[index].width, resData.y * getphoneXYinfo()[index].height, index)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
clickxy(resData.x * iponeCoefficient.value[index].width, resData.y * iponeCoefficient.value[index].height, index) //index为9的时候长按
|
clickxy(resData.x * getphoneXYinfo()[index].width, resData.y * getphoneXYinfo()[index].height, index) //index为9的时候长按
|
||||||
|
wsActions.clickSysMesage(deviceInformation.value[index].udid, index) //点击消息进入对话框
|
||||||
}, 100)
|
}, 100)
|
||||||
wsActions.clickSysMesage(deviceInformation.value[index].udid, index) //点击消息进入对话框
|
|
||||||
}, 1500)
|
}, 2000)
|
||||||
}
|
}
|
||||||
} else if (resData.type == 'clickMesage') {
|
} else if (resData.type == 'clickMesage') {
|
||||||
//点击进入新消息页面以后,获取页面信息
|
//点击进入新消息页面以后,获取页面信息
|
||||||
@@ -449,6 +480,15 @@ const initVideoStream = async (udid, index) => {
|
|||||||
if (runType.value == 'follow') {
|
if (runType.value == 'follow') {
|
||||||
LikesToLikesToLikes(deviceInformation.value[index].udid, index)
|
LikesToLikesToLikes(deviceInformation.value[index].udid, index)
|
||||||
}
|
}
|
||||||
|
// 仅监听模式下恢复
|
||||||
|
if (runType.value === 'listen' || isMonitorOn.value) {
|
||||||
|
resumeMonitorByIndex(index);
|
||||||
|
// 轻微延迟后立刻补一次检测
|
||||||
|
setTimeout(() => {
|
||||||
|
const udid = deviceInformation.value[index]?.udid;
|
||||||
|
if (udid) wsActions.getmesNum(udid, index);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
@@ -517,6 +557,9 @@ const initVideoStream = async (udid, index) => {
|
|||||||
// iponeCoefficient.value[index].height = 720 / scaledH;
|
// iponeCoefficient.value[index].height = 720 / scaledH;
|
||||||
iponeCoefficient.value[index].width = scaledW / resData.width
|
iponeCoefficient.value[index].width = scaledW / resData.width
|
||||||
iponeCoefficient.value[index].height = scaledH / resData.height
|
iponeCoefficient.value[index].height = scaledH / resData.height
|
||||||
|
console.log(index)
|
||||||
|
phoneXYinfo.value[index].width = scaledW / resData.width
|
||||||
|
phoneXYinfo.value[index].height = scaledH / resData.height
|
||||||
console.log(
|
console.log(
|
||||||
`[getSize] raw=${RAW_W}x${RAW_H} -> scaled=${scaledW}x${scaledH} (align↓${ALIGN}) ${iponeCoefficient.value[index].width} ${iponeCoefficient.value[index].height}`
|
`[getSize] raw=${RAW_W}x${RAW_H} -> scaled=${scaledW}x${scaledH} (align↓${ALIGN}) ${iponeCoefficient.value[index].width} ${iponeCoefficient.value[index].height}`
|
||||||
);
|
);
|
||||||
@@ -597,9 +640,13 @@ const initVideoStream = async (udid, index) => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
} else if (resData.type == 'PrivatePushFollow') {
|
} else if (resData.type == 'PrivatePushFollow') {
|
||||||
//如果有新消息,回复完私信以后,返回三次,然后继续下一个任务
|
//如果有新消息,回复完私信以后,返回三次,然后继续下一个任务
|
||||||
wsActions.getmesNum(deviceInformation.value[index].udid, index)
|
// wsActions.getmesNum(deviceInformation.value[index].udid, index)
|
||||||
// LikesToLikesToLikes(deviceInformation.value[index].udid, index)
|
// LikesToLikesToLikes(deviceInformation.value[index].udid, index)
|
||||||
|
|
||||||
|
if (runType.value === 'listen' || isMonitorOn.value) {
|
||||||
|
resumeAndKick(index); // ← 一步到位:恢复并在 1.5s 后补一次 getmesNum
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@@ -729,12 +776,17 @@ const initVideoStream = async (udid, index) => {
|
|||||||
LikesToLikesToLikes(deviceInformation.value[index].udid, index)
|
LikesToLikesToLikes(deviceInformation.value[index].udid, index)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
} else if (resData.type == 'Privatetex' || resData.type == 'hostVideo' || resData.type == 'search' || resData.type == 'Attention' || resData.type == 'Comment') {
|
} else if (resData.type == 'PrivatePush' || resData.type == 'Privatetex' || resData.type == 'hostVideo' || resData.type == 'search' || resData.type == 'Attention' || resData.type == 'Comment') {
|
||||||
if (runType.value == 'follow') {
|
if (runType.value == 'follow') {
|
||||||
//关注的时候出现无法私信和没有视频的情况 错误重置
|
//关注的时候出现无法私信和没有视频的情况 错误重置
|
||||||
resetApp(udid, index)
|
resetApp(udid, index)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
wsActions.getmesNum(deviceInformation.value[index].udid, index)
|
if (isMonitor.value) {
|
||||||
|
//正常没有消息,发送完私信以后,返回六次,然后继续下一个任务
|
||||||
|
wsActions.getmesNum(deviceInformation.value[index].udid, index)
|
||||||
|
} else {
|
||||||
|
LikesToLikesToLikes(deviceInformation.value[index].udid, index)
|
||||||
|
}
|
||||||
// LikesToLikesToLikes(deviceInformation.value[index].udid, index)
|
// LikesToLikesToLikes(deviceInformation.value[index].udid, index)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
@@ -903,12 +955,14 @@ onMounted(() => {
|
|||||||
text: '初始化中...',
|
text: '初始化中...',
|
||||||
background: 'rgba(0, 0, 0, 0.7)',
|
background: 'rgba(0, 0, 0, 0.7)',
|
||||||
})
|
})
|
||||||
|
// reloadPage()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loading.close()
|
loading.close()
|
||||||
|
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
//sse接收爬虫发送的消息
|
//sse接收爬虫发送的消息
|
||||||
connectSSE(`https://datasave.api.yolozs.com/api/sse/connect/${userdata.tenantId}/${userdata.id}`, (data) => {
|
const es = connectSSE(`https://datasave.api.yolozs.com/api/sse/connect/${userdata.tenantId}/${userdata.id}`, (data) => {
|
||||||
// connectSSE(`http://192.168.1.155:19665/api/sse/connect/${userdata.tenantId}/${userdata.id}`, (data) => {
|
// connectSSE(`http://192.168.1.155:19665/api/sse/connect/${userdata.tenantId}/${userdata.id}`, (data) => {
|
||||||
// 处理服务端推送的数据
|
// 处理服务端推送的数据
|
||||||
console.log('来自服务端:', data)
|
console.log('来自服务端:', data)
|
||||||
@@ -933,7 +987,7 @@ onMounted(() => {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
message: '任务开启成功',
|
message: '任务开启成功',
|
||||||
})
|
})
|
||||||
setHostList(stroageHost.value)
|
addToHostList(stroageHost.value)
|
||||||
//重启tk
|
//重启tk
|
||||||
resetTk()
|
resetTk()
|
||||||
//获取评论
|
//获取评论
|
||||||
@@ -968,16 +1022,17 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// stroageHost.value = getHostList()
|
stroageHost.value = getHostList()
|
||||||
stroageHost.value.push(({ country: data.country, text: data.hostsId, state: false }))
|
stroageHost.value.push(({ country: data.country, text: data.hostsId, state: false }))
|
||||||
if (runType.value == 'follow') {
|
if (runType.value == 'follow') {
|
||||||
setHostList(stroageHost.value)
|
addToHostList([{ country: data.country, text: data.hostsId, state: false }])
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
td.setSSE(es)
|
||||||
});
|
});
|
||||||
//更新状态
|
//更新状态
|
||||||
// update(
|
// update(
|
||||||
@@ -1003,6 +1058,7 @@ onUnmounted(() => {
|
|||||||
const ObtainDeviceInformation = () => {
|
const ObtainDeviceInformation = () => {
|
||||||
// 2. 连接 WebSocket
|
// 2. 连接 WebSocket
|
||||||
const ws = new WebSocket("ws://127.0.0.1:8000/?action=multiplex");
|
const ws = new WebSocket("ws://127.0.0.1:8000/?action=multiplex");
|
||||||
|
td.setMultiplexWS(ws)
|
||||||
ws.binaryType = "arraybuffer";
|
ws.binaryType = "arraybuffer";
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
ws.send(eitwo);
|
ws.send(eitwo);
|
||||||
@@ -1016,14 +1072,15 @@ const ObtainDeviceInformation = () => {
|
|||||||
deviceInformation.value = [];
|
deviceInformation.value = [];
|
||||||
const filteredList = data.data.list.filter(item => item.state === 'device');
|
const filteredList = data.data.list.filter(item => item.state === 'device');
|
||||||
//检测到设备列表时,渲染所有设备
|
//检测到设备列表时,渲染所有设备
|
||||||
for (const item of filteredList) {
|
for (let i = 0; i < filteredList.length; i++) {
|
||||||
|
const item = filteredList[i];
|
||||||
deviceInformation.value.push(item);
|
deviceInformation.value.push(item);
|
||||||
await nextTick(); // 等 v-for 渲染出 <video>
|
await nextTick(); // 等 v-for 渲染出 <video>
|
||||||
initCanvas(item.udid); // 如果它也依赖 DOM,同样要在 nextTick 之后
|
initCanvas(item.udid); // 如果它也依赖 DOM,同样要在 nextTick 之后
|
||||||
initVideoStream(item.udid, deviceInformation.value.length - 1);
|
initVideoStream(item.udid, i); // 直接使用循环变量 i
|
||||||
// getSize 建议放到 wslist[index].onopen 里最稳,
|
// getSize 建议放到 wslist[index].onopen 里最稳,
|
||||||
// 若保留延时也可以:
|
// 若保留延时也可以:
|
||||||
setTimeout(() => wsActions?.getSize(item.udid, deviceInformation.value.length - 1), 2000);
|
setTimeout(() => wsActions?.getSize(item.udid, i), 2000); // 直接使用循环变量 i
|
||||||
}
|
}
|
||||||
} else if (data.type == "device") {
|
} else if (data.type == "device") {
|
||||||
if (data.data.device.state === "offline") {
|
if (data.data.device.state === "offline") {
|
||||||
@@ -1234,8 +1291,11 @@ async function drag(udid, index, x1, y1, x2, y2, durationMs = 300, steps = 8) {
|
|||||||
|
|
||||||
// 用 pointer down/up/move 改写后的 clickxy
|
// 用 pointer down/up/move 改写后的 clickxy
|
||||||
async function clickxy(x, y, index, type) {
|
async function clickxy(x, y, index, type) {
|
||||||
const udid = deviceInformation.value[index]?.udid;
|
const udid = deviceInformation.value[index].udid;
|
||||||
if (!udid) return;
|
if (!udid) {
|
||||||
|
console.error('clickxy: no udid');
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (type === 3) {
|
if (type === 3) {
|
||||||
@@ -1283,16 +1343,9 @@ async function clickxy(x, y, index, type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空喂帧状态(避免旧帧冲突)
|
|
||||||
function resetFeedState(index) {
|
|
||||||
const st = feedState[index];
|
|
||||||
if (!st) return;
|
|
||||||
st.processing = false;
|
|
||||||
st.pending = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reload = (opts = {}) => {
|
const reload = (opts = {}) => {
|
||||||
const { onlySelected = false, hard = false } = opts;
|
const { onlySelected = false, hard = true } = opts;
|
||||||
const targets = (onlySelected && selectedDevice.value !== 999)
|
const targets = (onlySelected && selectedDevice.value !== 999)
|
||||||
? [selectedDevice.value]
|
? [selectedDevice.value]
|
||||||
: deviceInformation.value.map((_, i) => i);
|
: deviceInformation.value.map((_, i) => i);
|
||||||
@@ -1301,43 +1354,6 @@ const reload = (opts = {}) => {
|
|||||||
ElMessage.success(`已刷新${onlySelected ? '当前设备' : '全部设备'}`);
|
ElMessage.success(`已刷新${onlySelected ? '当前设备' : '全部设备'}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 重建某台设备的视频解码器,不动 ws、不动 canvas */
|
|
||||||
function refreshStream(index, hard = false) {
|
|
||||||
const dev = deviceInformation.value[index];
|
|
||||||
if (!dev) return;
|
|
||||||
|
|
||||||
const udid = dev.udid;
|
|
||||||
const video = videoElement.value && videoElement.value[udid];
|
|
||||||
if (!video || !instanceList[index]) return;
|
|
||||||
|
|
||||||
// 1) 停止旧的喂帧状态,销毁旧 converter
|
|
||||||
resetFeedState(index);
|
|
||||||
try {
|
|
||||||
const conv = instanceList[index].converter;
|
|
||||||
if (conv && typeof conv.destroy === 'function') conv.destroy();
|
|
||||||
} catch (e) { }
|
|
||||||
instanceList[index].converter = null;
|
|
||||||
|
|
||||||
// 2) 可选“硬刷新”:彻底重置 <video>,规避 SourceBuffer 残留
|
|
||||||
if (hard) {
|
|
||||||
try { video.pause && video.pause(); } catch (e) { }
|
|
||||||
try { video.removeAttribute && video.removeAttribute('src'); } catch (e) { }
|
|
||||||
try { video.load && video.load(); } catch (e) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) 新建 converter 挂到同一个 <video>
|
|
||||||
instanceList[index].converter = new VideoConverter(video, 60, 1);
|
|
||||||
|
|
||||||
// 4) 让后端立刻推关键帧/重开编码
|
|
||||||
try { wslist[index] && wslist[index].send(openStr); } catch (e) { }
|
|
||||||
|
|
||||||
// 5) 同步尺寸(不影响已有 canvas 坐标换算)
|
|
||||||
setTimeout(() => {
|
|
||||||
if (wsActions && typeof wsActions.getSize === 'function') {
|
|
||||||
wsActions.getSize(udid, index);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
//发送任务前的处理
|
//发送任务前的处理
|
||||||
function sendWsTask(index, data) {
|
function sendWsTask(index, data) {
|
||||||
@@ -1450,7 +1466,7 @@ function getVideoStyle(index) {
|
|||||||
return {
|
return {
|
||||||
width: isSelected ? baseWidth * 1.4 + 'px' : baseWidth + 'px',
|
width: isSelected ? baseWidth * 1.4 + 'px' : baseWidth + 'px',
|
||||||
height: isSelected ? baseHeight * 1.4 + 'px' : baseHeight + 'px',
|
height: isSelected ? baseHeight * 1.4 + 'px' : baseHeight + 'px',
|
||||||
border: isSelected ? '2px solid blue' : '1px solid blue',
|
// border: isSelected ? '2px solid blue' : '1px solid blue',
|
||||||
position: isSelected ? 'absolute' : 'relative',
|
position: isSelected ? 'absolute' : 'relative',
|
||||||
top: isSelected ? '0' : 'unset',
|
top: isSelected ? '0' : 'unset',
|
||||||
left: isSelected ? '0' : 'unset',
|
left: isSelected ? '0' : 'unset',
|
||||||
@@ -1463,7 +1479,7 @@ function getVideoStyle(index) {
|
|||||||
|
|
||||||
function stop() {
|
function stop() {
|
||||||
// actions[index] = [];
|
// actions[index] = [];
|
||||||
cloesMonitor(); //关闭监听
|
stopAll(); // ← 替代 cloesMonitor + pausedDevices.clear
|
||||||
isStop.value = true; //停止所有任务
|
isStop.value = true; //停止所有任务
|
||||||
isMsgPop.value = false;//关闭爬虫sse任务
|
isMsgPop.value = false;//关闭爬虫sse任务
|
||||||
|
|
||||||
@@ -1495,29 +1511,15 @@ function resetTk() {
|
|||||||
resetApp(device.udid, index)
|
resetApp(device.udid, index)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
//监听所有手机是否有消息
|
|
||||||
function openMonitor(type) {
|
|
||||||
isStop.value = false;
|
|
||||||
|
|
||||||
deviceInformation.value.forEach((device, index) => {
|
|
||||||
wsActions.getmesNum(device.udid, index)
|
|
||||||
runType.value = 'listen'
|
|
||||||
})
|
|
||||||
isShowMes.value = setInterval(() => {
|
|
||||||
deviceInformation.value.forEach((device, index) => {
|
|
||||||
wsActions.getmesNum(device.udid, index)
|
|
||||||
})
|
|
||||||
}, 10000)
|
|
||||||
}
|
|
||||||
|
|
||||||
//关闭监听
|
//关闭监听
|
||||||
function cloesMonitor() {
|
function cloesMonitor() {
|
||||||
isMonitorOn.value = false;//关闭监听
|
isMonitorOn.value = false;
|
||||||
deviceInformation.value.forEach((device, index) => {
|
deviceInformation.value.forEach(() => { runType.value = '' });
|
||||||
runType.value = ''
|
clearInterval(isShowMes.value);
|
||||||
})
|
isShowMes.value = '';
|
||||||
clearInterval(isShowMes.value)
|
pausedDevices.clear(); // 新增
|
||||||
isShowMes.value = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//一键养号
|
//一键养号
|
||||||
@@ -1591,7 +1593,7 @@ function onDialogConfirm(result, type, index, isMon) {
|
|||||||
result.forEach((item, indexA) => {
|
result.forEach((item, indexA) => {
|
||||||
hostListResult.push({ country: '', text: item, state: false })
|
hostListResult.push({ country: '', text: item, state: false })
|
||||||
})
|
})
|
||||||
setHostList(hostListResult)
|
addToHostList(hostListResult)
|
||||||
//打开评论弹窗
|
//打开评论弹窗
|
||||||
selectedDevice.value = 998;
|
selectedDevice.value = 998;
|
||||||
dialogTitle.value = '评论';
|
dialogTitle.value = '评论';
|
||||||
@@ -1631,16 +1633,7 @@ function manualGc() {
|
|||||||
window.electronAPI.manualGc()
|
window.electronAPI.manualGc()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待 video 引用就绪的小工具
|
|
||||||
async function waitForVideoEl(udid, tries = 20, delay = 16) {
|
|
||||||
for (let i = 0; i < tries; i++) {
|
|
||||||
const el = videoElement.value?.[udid];
|
|
||||||
if (el) return el;
|
|
||||||
await nextTick(); // 等下一次 DOM 刷新
|
|
||||||
await new Promise(r => setTimeout(r, delay)); // 再小等一帧
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|||||||
BIN
tk-ai-adb.zip
BIN
tk-ai-adb.zip
Binary file not shown.
BIN
tkAiPage.zip
BIN
tkAiPage.zip
Binary file not shown.
Reference in New Issue
Block a user