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
+1
View File
@@ -7,3 +7,4 @@
- 2026-03-25`/workspace` 默认布局改为“左侧 Workbench + 中央终端 + 右侧状态监控”,并在状态监控中新增开机累计上/下行流量展示。
- 2026-03-25:继续微调 `/workspace` Workbench,新增默认“快捷指令”标签、调整三栏宽度到更接近 xterminal 参考图,并修复终端区域鼠标悬停时指针异常消失的问题。
- 2026-03-25:前端主站视觉语言统一升级为 `Slate Control Center`,新增公共页面壳层与认证壳层,并重做 Dashboard、Settings、Login、Setup、Notifications、Proxies、Audit Logs、Workbench、StatusMonitor 的现代化 UI 表达。
- 2026-03-25:收口前端控制中心细节,修复顶部 `app-nav` 激活下划线误命中品牌链接的问题,并将 `ConnectionsView` 重做为控制中心卡片列表,同时升级 `/workspace` 外层容器与 `TerminalTabBar` 的玻璃面板视觉。
+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;
}