From ece55949f88d704d044e3722607de0f07139f818 Mon Sep 17 00:00:00 2001 From: liuhaodong Date: Thu, 20 Aug 2026 14:45:05 +0800 Subject: [PATCH] feat: connect device, task, and live views Wire the frontend to device management, task execution, video downloads, and live session controls. --- src/api/live.js | 17 +++ src/api/request.js | 4 +- src/components/LivePlayer.vue | 27 +++++ src/styles/prototype.css | 12 +- src/views/Devices.vue | 15 +++ src/views/Media.vue | 20 +++- src/views/Monitor.vue | 75 +++++++++++- src/views/Tasks.vue | 207 ++++++++++++++++++++++++++++------ 8 files changed, 337 insertions(+), 40 deletions(-) create mode 100644 src/api/live.js create mode 100644 src/components/LivePlayer.vue diff --git a/src/api/live.js b/src/api/live.js new file mode 100644 index 0000000..e7e789c --- /dev/null +++ b/src/api/live.js @@ -0,0 +1,17 @@ +import request from './request' + +export function joinLive(dockId, payload = {}) { + return request.post(`/v1/live/${dockId}/sessions`, payload) +} + +export function getLivePlayURL(dockId) { + return request.get(`/v1/live/${dockId}/play-url`) +} + +export function heartbeatLive(dockId, sessionId) { + return request.post(`/v1/live/${dockId}/sessions/${sessionId}/heartbeat`) +} + +export function leaveLive(dockId, sessionId) { + return request.delete(`/v1/live/${dockId}/sessions/${sessionId}/viewers/me`) +} diff --git a/src/api/request.js b/src/api/request.js index 79f6090..800152d 100644 --- a/src/api/request.js +++ b/src/api/request.js @@ -36,7 +36,9 @@ request.interceptors.response.use( } else if (status === 403) { useUiStore().toast(data?.msg || '无权限访问') } else { - useUiStore().toast(data?.msg || error.message || '网络错误') + const message = data?.msg || error.message || '网络错误' + useUiStore().toast(message) + return Promise.reject(new Error(message)) } return Promise.reject(error) } diff --git a/src/components/LivePlayer.vue b/src/components/LivePlayer.vue new file mode 100644 index 0000000..237c602 --- /dev/null +++ b/src/components/LivePlayer.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/styles/prototype.css b/src/styles/prototype.css index 34c36f7..8f89650 100644 --- a/src/styles/prototype.css +++ b/src/styles/prototype.css @@ -422,7 +422,17 @@ svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: .task-schedule-fields[hidden] { display: none; }.task-schedule-fields { margin-top: 2px; } .weekday-picker { height: 38px; padding: 3px; display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 3px; border-radius: 4px; background: #edf1f4; } .weekday-picker button { min-width: 0; border-radius: 3px; color: #65737f; background: transparent; font-size: 11px; }.weekday-picker button:hover { color: #2869aa; }.weekday-picker button.active { color: #fff; background: #2c72bd; box-shadow: 0 1px 4px rgba(23,66,108,.18); } -.task-video-settings { margin: 3px 0 12px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } +.task-dialog .mode-segment { grid-template-columns: repeat(4, minmax(0, 1fr)); } +.task-dialog .mode-segment button { min-width: 0; padding: 0 4px; white-space: nowrap; } +.task-dialog .task-schedule-settings { margin-bottom: 3px; } +.task-dialog .task-schedule-settings > label:last-child { margin-bottom: 12px; } +.task-dialog .task-schedule-settings label small { color: var(--muted); font-size: 10px; line-height: 1.4; } +.task-dialog .task-schedule-settings input[type='text'] { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } +@media (max-width: 560px) { + .task-dialog .mode-segment { grid-template-columns: repeat(2, minmax(0, 1fr)); height: auto; min-height: 72px; } + .task-dialog .mode-segment button { min-height: 30px; } +} + .task-video-title { height: 35px; display: flex; align-items: center; justify-content: space-between; }.task-video-title strong { font-size: 12px; }.task-video-title span { color: var(--muted); font-size: 10px; } .add-dock-dialog .task-video-option { min-height: 48px; margin: 0; padding: 7px 2px; display: grid; grid-template-columns: 1fr 34px; align-items: center; gap: 10px; border-top: 1px solid #edf0f2; cursor: pointer; } .task-video-option > span { min-width: 0; display: flex; flex-direction: column; gap: 3px; }.task-video-option strong { color: #3c4d59; font-size: 11px; }.add-dock-dialog .task-video-option small { color: var(--muted); font-size: 10px; } diff --git a/src/views/Devices.vue b/src/views/Devices.vue index 2d4503a..b108da8 100644 --- a/src/views/Devices.vue +++ b/src/views/Devices.vue @@ -88,6 +88,7 @@ + @@ -184,6 +185,7 @@ @@ -403,6 +405,19 @@ function openEditDock(dock) { editDockVisible.value = true } +async function deleteDock(dock) { + const ok = await ui.confirm(`确认硬删除机巢「${dock.name}」?`, '删除后机巢及其关联设备数据将永久移除,且无法恢复。') + if (!ok) return + try { + await request.delete(`/v1/docks/${dock.id}`) + drawerOpen.value = false + ui.toast('机巢已永久删除') + await devices.load() + } catch (e) { + ui.toast(e.message || '删除机巢失败') + } +} + async function handleDockSaved() { editDockVisible.value = false try { diff --git a/src/views/Media.vue b/src/views/Media.vue index d30dda3..c763543 100644 --- a/src/views/Media.vue +++ b/src/views/Media.vue @@ -26,7 +26,7 @@
LIVE - + {{ s.source }}
@@ -108,8 +108,9 @@ async function load() { request.get('/v1/live', { params: { pageNum: 1, pageSize: 100 } }), request.get('/v1/videos', { params: { pageNum: 1, pageSize: 100 } }) ]) - liveStreams.value = (livePage?.records || []).map((s) => ({ + liveStreams.value = (livePage?.records || []).filter((s) => ['starting', 'streaming', 'reconnecting'].includes(s.phase)).map((s) => ({ id: s.id, + dockId: s.dockId, name: s.dockId, desc: s.streamName ? `${s.streamName} · ${s.provider}` : s.provider, source: s.streamName || '--', @@ -129,6 +130,21 @@ async function load() { onMounted(load) +async function openLive(stream) { + try { + const result = await request.post(`/v1/live/${stream.dockId}/sessions`) + if (result?.session?.phase === 'streaming') { + const play = await request.get(`/v1/live/${stream.dockId}/play-url`) + if (play?.playUrl && !play.playUrl.startsWith('fake://')) window.open(play.playUrl, '_blank') + else ui.toast('控制面已验证,当前未配置可播放媒体流') + } else { + ui.toast('直播正在启动,请稍后重试') + } + } catch (e) { + ui.toast(e.message || '打开直播失败') + } +} + async function download(v) { try { const res = await request.post(`/v1/videos/${v.id}/download`) diff --git a/src/views/Monitor.vue b/src/views/Monitor.vue index f7ec6a9..028b26a 100644 --- a/src/views/Monitor.vue +++ b/src/views/Monitor.vue @@ -130,10 +130,12 @@

实时画面

- 暂无视频流 + {{ livePhaseText }}
-
- 暂无实时视频数据 + +
+ +
@@ -244,12 +246,14 @@