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