爬虫联动弹窗版本备份
This commit is contained in:
@@ -4,7 +4,15 @@
|
||||
<div style="position: absolute;left: 20px; top: 20px;">
|
||||
<el-button style="background: linear-gradient(90deg, #60a5fa, #34d399); color: azure;"
|
||||
@click="showMyInfo = true">人设编辑</el-button>
|
||||
|
||||
<!-- <el-button style="background: linear-gradient(90deg, #60a5fa, #34d399); color: azure;" @click="MesNewList.push({
|
||||
sender: 'Alice', // 或 name/user/from
|
||||
device: 'iPhone 14 Pro', // 或 deviceName/udid
|
||||
text: '你好呀~', // 或 content
|
||||
type: 'msg' // 可选;有些是 'time' 会被过滤
|
||||
})">新增</el-button> -->
|
||||
</div>
|
||||
|
||||
<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"
|
||||
@@ -16,15 +24,21 @@
|
||||
<div style="position: absolute;left: 20px; bottom: 20px;">
|
||||
<el-button @click="showHostDlg = true">执行主播库</el-button>
|
||||
<el-button type="info" @click="uploadLogFile">上传日志</el-button>
|
||||
<!-- 新增:SSE 弹窗总开关 -->
|
||||
<el-switch v-model="sseEnabled" inline-prompt active-text="监听爬虫" inactive-text="监听爬虫"
|
||||
style="margin-left: 8px;" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<!-- 中间手机区域 -->
|
||||
<div class="content" @click.self="selectedDevice = 999">
|
||||
<div v-if="isImg" class="video-container" v-for="(device, index) in deviceInformation" :key="device.deviceId">
|
||||
<div class="video-container" v-for="(device, index) in deviceInformation" :key="device.deviceId">
|
||||
<div class="video-canvas" :class="{ active: selectedDevice === index }" :style="getCanvasStyle(index)"
|
||||
@click="selectDevice(index)">
|
||||
<img class="stream" :src="'http://localhost:' + device.screenPort" />
|
||||
<img class="stream" :key="device.deviceId + '-' + imgKeyTick"
|
||||
:src="`http://localhost:${device.screenPort}/?t=${Date.now() || 0}`" :data-id="device.deviceId"
|
||||
:ref="el => (imgRefs[device.deviceId] = el)" />
|
||||
|
||||
<canvas v-show="selectedDevice === index" class="overlay"
|
||||
@mousedown.stop="(e) => onCanvasDown(device.deviceId, e, index)"
|
||||
@mouseup.stop="(e) => onCanvasUp(device.deviceId, e, index)"
|
||||
@@ -61,11 +75,11 @@
|
||||
<div class="right center-line" @click.self="selectedDevice = 999">
|
||||
<!-- <div style="margin: 30px;"></div> -->
|
||||
<ChatDialog :visible="openShowChat" :messages="chatList" />
|
||||
<MessageDialogd :visible="openShowChat" :messages="MesNewList" />
|
||||
<MessageDialogd :visible="openShowChat" :messages="MesNewList" :sound-src="ding" />
|
||||
</div>
|
||||
<MultiLineInputDialog v-model:visible="showDialog" :initialText='""' :title="dialogTitle" :index="selectedDevice"
|
||||
@confirm="onDialogConfirm" @cancel="stopAll" />
|
||||
<HostListManagerDialog v-model:visible="showHostDlg" @save="onHostSaved" />
|
||||
<HostListManagerDialog v-model:visible="showHostDlg" @save="onHostSaved" @invitType="invitTypeFun" />
|
||||
|
||||
</div>
|
||||
<!-- <AgentGuildDialog v-model="showMyInfo" :model="formInit" @save="handleSave" /> -->
|
||||
@@ -120,7 +134,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted, onBeforeUnmount, watch, inject, computed } from "vue";
|
||||
import { ref, reactive, onMounted, onUnmounted, onBeforeUnmount, watch, inject, computed, nextTick } from "vue";
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
setphoneXYinfo, getphoneXYinfo, getUser,
|
||||
@@ -155,11 +169,14 @@ import {
|
||||
getChatTextInfo,
|
||||
setLoginInfo,
|
||||
aiConfig,
|
||||
|
||||
selectLast,
|
||||
updatelast,
|
||||
stopAllTask
|
||||
} from '@/api/ios';
|
||||
import ding from '@/assets/mes.wav'
|
||||
import { set } from "lodash";
|
||||
const router = useRouter();
|
||||
const openShowChat = ref([true])
|
||||
const openShowChat = ref(true)
|
||||
//主播库
|
||||
const showHostDlg = ref(false)
|
||||
//ai人设
|
||||
@@ -172,66 +189,22 @@ const borkerConfig = reactive({
|
||||
contact: ''
|
||||
})
|
||||
|
||||
// 批次缓冲(仅用于当前“波”)
|
||||
let batch = []; // [{ country, text }]
|
||||
let flushTimer = null;
|
||||
|
||||
let hostList = []
|
||||
//查询列表轮询
|
||||
let getListtimer = null;
|
||||
let userdata = getUser();
|
||||
let chatList = ref([])
|
||||
|
||||
let MesNewList = ref([{
|
||||
sender: 'Alice', // 或 name/user/from
|
||||
device: 'iPhone 14 Pro', // 或 deviceName/udid
|
||||
text: '你好呀~', // 或 content
|
||||
type: 'msg' // 可选;有些是 'time' 会被过滤
|
||||
}, {
|
||||
sender: 'Alice', // 或 name/user/from
|
||||
device: 'iPhone 14 Pro', // 或 deviceName/udid
|
||||
text: '你好呀~', // 或 content
|
||||
type: 'msg' // 可选;有些是 'time' 会被过滤
|
||||
}, {
|
||||
sender: 'Alice', // 或 name/user/from
|
||||
device: 'iPhone 14 Pro', // 或 deviceName/udid
|
||||
text: '你好呀~', // 或 content
|
||||
type: 'msg' // 可选;有些是 'time' 会被过滤
|
||||
}, {
|
||||
sender: 'Alice', // 或 name/user/from
|
||||
device: 'iPhone 14 Pro', // 或 deviceName/udid
|
||||
text: '你好呀~', // 或 content
|
||||
type: 'msg' // 可选;有些是 'time' 会被过滤
|
||||
}, {
|
||||
sender: 'Alice', // 或 name/user/from
|
||||
device: 'iPhone 14 Pro', // 或 deviceName/udid
|
||||
text: '你好呀~', // 或 content
|
||||
type: 'msg' // 可选;有些是 'time' 会被过滤
|
||||
}, {
|
||||
sender: 'Alice', // 或 name/user/from
|
||||
device: 'iPhone 14 Pro', // 或 deviceName/udid
|
||||
text: '你好呀~', // 或 content
|
||||
type: 'msg' // 可选;有些是 'time' 会被过滤
|
||||
}, {
|
||||
sender: 'Alice', // 或 name/user/from
|
||||
device: 'iPhone 14 Pro', // 或 deviceName/udid
|
||||
text: '你好呀~', // 或 content
|
||||
type: 'msg' // 可选;有些是 'time' 会被过滤
|
||||
}, {
|
||||
sender: 'Alice', // 或 name/user/from
|
||||
device: 'iPhone 14 Pro', // 或 deviceName/udid
|
||||
text: '你好呀~', // 或 content
|
||||
type: 'msg' // 可选;有些是 'time' 会被过滤
|
||||
}, {
|
||||
sender: 'Alice', // 或 name/user/from
|
||||
device: 'iPhone 14 Pro', // 或 deviceName/udid
|
||||
text: '你好呀~', // 或 content
|
||||
type: 'msg' // 可选;有些是 'time' 会被过滤
|
||||
},])
|
||||
let isImg = ref(true)
|
||||
let MesNewList = ref([])
|
||||
|
||||
// 引入刷新方法
|
||||
const reload = inject("reload")
|
||||
const reloadImg = () => {
|
||||
isImg.value = false
|
||||
setTimeout(() => {
|
||||
isImg.value = true
|
||||
}, 1000)
|
||||
refreshAllImgs()
|
||||
}
|
||||
//start弹窗
|
||||
let isMsgPop = ref(false)
|
||||
@@ -252,6 +225,12 @@ let stopLoading = null
|
||||
// 当前是否被其它模式占用(四个互斥按钮专用)
|
||||
const isLocked = (type) => !!runType.value && runType.value !== type
|
||||
|
||||
// —— SSE 弹窗/接收总开关(持久化)——
|
||||
const sseEnabled = ref(JSON.parse(localStorage.getItem('SSE_ENABLED') ?? 'true'))
|
||||
watch(sseEnabled, v => {
|
||||
localStorage.setItem('SSE_ENABLED', JSON.stringify(v))
|
||||
})
|
||||
|
||||
// 互斥按钮的样式:激活=红,锁定=半透明且禁点
|
||||
const ctrlStyle = (type) => ({
|
||||
backgroundColor: runType.value === type ? 'red' : '',
|
||||
@@ -421,7 +400,9 @@ const buttons = [
|
||||
{
|
||||
label: '登出',
|
||||
onClick: () => {
|
||||
refreshAllStopImgs() // 停止所有视频流
|
||||
logout({ userId: userdata.id, tenantId: userdata.tenantId })
|
||||
|
||||
router.push('/')
|
||||
|
||||
},
|
||||
@@ -502,6 +483,51 @@ function saveSchedule() {
|
||||
|
||||
const selectedDevice = ref(null)
|
||||
|
||||
|
||||
// 每台设备的 <img> 引用
|
||||
const imgRefs = ref({}) // { [id]: HTMLImageElement }
|
||||
const imgTs = ref({}) // { [id]: number } // cache-bust
|
||||
const imgKeyTick = ref(0) // 强制重建 <img>
|
||||
|
||||
function refreshOneImg(id, port) {
|
||||
const el = imgRefs.value[id]
|
||||
console.log("终止", id)
|
||||
|
||||
if (el) {
|
||||
// 硬中断旧请求
|
||||
el.src = ''
|
||||
el.removeAttribute('src')
|
||||
}
|
||||
// 下一拍再重建
|
||||
nextTick(() => {
|
||||
imgTs.value[id] = Date.now()
|
||||
imgKeyTick.value++ // 触发 key 变化 -> 销毁并新建 <img>
|
||||
})
|
||||
}
|
||||
|
||||
// —— 关键:登出/离开时“批量硬中断”所有图片流 ——
|
||||
async function hardStopAllImgStreams(id, port) {
|
||||
const el = imgRefs.value[id]
|
||||
console.log("终止", id)
|
||||
|
||||
if (el) {
|
||||
// 硬中断旧请求
|
||||
el.src = ''
|
||||
el.removeAttribute('src')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function refreshAllImgs() {
|
||||
Object.keys(imgRefs.value).forEach(id => refreshOneImg(id))
|
||||
}
|
||||
function refreshAllStopImgs() {
|
||||
Object.keys(imgRefs.value).forEach(id => hardStopAllImgStreams(id))
|
||||
}
|
||||
|
||||
|
||||
// —— 显示尺寸固定为 320x720;未选中缩略为 THUMB_SCALE 倍 ——
|
||||
// 尺寸与排布
|
||||
const BASE_W = 320
|
||||
@@ -725,29 +751,25 @@ async function stopAll() {
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
});
|
||||
// if (!runType.value) return
|
||||
// 所有操作完成后执行以下代码
|
||||
scheduleEnabled.value = false
|
||||
runType.value = ''
|
||||
batchMode.value = 'init' // 初始化状态 关注状态
|
||||
isMsgPop.value = false; //弹窗状态(不是弹窗)
|
||||
dropCurrentWave() // 丢弃当前波的残留缓冲
|
||||
|
||||
try {
|
||||
// 使用 Promise.all 并行处理所有设备的停止操作
|
||||
await Promise.all(deviceInformation.value.map(async (item) => {
|
||||
try {
|
||||
const res = await stopScript({ udid: item.deviceId })
|
||||
console.log(`停止成功:${item.deviceId}`, res, printCurrentTime())
|
||||
ElMessage.success(`停止成功:${item.deviceId}`)
|
||||
stopLoading.close()
|
||||
} catch (error) {
|
||||
console.log(`停止失败`, printCurrentTime())
|
||||
ElMessage.error(`脚本已停止`)
|
||||
stopLoading.close()
|
||||
}
|
||||
}))
|
||||
|
||||
// 所有操作完成后执行以下代码
|
||||
scheduleEnabled.value = false
|
||||
runType.value = ''
|
||||
batchMode.value = 'init'
|
||||
const res = await stopAllTask(deviceInformation.value.map((item) => item.deviceId))
|
||||
console.log(`全部停止成功`, printCurrentTime())
|
||||
ElMessage.success(`全部停止成功`)
|
||||
stopLoading.close()
|
||||
} catch (error) {
|
||||
console.error('批量停止过程中发生错误:', error)
|
||||
console.log(`停止失败`, printCurrentTime())
|
||||
ElMessage.error(`脚本已停止`)
|
||||
stopLoading.close()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
//确认多行文本框内容
|
||||
@@ -763,12 +785,15 @@ function onDialogConfirm(result, type, index, isMon) {
|
||||
} else if (type == '私信') {
|
||||
runType.value = 'follow'
|
||||
setContentpriList(result)
|
||||
console.log('hostList', hostList)
|
||||
passAnchorData(
|
||||
{
|
||||
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||||
anchorList: hostList.map(item => ({
|
||||
anchorId: item.id,
|
||||
country: item.country
|
||||
country: item.country,
|
||||
invitationType: item.invitationType,
|
||||
state: stateByInvType(item.invitationType),
|
||||
})),
|
||||
prologueList: result,
|
||||
needReply: isMon
|
||||
@@ -783,7 +808,7 @@ function onDialogConfirm(result, type, index, isMon) {
|
||||
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: 'Loading',
|
||||
@@ -795,14 +820,16 @@ onMounted(() => {
|
||||
getListtimer = setInterval(() => {
|
||||
loading.close();
|
||||
getDeviceListFun()
|
||||
selectLastFun()
|
||||
|
||||
|
||||
}, 3000)
|
||||
if (!await isAiConfig()) {
|
||||
showMyInfo.value = true
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 批次缓冲(仅用于当前“波”)
|
||||
let batch = []; // [{ country, text }]
|
||||
let flushTimer = null;
|
||||
|
||||
function scheduleFlush(handler, delay = 400) {
|
||||
if (flushTimer) clearTimeout(flushTimer);
|
||||
flushTimer = setTimeout(() => {
|
||||
@@ -821,11 +848,26 @@ onMounted(() => {
|
||||
|
||||
// —— SSE 接收 ——
|
||||
const es = connectSSE('http://localhost:3312/events', (data) => {
|
||||
console.log('来自服务端:', data);
|
||||
// console.log('来自服务端:', data);
|
||||
// console.log(1)
|
||||
//总开关
|
||||
// 总开关关闭:不弹窗、丢弃所有消息
|
||||
if (!sseEnabled.value) {
|
||||
if (data === 'start') {
|
||||
// 开始一个新波时顺便清空(保险起见)
|
||||
dropCurrentWave()
|
||||
}
|
||||
return // 其余任何数据也直接丢弃
|
||||
}
|
||||
|
||||
|
||||
if (data === 'start') {
|
||||
console.log(2)
|
||||
|
||||
// 新一波开始:根据当前状态决定“本波 flush 用谁”
|
||||
if (!isMsgPop.value) {
|
||||
console.log(3)
|
||||
|
||||
// 还没开始过 -> 首次弹框,确认后使用处理本波
|
||||
isMsgPop.value = true;
|
||||
batchMode.value = 'init';
|
||||
@@ -841,7 +883,8 @@ onMounted(() => {
|
||||
// 不在这里立刻提交;让后续主播数据先进 batch,再由防抖统一 flush
|
||||
// 不直接发;把这“一波”的主播先塞进 hostList,然后弹出“私信”输入框
|
||||
scheduleFlush((items) => {
|
||||
hostList = (items || []).map(h => ({ id: h.text, country: h.country || '' }))
|
||||
//1普票 2金票 invitationType
|
||||
hostList = (items || []).map(h => ({ id: h.text, country: h.country || '', invitationType: h.invitationType }))
|
||||
|
||||
})
|
||||
setTimeout(() => {
|
||||
@@ -859,19 +902,27 @@ onMounted(() => {
|
||||
batchMode.value = 'follow';
|
||||
// 立刻安排一次“尾随防抖”flush,等本波数据齐了再送
|
||||
scheduleFlush((items) => {
|
||||
addTempAnchorData(items.map(h => ({ anchorId: h.text, country: h.country || '' })));
|
||||
// 这里 items 元素是 h
|
||||
const list = items.map(h => ({
|
||||
anchorId: h.text,
|
||||
country: h.country || '',
|
||||
invitationType: h.invitationType,
|
||||
state: stateByInvType(h.invitationType)
|
||||
}))
|
||||
addTempAnchorData(list)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 非 start:本波主播数据进入批次
|
||||
const country = data && data.country != null ? data.country : '';
|
||||
const text = data && (data.hostsId != null ? data.hostsId : data.text);
|
||||
const invitationType = data && (data.invitationType != null ? data.invitationType : '');
|
||||
if (text == null) {
|
||||
// 数据格式不对,丢弃或打印
|
||||
console.warn('[SSE] 非法数据,缺少 hostsId/text:', data);
|
||||
return;
|
||||
}
|
||||
batch.push({ country, text });
|
||||
batch.push({ country, text, invitationType });
|
||||
|
||||
// 根据当前模式,刷新防抖(让“最后一条到来后”延迟几百毫秒再统一提交)
|
||||
if (batchMode.value === 'init') {
|
||||
@@ -881,7 +932,7 @@ onMounted(() => {
|
||||
if (runType.value === 'follow') {
|
||||
passAnchorData({
|
||||
deviceList: deviceInformation.value.map(item => item.deviceId),
|
||||
anchorList: items.map(h => ({ anchorId: h.text, country: h.country || '' })),
|
||||
anchorList: items.map(h => ({ anchorId: h.text, country: h.country || '', invitationType: h.invitationType, state: stateByInvType(h.invitationType) })),
|
||||
needReply: false
|
||||
});
|
||||
} else {
|
||||
@@ -892,7 +943,14 @@ onMounted(() => {
|
||||
} else {
|
||||
// 已在关注:走追加逻辑
|
||||
scheduleFlush((items) => {
|
||||
addTempAnchorData(items.map(h => ({ anchorId: h.text, country: h.country || '' })));
|
||||
// 这里 items 元素是 h
|
||||
const list = items.map(h => ({
|
||||
anchorId: h.text,
|
||||
country: h.country || '',
|
||||
invitationType: h.invitationType,
|
||||
state: stateByInvType(h.invitationType)
|
||||
}))
|
||||
addTempAnchorData(list)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -909,16 +967,18 @@ onUnmounted(() => {
|
||||
let isStartLac = false
|
||||
const getDeviceListFun = () => {
|
||||
getDeviceList().then((res) => {
|
||||
console.log('返回', res.length)
|
||||
// console.log('返回', res.length)
|
||||
if (res && res.length > 0 && deviceInformation.value.length !== res.length) {
|
||||
console.log("设备变更")
|
||||
deviceInformation.value = res
|
||||
reloadImg()
|
||||
// refreshAllStopImgs()
|
||||
// reloadImg()
|
||||
}
|
||||
|
||||
if (res.length == 0) {
|
||||
deviceInformation.value = []
|
||||
reloadImg()
|
||||
// refreshAllStopImgs()
|
||||
// reloadImg()
|
||||
}
|
||||
// deviceInformation.value = ['', '', '', '', '', '',]
|
||||
}).catch((err) => {
|
||||
@@ -932,6 +992,27 @@ const getDeviceListFun = () => {
|
||||
})
|
||||
}
|
||||
|
||||
//获取新消息
|
||||
const selectLastFun = () => {
|
||||
selectLast().then((res) => {
|
||||
// console.log("返回", deviceInformation, res)
|
||||
let mesInfoData = res
|
||||
mesInfoData.forEach(element => {
|
||||
deviceInformation.value.forEach((item, index) => {
|
||||
console.log(res)
|
||||
console.log(item.deviceId == element.device)
|
||||
if (item.deviceId == element.device) {
|
||||
element.device = index + 1 + '号设备'
|
||||
console.log(element.device)
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// console.log('============', mesInfoData)
|
||||
MesNewList.value = mesInfoData
|
||||
})
|
||||
}
|
||||
|
||||
async function uploadLogFile() {
|
||||
let loading = null
|
||||
try {
|
||||
@@ -1156,6 +1237,50 @@ function onSave(payload) {
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function isAiConfig() {
|
||||
const res = await window.electronAPI.fileExists("resources/iOSAI/data/aiConfig.json");
|
||||
console.log(res);
|
||||
return res.exists;
|
||||
}
|
||||
|
||||
//金票
|
||||
const gold = ref(true) // ON=执行
|
||||
//普票
|
||||
const ordinary = ref(true) // ON=执行
|
||||
|
||||
|
||||
function invitTypeFun(invitType, nextEnabled) {
|
||||
// 子组件已传来“切换后的布尔值”
|
||||
if (invitType === 'gold') {
|
||||
gold.value = nextEnabled
|
||||
} else if (invitType === 'ordinary') {
|
||||
ordinary.value = nextEnabled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** 由邀请类型 + 当前全局开关,得到 state(0/1) */
|
||||
function stateByInvType(invitationType) {
|
||||
// 2=金票,其他=普票(按你的写法)
|
||||
const enabled = invitationType == 2 ? gold.value : ordinary.value
|
||||
return enabled ? 1 : 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//清空当前批次的小工具
|
||||
function dropCurrentWave() {
|
||||
// 丢弃当前这“波”已缓冲的数据 & 取消待 flush
|
||||
batch.length = 0
|
||||
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null }
|
||||
// 复位本波的弹窗与模式标记
|
||||
isMsgPop.value = false
|
||||
batchMode.value = 'init'
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
Reference in New Issue
Block a user