爬虫联动弹窗版本备份

This commit is contained in:
2025-09-20 13:31:06 +08:00
parent 515cbab7c3
commit 7bfe1b744a
11 changed files with 584 additions and 193 deletions

View File

@@ -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">