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:
@@ -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` 的玻璃面板视觉。
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
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 */
|
||||
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 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
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 */
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user