feat(frontend): redesign connections and workspace ui

refresh the connections view into a control center card layout
with summary stats, bulk actions, and faster access to test and
connect filtered SSH entries

polish workspace chrome with glass panel containers and update
the terminal tab bar styling and scrolling behavior

fix the top nav underline so it only tracks active nav links and
stays aligned after window resizing
This commit is contained in:
yinjianm
2026-03-25 05:10:01 +08:00
parent 91aa6e83ca
commit 191962b854
5 changed files with 617 additions and 468 deletions
+12 -1
View File
@@ -49,7 +49,7 @@ const altShortcutKey = ref<string | null>(null);
const updateUnderline = async () => {
await nextTick();
if (navRef.value && underlineRef.value) {
const activeLink = navRef.value.querySelector('.router-link-exact-active') as HTMLElement;
const activeLink = navRef.value.querySelector('.app-nav__link.is-active, .app-nav__link.router-link-exact-active') as HTMLElement | null;
if (activeLink) {
underlineRef.value.style.left = `${activeLink.offsetLeft}px`;
underlineRef.value.style.width = `${activeLink.offsetWidth}px`;
@@ -65,6 +65,7 @@ onMounted(() => {
window.addEventListener('keydown', handleAltKeyDown);
window.addEventListener('keyup', handleGlobalKeyUp);
window.addEventListener('resize', updateUnderline);
window.addEventListener('beforeinstallprompt', () => {
console.log('[App.vue] beforeinstallprompt event fired. Browser will handle install prompt.');
@@ -90,6 +91,7 @@ watch(
onUnmounted(() => {
window.removeEventListener('keydown', handleAltKeyDown);
window.removeEventListener('keyup', handleGlobalKeyUp);
window.removeEventListener('resize', updateUnderline);
});
const isWorkspaceRoute = computed(() => route.path === '/workspace');
@@ -368,9 +370,18 @@ const handleGlobalKeyUp = async (event: KeyboardEvent) => {
display: flex;
align-items: center;
gap: 0.35rem;
min-width: 0;
max-width: 100%;
padding: 0.35rem;
border-radius: 18px;
background: rgba(241, 245, 251, 0.9);
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
}
.app-nav::-webkit-scrollbar {
display: none;
}
.app-nav__link {
@@ -63,6 +63,7 @@ const sessionStore = useSessionStore(); // Session store 保持不变
const showConnectionListPopup = ref(false); // 连接列表弹出状态
const draggableSessions = ref<SessionTabInfoWithStatus[]>([]); // + Local state for draggable
const showTransferProgressModal = ref(false); // 控制传输进度模态框的显示状态
const tabScrollerRef = ref<HTMLElement | null>(null);
// + Watch prop changes to update local state
watch(() => props.sessions, (newSessions) => {
@@ -393,7 +394,7 @@ const handleWheel: EventListener = (event: Event) => {
// 在组件挂载时添加滚轮事件监听
onMounted(() => {
const tabContainer = document.querySelector('.overflow-x-auto');
const tabContainer = tabScrollerRef.value;
if (tabContainer) {
tabContainer.addEventListener('wheel', handleWheel as EventListener, { passive: false });
}
@@ -401,7 +402,7 @@ onMounted(() => {
// 在组件卸载时移除滚轮事件监听
onBeforeUnmount(() => {
const tabContainer = document.querySelector('.overflow-x-auto');
const tabContainer = tabScrollerRef.value;
if (tabContainer) {
tabContainer.removeEventListener('wheel', handleWheel as EventListener);
}
@@ -411,11 +412,13 @@ onBeforeUnmount(() => {
<template>
<!-- +++ 使用 :class 绑定来条件化样式包括高度 (修正 props 引用) +++ -->
<div :class="['flex bg-header border border-border overflow-hidden',
{ 'rounded-t-md mx-2 mt-2': !props.isMobile }, // Desktop margins/rounding - Use props.isMobile
props.isMobile ? 'h-8' : 'h-10' // Mobile height h-8, Desktop h-10 - Use props.isMobile
]">
<div class="flex items-center overflow-x-auto flex-shrink min-w-0 h-full"> <!-- Ensure inner div has h-full -->
<div
:class="[
'terminal-tab-shell flex overflow-hidden',
props.isMobile ? 'terminal-tab-shell--mobile h-10' : 'terminal-tab-shell--desktop h-12',
]"
>
<div ref="tabScrollerRef" class="terminal-tab-shell__scroller flex items-center overflow-x-auto flex-shrink min-w-0 h-full">
<draggable
v-model="draggableSessions"
item-key="sessionId"
@@ -518,3 +521,32 @@ onBeforeUnmount(() => {
<TransferProgressModal v-model:visible="showTransferProgressModal" />
</div>
</template>
<style scoped>
.terminal-tab-shell {
position: relative;
align-items: stretch;
border: 1px solid rgba(103, 124, 155, 0.18);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(243, 247, 253, 0.74));
box-shadow: var(--shadow-card);
backdrop-filter: blur(18px);
}
.terminal-tab-shell--desktop {
margin: 1rem 1rem 0;
border-radius: 28px;
}
.terminal-tab-shell--mobile {
margin: 0.75rem 0.75rem 0;
border-radius: 22px;
}
.terminal-tab-shell__scroller {
scrollbar-width: none;
}
.terminal-tab-shell__scroller::-webkit-scrollbar {
display: none;
}
</style>
File diff suppressed because it is too large Load Diff
+81 -40
View File
@@ -824,14 +824,32 @@ const closeFileManagerModal = () => {
<style scoped>
.workspace-view {
position: relative;
display: flex;
background-color: transparent;
flex-direction: column;
gap: 0.85rem;
box-sizing: border-box;
padding: 0 1rem 1rem;
background-color: transparent;
height: 100dvh; /* 使用动态视口高度 */
min-height: 0;
overflow: hidden;
transition: height 0.3s ease; /* 可选:添加过渡效果 */
}
.workspace-view::before {
content: '';
position: absolute;
inset: 0.9rem 1rem 1rem;
border-radius: 32px;
border: 1px solid rgba(103, 124, 155, 0.12);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.4), rgba(241, 246, 252, 0.18)),
radial-gradient(circle at top right, rgba(60, 105, 231, 0.1), transparent 28%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72);
pointer-events: none;
}
/* 当 Header 可见时,调整高度 */
.workspace-view.with-header {
/* 假设 Header 高度为 55px (根据 App.vue CSS) */
@@ -839,14 +857,20 @@ const closeFileManagerModal = () => {
}
.main-content-area {
display: flex;
flex: 1;
overflow: hidden; /* Keep overflow hidden */
border: 1px solid var(--border-color, #ccc); /* Use variable for border */
border-top: none; /* Remove top border as it's handled by the tab bar */
border-radius: 0 0 5px 5px; /* Top-left, Top-right, Bottom-right, Bottom-left */
margin: var(--base-margin, 0.5rem); /* Add some margin around the content area */
margin-top: 0; /* Remove top margin if tab bar is directly above */
position: relative;
z-index: 1;
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
border: 1px solid rgba(103, 124, 155, 0.18);
border-radius: 30px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.84), rgba(242, 247, 253, 0.72)),
linear-gradient(135deg, rgba(60, 105, 231, 0.05), transparent);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(22px);
padding: 0.85rem;
}
.layout-renderer-wrapper {
@@ -854,64 +878,81 @@ const closeFileManagerModal = () => {
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 24px;
background: rgba(255, 255, 255, 0.46);
}
/* 面板占位符样式 (用于加载或错误状态) */
.pane-placeholder {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
color: var(--text-color-secondary); /* Use secondary text color variable */
background-color: var(--header-bg-color); /* Use header background for slight contrast */
font-size: 0.9em;
padding: var(--base-padding); /* Use base padding variable */
position: relative;
z-index: 1;
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
color: var(--text-color-secondary);
background: rgba(248, 251, 255, 0.72);
border: 1px dashed rgba(103, 124, 155, 0.28);
border-radius: 24px;
font-size: 0.92em;
padding: var(--base-padding);
}
/* --- Mobile Layout Styles --- */
.workspace-view.is-mobile {
/* Ensure flex column layout */
display: flex; /* Uncommented */
flex-direction: column; /* Uncommented */
/* Height is already handled by .workspace-view and .with-header */
padding: 0 0 0.85rem;
}
.workspace-view.is-mobile .main-content-area {
/* Hide the desktop content area in mobile view */
display: none;
}
.mobile-content-area {
display: flex; /* Use flex for the terminal container */
flex-direction: column; /* Stack elements vertically if needed */
flex-grow: 1; /* Allow this area to take up remaining space */
overflow: hidden; /* Prevent overflow */
position: relative; /* Needed for potential absolute positioning inside */
/* Remove desktop margins/borders */
margin: 0;
border: none;
border-radius: 0;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
flex-grow: 1;
min-height: 0;
overflow: hidden;
margin: 0 0.75rem;
border: 1px solid rgba(103, 124, 155, 0.18);
border-radius: 26px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.84), rgba(242, 247, 253, 0.72));
box-shadow: var(--shadow-soft);
backdrop-filter: blur(22px);
padding: 0.75rem;
}
.mobile-terminal {
flex-grow: 1; /* Terminal takes all available space in mobile-content-area */
flex-grow: 1;
width: 100%;
overflow: hidden;
}
.mobile-command-bar {
flex-shrink: 0; /* Prevent command bar from shrinking */
/* Add specific styles if needed, e.g., border-top */
border-top: 1px solid var(--border-color, #ccc);
position: relative;
z-index: 1;
flex-shrink: 0;
margin: 0.75rem 0.75rem 0;
border: 1px solid rgba(103, 124, 155, 0.18);
border-radius: 22px;
background: rgba(255, 255, 255, 0.82);
box-shadow: var(--shadow-card);
overflow: hidden;
}
.mobile-virtual-keyboard {
flex-shrink: 0; /* 防止虚拟键盘缩小 */
width: 100%; /* 确保宽度为 100% */
box-sizing: border-box; /* 边框和内边距包含在宽度内 */
/* 可以添加更多样式,例如背景色、边框等 */
position: relative;
z-index: 1;
flex-shrink: 0;
width: calc(100% - 1.5rem);
margin: 0.75rem auto 0;
box-sizing: border-box;
border-radius: 24px;
overflow: hidden;
}