feat(files): add fullscreen file manager
为文件管理添加全屏模式,保留目录与共享连接状态。
This commit is contained in:
1 parent
4efa997ad1
commit
645ed777b4
7 files changed
+125
-1
No files matched your search
@@ -470,6 +470,10 @@
|
||||
<button id="file-delete" class="file-action file-action-wide danger" type="button" disabled data-i18n-zh="删除" data-i18n-en="Delete">删除</button>
|
||||
<input id="file-upload-input" class="sr-only" type="file" tabindex="-1" />
|
||||
</div>
|
||||
<div class="file-view-controls">
|
||||
<button id="fullscreen-files" class="file-action" type="button" disabled aria-label="放大文件管理" title="放大文件管理" data-i18n-aria-label-zh="放大文件管理" data-i18n-aria-label-en="Expand file manager" data-i18n-title-zh="放大文件管理" data-i18n-title-en="Expand file manager"></button>
|
||||
<button id="exit-fullscreen-files" class="file-action" type="button" aria-label="退出全屏" title="退出全屏" data-i18n-aria-label-zh="退出全屏" data-i18n-aria-label-en="Exit fullscreen" data-i18n-title-zh="退出全屏" data-i18n-title-en="Exit fullscreen"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-table-wrap">
|
||||
|
||||
@@ -48,6 +48,7 @@ body[data-view='files'] { height: auto; min-height: 100%; overflow-y: auto; back
|
||||
.files-page .file-path-field input { height: 40px; font-size: 13px; padding-inline: 12px; }
|
||||
.files-page .file-actions { flex: 1; flex-wrap: wrap; gap: 6px; overflow: visible; }
|
||||
#dashboard .files-page .file-action { min-width: 38px; height: 38px; border: 1px solid var(--home-line); background: var(--home-surface); font-size: 14px; }
|
||||
#dashboard .files-page .file-view-controls .file-action { min-width: 44px; height: 44px; }
|
||||
#dashboard .files-page .file-action-wide { padding-inline: 14px; font-size: 13px; font-weight: 500; }
|
||||
#dashboard .files-page #file-upload { background: var(--home-accent-hover); color: var(--home-surface); border-color: var(--home-accent-hover); }
|
||||
#dashboard .files-page #file-delete { color: var(--home-danger); margin-left: auto; }
|
||||
@@ -68,6 +69,8 @@ body[data-view='files'] { height: auto; min-height: 100%; overflow-y: auto; back
|
||||
.files-page .file-manager-state { font-size: 14px; line-height: 1.8; }
|
||||
.files-page .file-statusbar { min-height: 42px; padding: 8px 16px; font-size: 11px; flex-wrap: wrap; }
|
||||
.files-page .file-progress { max-width: 100%; }
|
||||
.files-page .file-manager:fullscreen .file-toolbar { gap: 8px; padding: 8px 12px; }
|
||||
.files-page .file-manager:fullscreen .file-path-field { flex-basis: 260px; }
|
||||
#dashboard .files-page .file-progress-cancel { height: 30px; color: var(--home-ink); background: var(--home-bg); }
|
||||
.files-footer { display: flex; justify-content: space-between; gap: 12px; color: var(--home-muted); font-size: 12px; }
|
||||
body[data-view='files'] #dashboard .home-search, body[data-view='files'] #dashboard .header-add { display: none; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import { createElement, Maximize, Minimize } from 'lucide';
|
||||
import { historyKey, historyLabel } from './history';
|
||||
import { listHosts, hostCredentials, saveHost, removeHost, updateHostSystem, type CloudHost, type Credentials, type HostSystemInfo } from './cloud-api';
|
||||
import { Dashboard } from './dashboard';
|
||||
@@ -295,6 +296,8 @@ const ui = {
|
||||
eventMessage: element<HTMLElement>('event-message'),
|
||||
fileManagerTab: element<HTMLButtonElement>('file-manager-tab'),
|
||||
fileManagerPanel: element<HTMLElement>('file-manager-panel'),
|
||||
fullscreenFiles: element<HTMLButtonElement>('fullscreen-files'),
|
||||
exitFullscreenFiles: element<HTMLButtonElement>('exit-fullscreen-files'),
|
||||
fileTree: element<HTMLElement>('file-tree'),
|
||||
processManagerTab: element<HTMLButtonElement>('process-manager-tab'),
|
||||
processManagerPanel: element<HTMLElement>('process-manager-panel'),
|
||||
@@ -1103,6 +1106,7 @@ function updateConnectionStatus(message: LocalizedMessage): void {
|
||||
|
||||
function setState(state: ConnectionState, label?: string): void {
|
||||
connectionState = state;
|
||||
ui.fullscreenFiles.disabled = state !== 'connected';
|
||||
filePage?.setConnection(state, profiles.find((profile) => passwordContext(profile) === currentTargetKey)?.id, currentTargetLabel);
|
||||
const stateLabel = label ?? ({
|
||||
idle: bilingual('离线', 'Offline'),
|
||||
@@ -2069,6 +2073,22 @@ ui.fullscreenTerminal.addEventListener('click', async () => {
|
||||
else await ui.terminalCard.requestFullscreen();
|
||||
});
|
||||
document.addEventListener('fullscreenchange', () => fitTerminal(true));
|
||||
ui.fullscreenFiles.append(createElement(Maximize, { 'aria-hidden': 'true' }));
|
||||
ui.exitFullscreenFiles.append(createElement(Minimize, { 'aria-hidden': 'true' }));
|
||||
ui.fullscreenFiles.addEventListener('click', () => {
|
||||
// 全屏共享面板而非整个页面,保留目录及传输状态,同时移除周围的连接与导航区域。
|
||||
void ui.fileManagerPanel.requestFullscreen().catch(() => {
|
||||
toast(bilingual('无法进入全屏,请检查浏览器的全屏权限。', 'Unable to enter fullscreen. Check your browser fullscreen permissions.'), 'error');
|
||||
});
|
||||
});
|
||||
ui.exitFullscreenFiles.addEventListener('click', () => void document.exitFullscreen());
|
||||
let filesFullscreen = false;
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
const active = document.fullscreenElement === ui.fileManagerPanel;
|
||||
if (active) ui.exitFullscreenFiles.focus();
|
||||
else if (filesFullscreen) ui.fullscreenFiles.focus();
|
||||
filesFullscreen = active;
|
||||
});
|
||||
ui.fileManagerTab.addEventListener('click', () => toggleWorkspaceTab('files'));
|
||||
ui.processManagerTab.addEventListener('click', () => toggleWorkspaceTab('processes'));
|
||||
ui.eventToggle.addEventListener('click', () => toggleWorkspaceTab('log'));
|
||||
|
||||
+35
-1
@@ -2185,6 +2185,28 @@ button:disabled {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.file-view-controls {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.file-view-controls .file-action {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.file-view-controls svg { width: 18px; height: 18px; }
|
||||
.file-view-controls #exit-fullscreen-files { display: none; }
|
||||
.file-manager:fullscreen #fullscreen-files { display: none; }
|
||||
.file-manager:fullscreen #exit-fullscreen-files { display: grid; }
|
||||
|
||||
.file-view-controls .file-action:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.file-action:hover:not(:disabled) {
|
||||
border-color: var(--border);
|
||||
background: var(--surface-2);
|
||||
@@ -2421,6 +2443,14 @@ button:disabled {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.file-manager:fullscreen {
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
.toast-region {
|
||||
position: fixed;
|
||||
top: calc(var(--topbar-height) + 12px);
|
||||
@@ -2915,7 +2945,7 @@ button:disabled {
|
||||
|
||||
.file-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 3px;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
@@ -2945,8 +2975,12 @@ button:disabled {
|
||||
|
||||
.file-actions {
|
||||
width: 100%;
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.file-view-controls { grid-column: 2; grid-row: 1; }
|
||||
|
||||
.process-table {
|
||||
min-width: 650px;
|
||||
}
|
||||
|
||||
Generated
+7
@@ -12,6 +12,7 @@
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"jose": "^6.2.12",
|
||||
"lucide": "^1.51.0",
|
||||
"mappo": "^0.3.0",
|
||||
"react": "^19.3.0",
|
||||
"react-arborist": "^3.16.0",
|
||||
@@ -1857,6 +1858,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide": {
|
||||
"version": "1.51.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide/-/lucide-1.51.0.tgz",
|
||||
"integrity": "sha512-7K2Zm6Haz2EhYOAAE1ZQfsfs83ywGodSCvwr7YvCBzB9oelqDsRvMFUBDbXEGUaUMAisC7xcjMmSrozzYL/UwQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mappo": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mappo/-/mappo-0.3.0.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"jose": "^6.2.12",
|
||||
"lucide": "^1.51.0",
|
||||
"mappo": "^0.3.0",
|
||||
"react": "^19.3.0",
|
||||
"react-arborist": "^3.16.0",
|
||||
|
||||
@@ -8,12 +8,67 @@ test('独立入口、空状态与移动端布局', async ({ page }, testInfo) =>
|
||||
await expect(page.locator('#rail-files')).toHaveAttribute('aria-current', 'page');
|
||||
await expect(page.locator('#files-connect')).toBeDisabled();
|
||||
await expect(page.locator('#file-upload')).toBeDisabled();
|
||||
await expect(page.locator('#fullscreen-files')).toBeDisabled();
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||
await page.screenshot({ path: testInfo.outputPath('files-empty.png'), fullPage: true });
|
||||
await page.locator('#rail-overview').click();
|
||||
await expect(page.locator('#hosts-heading')).toBeVisible();
|
||||
});
|
||||
|
||||
test('文件管理全屏在独立页与工作台保留目录、选择和共享连接', async ({ page }, testInfo) => {
|
||||
const errors: string[] = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
const fixture = await fileFixture(page);
|
||||
await expect(page.locator('#fullscreen-files')).toBeDisabled();
|
||||
await connectFiles(page);
|
||||
await expect(page.locator('#file-manager-path')).toHaveValue('/root');
|
||||
await page.locator('.files-list').getByRole('treeitem', { name: 'README.md', exact: true }).click();
|
||||
|
||||
for (const view of ['files', 'workspace']) {
|
||||
const panel = page.locator('#file-manager-panel');
|
||||
const before = await panel.boundingBox();
|
||||
await page.locator('#fullscreen-files').click();
|
||||
await expect.poll(() => page.evaluate(() => document.fullscreenElement?.id)).toBe('file-manager-panel');
|
||||
await expect(page.locator('#fullscreen-files')).toBeHidden();
|
||||
await expect(page.locator('#exit-fullscreen-files')).toBeVisible();
|
||||
await expect(page.locator('#exit-fullscreen-files')).toBeFocused();
|
||||
const exitBounds = await page.locator('#exit-fullscreen-files').boundingBox();
|
||||
expect(exitBounds!.width).toBeGreaterThanOrEqual(44);
|
||||
expect(exitBounds!.height).toBeGreaterThanOrEqual(44);
|
||||
await expect(page.locator('#file-download')).toBeEnabled();
|
||||
await expect(page.locator('#file-manager-path')).toHaveValue('/root');
|
||||
const bounds = await panel.boundingBox();
|
||||
const viewport = await page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
||||
expect(bounds!.x).toBe(0);
|
||||
expect(bounds!.y).toBe(0);
|
||||
expect(bounds!.width).toBe(viewport.width);
|
||||
expect(bounds!.height).toBe(viewport.height);
|
||||
expect(bounds!.height).toBeGreaterThan(before!.height);
|
||||
expect(await panel.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
||||
const table = await panel.locator('.file-table-wrap').boundingBox();
|
||||
expect(table!.height).toBeGreaterThan(viewport.height / 2);
|
||||
await page.screenshot({ path: testInfo.outputPath(`files-fullscreen-${view}.png`) });
|
||||
await page.locator('#exit-fullscreen-files').click();
|
||||
await expect.poll(() => page.evaluate(() => document.fullscreenElement)).toBeNull();
|
||||
await expect(page.locator('#fullscreen-files')).toBeFocused();
|
||||
await expect(page.locator('#exit-fullscreen-files')).toBeHidden();
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(page.locator('body')).toHaveAttribute('data-view', view);
|
||||
if (view === 'files') {
|
||||
await expect(page.locator('#files-connection-state')).toHaveText('SSH 已连接');
|
||||
await page.locator('#files-terminal').click();
|
||||
await page.locator('#file-manager-tab').click();
|
||||
}
|
||||
}
|
||||
|
||||
expect(fixture.sshSockets).toHaveLength(1);
|
||||
expect(fixture.sftpSockets).toHaveLength(1);
|
||||
expect(errors).toEqual([]);
|
||||
await page.getByRole('button', { name: '文件管理', exact: true }).click();
|
||||
await page.locator('#files-connect').click();
|
||||
await expect(page.locator('#fullscreen-files')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('连接、目录导航与共享终端会话不重复附着', async ({ page }, testInfo) => {
|
||||
const fixture = await fileFixture(page);
|
||||
await connectFiles(page);
|
||||
|
||||
Reference in new issue
Block a user