V1.2.0 #17
+32
-1
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { Folder, Pencil, Trash2 } from "lucide-vue-next";
|
import { Folder, Pencil, Trash2, Radio } from "lucide-vue-next";
|
||||||
import {
|
import {
|
||||||
api,
|
api,
|
||||||
type HostRow,
|
type HostRow,
|
||||||
@@ -37,6 +37,22 @@ const tabs = ref<{ id: string; hostId: number; label: string }[]>([]);
|
|||||||
const activePanes = ref<string[]>([]);
|
const activePanes = ref<string[]>([]);
|
||||||
const draggedTabId = ref<string | null>(null);
|
const draggedTabId = ref<string | null>(null);
|
||||||
|
|
||||||
|
const broadcastMode = ref(false);
|
||||||
|
const tabRefs = ref<Record<string, any>>({});
|
||||||
|
function setTabRef(el: any, id: string) {
|
||||||
|
if (el) tabRefs.value[id] = el;
|
||||||
|
else delete tabRefs.value[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBroadcast(data: string, sourceId: string) {
|
||||||
|
if (!broadcastMode.value) return;
|
||||||
|
for (const paneId of activePanes.value) {
|
||||||
|
if (paneId !== sourceId && tabRefs.value[paneId]) {
|
||||||
|
tabRefs.value[paneId].sendData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onTabDragStart(id: string) {
|
function onTabDragStart(id: string) {
|
||||||
draggedTabId.value = id;
|
draggedTabId.value = id;
|
||||||
}
|
}
|
||||||
@@ -744,6 +760,19 @@ async function deleteIdentityRow(id: number) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
v-if="activePanes.length > 1"
|
||||||
|
type="button"
|
||||||
|
class="hidden rounded-lg px-3 py-1.5 text-xs md:inline-flex border transition-colors"
|
||||||
|
:class="broadcastMode ? 'border-accent bg-accent/10 text-accent' : 'border-slate-800 text-slate-400 hover:border-slate-700 hover:text-white'"
|
||||||
|
@click="broadcastMode = !broadcastMode"
|
||||||
|
title="Broadcast input to all visible terminals"
|
||||||
|
>
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<Radio class="h-3.5 w-3.5" />
|
||||||
|
Broadcast
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="hidden rounded-lg px-3 py-1.5 text-xs md:inline-flex"
|
class="hidden rounded-lg px-3 py-1.5 text-xs md:inline-flex"
|
||||||
@@ -1067,9 +1096,11 @@ async function deleteIdentityRow(id: number) {
|
|||||||
class="flex-1 min-w-0 h-full"
|
class="flex-1 min-w-0 h-full"
|
||||||
>
|
>
|
||||||
<TabContent
|
<TabContent
|
||||||
|
:ref="(el) => setTabRef(el, t.id)"
|
||||||
:host-id="t.hostId"
|
:host-id="t.hostId"
|
||||||
:visible="activePanes.includes(t.id)"
|
:visible="activePanes.includes(t.id)"
|
||||||
:show-sftp="showSftpPanel"
|
:show-sftp="showSftpPanel"
|
||||||
|
@broadcast-data="(data: string) => handleBroadcast(data, t.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ const props = defineProps<{
|
|||||||
showSftp: boolean;
|
showSftp: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'broadcast-data', data: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
sendData: (data: string) => {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(new TextEncoder().encode(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const termEl = ref<HTMLElement | null>(null);
|
const termEl = ref<HTMLElement | null>(null);
|
||||||
const status = ref("Connecting…");
|
const status = ref("Connecting…");
|
||||||
const connId = ref<string | null>(null);
|
const connId = ref<string | null>(null);
|
||||||
@@ -97,6 +109,7 @@ onMounted(async () => {
|
|||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(new TextEncoder().encode(data));
|
ws.send(new TextEncoder().encode(data));
|
||||||
}
|
}
|
||||||
|
emit("broadcast-data", data);
|
||||||
});
|
});
|
||||||
|
|
||||||
term.onResize(({ cols, rows }) => {
|
term.onResize(({ cols, rows }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user