From f0c725449d01cc6f705446be2da391767efe8127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=A1=E5=A4=8D=E4=B9=A0?= <2353956224@qq.com> Date: Tue, 18 Nov 2025 18:05:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=86=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 10 +- src/composables/useDevices.js | 105 +++++ src/composables/useNetStatus.js | 0 src/composables/useSSEAnchors.js | 0 src/composables/useSchedule.js | 0 src/composables/useScreenStreams.js | 255 ++++++++++++ src/views/HomeView.vue | 2 +- src/views/VideoStream.vue | 579 +++++++--------------------- 8 files changed, 505 insertions(+), 446 deletions(-) create mode 100644 src/composables/useDevices.js create mode 100644 src/composables/useNetStatus.js create mode 100644 src/composables/useSSEAnchors.js create mode 100644 src/composables/useSchedule.js create mode 100644 src/composables/useScreenStreams.js diff --git a/.env.development b/.env.development index 025ed70..945ed78 100644 --- a/.env.development +++ b/.env.development @@ -1,11 +1,11 @@ # iOS 控制服务 -# VUE_APP_BASE_LOCAL=http://192.168.1.218:34567/ -VUE_APP_BASE_LOCAL=http://127.0.0.1:34567/ -# VUE_APP_BASE_LOCAL=http://192.168.1.209:34567/ +VUE_APP_BASE_LOCAL=https://192.168.1.231:34567/ +# VUE_APP_BASE_LOCAL=https://127.0.0.1:34567/ +# VUE_APP_BASE_LOCAL=https://192.168.1.209:34567/ # 业务后端(开发用内网地址) -# VUE_APP_BASE_REMOTE=https://crawlclient.api.yolozs.com -VUE_APP_BASE_REMOTE=http://192.168.1.144:8101/ +VUE_APP_BASE_REMOTE=https://crawlclient.api.yolozs.com +# VUE_APP_BASE_REMOTE=http://192.168.1.144:8101/ # AI 服务 VUE_APP_BASE_SPECIAL=https://ai.yolozs.com \ No newline at end of file diff --git a/src/composables/useDevices.js b/src/composables/useDevices.js new file mode 100644 index 0000000..662a0ad --- /dev/null +++ b/src/composables/useDevices.js @@ -0,0 +1,105 @@ +// src/composables/useDevices.js +import { ref } from 'vue' +import { ElMessage } from 'element-plus' +import { + getDeviceList, + stopScript, + deviceAppList, + launchApp, +} from '@/api/ios' +import { pickTikTokBundleId } from '@/utils/arrUtils' + +export function useDevices() { + const deviceInformation = ref([]) // 设备列表 + const getListTimer = ref(null) // 定时器句柄(方便在外面清理) + + // 是否已经提示过 IOSAI 服务错误 + let isStartLac = false + + // 拉设备列表 + const getDeviceListFun = async () => { + try { + const res = await getDeviceList() + if (res && res.length > 0 && deviceInformation.value.length !== res.length) { + console.log('设备变更') + deviceInformation.value = res + } + if (!res || res.length === 0) { + deviceInformation.value = [] + } + } catch (err) { + if (isStartLac) { + ElMessage.error('IOSAI 服务错误') + } else { + // 第一次忽略,等下次再报(保持你原来的行为) + isStartLac = true + } + } + } + + // 打开 Tiktok + const openTk = async () => { + if (!deviceInformation.value?.length) { + ElMessage.warning('暂无在线设备') + return + } + + const { ElLoading } = await import('element-plus') + const loading = ElLoading.service({ + text: '正在打开 TikTok …', + background: 'rgba(0,0,0,.35)' + }) + + const results = [] + + try { + for (const dev of deviceInformation.value) { + const udid = dev.deviceId + try { + const apps = await deviceAppList({ udid }) + const bundleId = pickTikTokBundleId(apps) + + if (!bundleId) { + results.push({ udid, ok: false, msg: '未找到 TikTok' }) + continue + } + + await launchApp({ udid, bundleId }) + results.push({ udid, ok: true, msg: `已启动 TikTok (${bundleId})` }) + } catch (e) { + console.error('openTk error', udid, e) + results.push({ udid, ok: false, msg: '请求失败' }) + } + } + } finally { + loading.close() + } + + const okCount = results.filter(r => r.ok).length + const fail = results.filter(r => !r.ok) + if (okCount) ElMessage.success(`已在 ${okCount} 台设备启动 TikTok`) + if (fail.length) { + const udids = fail.map(f => f.udid).join(', ') + ElMessage.error(`以下设备未能启动:${udids}`) + } + } + + // 停止单台任务 + const stopOne = async (deviceId) => { + try { + await stopScript({ udid: deviceId }) + // 你原来这里没有提示,我也保持不弹 + // ElMessage.success('停止成功') + } catch (e) { + console.error('stopOne error', e) + } + } + + return { + deviceInformation, + getDeviceListFun, + getListTimer, + openTk, + stopOne, + } +} diff --git a/src/composables/useNetStatus.js b/src/composables/useNetStatus.js new file mode 100644 index 0000000..e69de29 diff --git a/src/composables/useSSEAnchors.js b/src/composables/useSSEAnchors.js new file mode 100644 index 0000000..e69de29 diff --git a/src/composables/useSchedule.js b/src/composables/useSchedule.js new file mode 100644 index 0000000..e69de29 diff --git a/src/composables/useScreenStreams.js b/src/composables/useScreenStreams.js new file mode 100644 index 0000000..4ca4459 --- /dev/null +++ b/src/composables/useScreenStreams.js @@ -0,0 +1,255 @@ +// src/composables/useScreenStreams.js +import { ref, reactive, computed, watch } from 'vue' +import { tapAction, swipeAction } from '@/api/ios' + +const BASE_W = 320 +const BASE_H = 720 +const THUMB_SCALE = 0.6 +const PER_ROW = 3 +const BOTTOM_SHIFT = Math.round(BASE_H * (1 - THUMB_SCALE)) // 288 + +export function useScreenStreams(deviceInformation) { + // 选中的设备索引 + const selectedDevice = ref(null) + + // 每台设备的 引用 + const imgRefs = ref({}) // { [id]: HTMLImageElement } + + // 每台设备当前展示的 URL + const imgSrcMap = reactive({}) // { [deviceId]: string } + + // 每台设备循环状态 + const loops = new Map() // deviceId -> { timer, stopped } + + const makeUrl = (port) => `http://192.168.1.231:${port}/?t=${Date.now()}` + // const makeUrl = (port) => `http://localhost:${port}/?t=${Date.now()}` + + function hardCloseImg(deviceId) { + const el = imgRefs.value[deviceId] + if (el) { + el.src = '' + el.removeAttribute('src') + } + imgSrcMap[deviceId] = '' + } + + function stopLoop(deviceId) { + const s = loops.get(deviceId) + if (!s) { + hardCloseImg(deviceId) + return + } + s.stopped = true + if (s.timer) clearTimeout(s.timer) + loops.delete(deviceId) + hardCloseImg(deviceId) + } + + function startLoop(dev, idx = 0) { + stopLoop(dev.deviceId) + const state = { timer: null, stopped: false } + loops.set(dev.deviceId, state) + + const tick = () => { + if (state.stopped) return + const url = makeUrl(dev.screenPort) + imgSrcMap[dev.deviceId] = url + state.timer = window.setTimeout(tick, 3000) + } + + state.timer = window.setTimeout(tick, idx * 200) + } + + function reconcileLoopsByDevices(list) { + const keep = new Set(list.map(d => d.deviceId)) + + // 停掉没了的设备 + for (const id of Array.from(loops.keys())) { + if (!keep.has(id)) stopLoop(id) + } + // 为新增设备启动循环 + list.forEach((d, i) => { + if (!loops.has(d.deviceId)) startLoop(d, i) + }) + } + + function refreshOneImg(deviceId) { + const dev = deviceInformation.value.find(d => d.deviceId === deviceId) + if (dev) { + stopLoop(deviceId) + startLoop(dev, 0) + } + } + + function refreshAllImgs() { + deviceInformation.value.forEach((d, i) => { + stopLoop(d.deviceId) + startLoop(d, i) + }) + } + + function refreshAllStopImgs() { + Object.keys(imgRefs.value).forEach(id => hardCloseImg(id)) + } + + // 设备变动时自动对齐循环 + watch(deviceInformation, (list) => { + reconcileLoopsByDevices(list || []) + }, { deep: true }) + + // ——— 选中 + 缩放/上移样式 ——— + const hasTwoRows = computed(() => deviceInformation.value.length > PER_ROW) + + const isBottomRow = (index) => { + if (!hasTwoRows.value) return false + const lastRow = Math.floor((deviceInformation.value.length - 1) / PER_ROW) + return Math.floor(index / PER_ROW) === lastRow + } + + function getCanvasStyle(index) { + const isSelected = selectedDevice.value === index + if (!isSelected) { + return { transform: `scale(${THUMB_SCALE})` } + } + return isBottomRow(index) + ? { transform: `translateY(-${BOTTOM_SHIFT}px) scale(1)` } + : { transform: 'scale(1)' } + } + + const imgWH = (index) => { + const scale = (selectedDevice.value === index) ? 1 : THUMB_SCALE + return { + width: `${BASE_W * scale}px`, + height: `${BASE_H * scale}px`, + transition: 'all 0.3s ease', + } + } + + const displaySize = (index) => { + const scale = (selectedDevice.value === index) ? 1 : THUMB_SCALE + return { w: BASE_W * scale, h: BASE_H * scale } + } + + const selectDevice = (index) => { + selectedDevice.value = index + } + + // ——— 坐标映射 + 鼠标交互 ——— + const dragState = ref({}) // index -> { ox, oy, t, udid, ... } + + const mapToDeviceXY = (index, offsetX, offsetY) => { + const dev = deviceInformation.value[index] || {} + const realW = Number(dev.width) || BASE_W + const realH = Number(dev.height) || BASE_H + const rotation = Number(dev.rotation || 0) + + const { w: dispW, h: dispH } = displaySize(index) + let nx = Math.min(Math.max(offsetX / dispW, 0), 1) + let ny = Math.min(Math.max(offsetY / dispH, 0), 1) + + let x, y + switch (rotation % 360) { + case 90: + case -270: + x = Math.round(ny * realW) + y = Math.round((1 - nx) * realH) + break + case 180: + case -180: + x = Math.round((1 - nx) * realW) + y = Math.round((1 - ny) * realH) + break + case 270: + case -90: + x = Math.round((1 - ny) * realW) + y = Math.round(nx * realH) + break + default: + x = Math.round(nx * realW) + y = Math.round(ny * realH) + } + return { x, y } + } + + const onCanvasDown = (udid, e, index) => { + const startDev = mapToDeviceXY(index, e.offsetX, e.offsetY) + dragState.value[index] = { + ox: e.offsetX, + oy: e.offsetY, + t: Date.now(), + udid, + startDevXY: startDev, + startOffsetXY: { x: e.offsetX, y: e.offsetY } + } + } + + const onCanvasMove = (udid, e, index) => { + const st = dragState.value[index] + if (!st) return + // const curDev = mapToDeviceXY(index, e.offsetX, e.offsetY) + // 调试需要再打印 + } + + const onCanvasUp = async (udid, e, index) => { + const st = dragState.value[index] + if (!st) return + + const { ox, oy, t, startDevXY, startOffsetXY } = st + const dx = e.offsetX - ox + const dy = e.offsetY - oy + const elapsed = Date.now() - t + delete dragState.value[index] + + const endDevXY = mapToDeviceXY(index, e.offsetX, e.offsetY) + const endOffsetXY = { x: e.offsetX, y: e.offsetY } + + console.log('[鼠标滑动,起点/终点)+ 耗时]', { + udid, + start: { + offsetXY: startOffsetXY, + deviceXY: startDevXY + }, + end: { + offsetXY: endOffsetXY, + deviceXY: endDevXY + }, + deltaOffset: { dx, dy }, + durationMs: elapsed, + }) + + const MOVE_THR = 5 + const isTap = Math.hypot(dx, dy) < MOVE_THR && elapsed < 500 + + try { + if (isTap) { + await tapAction({ udid, x: endDevXY.x, y: endDevXY.y }) + } else { + await swipeAction({ + udid, + sx: startDevXY.x, + sy: startDevXY.y, + ex: endDevXY.x, + ey: endDevXY.y, + duration: elapsed / 1000 + }) + } + } catch (err) { + console.error(err) + } + } + + return { + selectedDevice, + imgRefs, + imgSrcMap, + refreshAllImgs, + refreshOneImg, + refreshAllStopImgs, + getCanvasStyle, + imgWH, + selectDevice, + onCanvasDown, + onCanvasMove, + onCanvasUp, + } +} diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index ce36bbc..9cd0662 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -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.9.1'); +let version = ref('3.0.0'); onMounted(() => { diff --git a/src/views/VideoStream.vue b/src/views/VideoStream.vue index 08349cd..3ffc195 100644 --- a/src/views/VideoStream.vue +++ b/src/views/VideoStream.vue @@ -19,9 +19,16 @@
执行主播库 上传日志 - - +
+ + + + + + +
@@ -125,13 +132,13 @@