feat: add multi-tab support to remote browser
This commit is contained in:
@@ -397,11 +397,21 @@ export const customPagesApi = {
|
||||
}
|
||||
|
||||
// ——— Remote browser sessions ———
|
||||
export interface BrowserTabData {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
created_at: number
|
||||
}
|
||||
|
||||
export interface BrowserSessionData {
|
||||
id: string
|
||||
custom_page_id: number
|
||||
url: string
|
||||
title: string
|
||||
active_tab_id?: string
|
||||
tabs?: BrowserTabData[]
|
||||
tab_revision?: number
|
||||
}
|
||||
|
||||
export type BrowserEventPayload =
|
||||
@@ -418,6 +428,10 @@ export const browserSessionsApi = {
|
||||
get: (id: string) => api.get<BrowserSessionData>(`/api/browser-sessions/${id}`),
|
||||
event: (id: string, data: BrowserEventPayload) =>
|
||||
api.post<BrowserSessionData>(`/api/browser-sessions/${id}/events`, data),
|
||||
activateTab: (id: string, tabId: string) =>
|
||||
api.post<BrowserSessionData>(`/api/browser-sessions/${id}/tabs/${tabId}/activate`),
|
||||
closeTab: (id: string, tabId: string) =>
|
||||
api.delete<BrowserSessionData>(`/api/browser-sessions/${id}/tabs/${tabId}`),
|
||||
selection: (id: string) => api.get<{ text: string }>(`/api/browser-sessions/${id}/selection`),
|
||||
clipboard: (id: string) => api.get<{ text?: string; error?: string }>(`/api/browser-sessions/${id}/clipboard`),
|
||||
close: (id: string) => api.delete(`/api/browser-sessions/${id}`),
|
||||
|
||||
@@ -64,6 +64,26 @@
|
||||
<div class="viewer-body">
|
||||
<div v-if="isRemoteBrowser" class="viewer-content viewer-content-remote">
|
||||
<div class="viewer-stage" :class="{ 'viewer-stage-error': showRemoteError }">
|
||||
<!-- Multi-tab bar -->
|
||||
<div v-if="!showRemoteError && remoteSession?.tabs?.length" class="remote-tabs">
|
||||
<div
|
||||
v-for="tab in remoteSession.tabs"
|
||||
:key="tab.id"
|
||||
class="remote-tab"
|
||||
:class="{ active: tab.id === remoteSession.active_tab_id }"
|
||||
@click="switchRemoteTab(tab.id)"
|
||||
>
|
||||
<span class="tab-title" :title="tab.url">{{ tab.title || 'Loading...' }}</span>
|
||||
<el-icon
|
||||
v-if="remoteSession.tabs.length > 1"
|
||||
class="tab-close"
|
||||
@click.stop="closeRemoteTab(tab.id)"
|
||||
>
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!showRemoteError"
|
||||
ref="remoteFrameRef"
|
||||
@@ -153,7 +173,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
ArrowLeft, TopRight, Refresh, Loading, Back, Right, Warning, DocumentCopy, Delete, EditPen,
|
||||
Link as LinkIcon, Monitor, SetUp, Reading, Cpu, DataLine,
|
||||
Grid, Connection, Ticket, Wallet, Key, Tools, Star, House,
|
||||
Grid, Connection, Ticket, Wallet, Key, Tools, Star, House, Close,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { browserSessionsApi, customPagesApi, type BrowserEventPayload, type BrowserSessionData, type CustomPageData } from '@/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -396,7 +416,7 @@ function connectRemoteWs() {
|
||||
// Text frame = JSON control message
|
||||
try {
|
||||
const msg = JSON.parse(evt.data as string)
|
||||
if (msg.type === 'init' && msg.session) {
|
||||
if ((msg.type === 'init' || msg.type === 'state') && msg.session) {
|
||||
remoteSession.value = msg.session as BrowserSessionData
|
||||
return
|
||||
}
|
||||
@@ -477,6 +497,26 @@ function sendRemoteCommand(type: 'reload' | 'back' | 'forward') {
|
||||
sendRemoteEvent({ type })
|
||||
}
|
||||
|
||||
async function switchRemoteTab(tabId: string) {
|
||||
if (!remoteSession.value || remoteSession.value.active_tab_id === tabId) return
|
||||
try {
|
||||
const res = await browserSessionsApi.activateTab(remoteSession.value.id, tabId)
|
||||
remoteSession.value = res.data
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '切换标签页失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function closeRemoteTab(tabId: string) {
|
||||
if (!remoteSession.value) return
|
||||
try {
|
||||
const res = await browserSessionsApi.closeTab(remoteSession.value.id, tabId)
|
||||
remoteSession.value = res.data
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '关闭标签页失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function copyRemoteSelection() {
|
||||
if (!remoteSession.value) return
|
||||
try {
|
||||
@@ -1138,6 +1178,62 @@ onBeforeUnmount(() => {
|
||||
var(--bg-surface);
|
||||
}
|
||||
|
||||
.remote-tabs {
|
||||
display: flex;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-bottom: 1px solid rgba(140, 119, 98, 0.15);
|
||||
padding: 4px 6px 0;
|
||||
gap: 2px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.remote-tabs::-webkit-scrollbar { display: none; }
|
||||
|
||||
.remote-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-radius: 8px 8px 0 0;
|
||||
color: var(--text-soft);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
max-width: 180px;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.remote-tab:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.remote-tab.active {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
border-color: rgba(140, 119, 98, 0.15);
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
font-size: 12px;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.tab-close:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.page-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"ignoreDeprecations": "5.0",
|
||||
"baseUrl": ".",
|
||||
"paths": { "@/*": ["src/*"] }
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user