工作区是编辑器的容器,管理着所有的编辑活动、打开的文件、面板布局和项目结构。一个良好的工作区设计能够显著提升开发效率。本文详细介绍多标签页管理、项目管理、侧边栏布局与会话管理的设计与实现。
工作区架构
整体布局结构
/* 工作区DOM结构 */
class="workspace">
class="title-bar">...
class="main-area">
class="sidebar left-sidebar">
class="activity-bar">...
class="panel tree-panel">...
class="editor-area">
class="tabbar">...
class="editor-group">
class="editor">...
class="panel bottom-panel">
class="panel-tabs">...
class="panel-content">...
class="sidebar right-sidebar">...
/* 工作区布局CSS */
.workspace {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.main-area {
display: flex;
flex: 1;
overflow: hidden;
}
.editor-area {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.sidebar {
width: 250px;
background: var(--bg2);
display: flex;
flex-direction: column;
}
多标签页管理
标签页数据模型
// 标签页模型
class Tab {
constructor(path, editor) {
this.id = generateId();
this.path = path; // 文件路径
this.title = basename(path); // 显示标题
this.editor = editor; // 编辑器实例
this.isDirty = false; // 是否已修改
this.isPinned = false; // 是否固定
this.accessTime = Date.now();
}
}
// 标签页管理器
class TabManager {
constructor(this.tabs = []);
constructor(this.activeId = null;) {}
open(path) {
// 检查是否已打开
let tab = this.tabs.find(t => t.path === path);
if (tab) {
this.activate(tab.id);
return tab;
}
// 创建新标签页
tab = new Tab(path, new Editor());
this.tabs.push(tab);
this.activate(tab.id);
return tab;
}
close(tabId) {
const index = this.tabs.findIndex(t => t.id === tabId);
if (index === -1) return;
const tab = this.tabs[index];
// 检查是否需要保存
if (tab.isDirty) {
const shouldSave = confirm(`${tab.title} 已修改,是否保存?`);
if (shouldSave) saveFile(tab);
}
this.tabs.splice(index, 1);
// 激活相邻标签页
if (this.activeId === tabId) {
const newIndex = Math.min(index, this.tabs.length - 1);
if (newIndex >= 0) this.activate(this.tabs[newIndex].id);
}
}
activate(tabId) {
this.activeId = tabId;
const tab = this.getTab(tabId);
if (tab) tab.accessTime = Date.now();
}
}
标签页渲染
// 标签页渲染
class TabRenderer {
render(tabs, activeId) {
const html = tabs.map(tab => {
const isActive = tab.id === activeId;
const dirtyMark = tab.isDirty ? '● ' : '';
return `
class="tab ${isActive ? 'active' : ''} ${tab.isPinned ? 'pinned' : ''}" data-id="${tab.id}">
class="tab-title">${dirtyMark}${tab.title}
`;;
}).join('');
this.container.innerHTML = html;
this.attachEventListeners();
}
}
/* 标签页CSS样式 */
.tabbar {
display: flex;
background: var(--bg2);
border-bottom: 1px solid var(--border);
overflow-x: auto;
scrollbar-width: none;
}
.tab {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-right: 1px solid var(--border);
cursor: pointer;
font-size: .9rem;
color: var(--muted);
transition: background .15s;
}
.tab:hover {
background: rgba(255,255,255,.05);
}
.tab.active {
background: var(--bg);
color: var(--text);
border-bottom: 2px solid var(--accent);
}
.tab-close {
background: none;
border: none;
color: var(--muted);
cursor: pointer;
font-size: 1.1rem;
padding: 0 4px;
border-radius: 4px;
}
.tab-close:hover {
background: rgba(255,255,255,.1);
color: var(--text);
}
项目文件树
文件树数据结构
// 文件树节点
class FileNode {
constructor(this.name, this.isDirectory) {
this.path = path;
this.children = [];
this.isExpanded = false;
this.isHidden = false;
}
}
// 加载项目目录
async function loadProjectTree(rootPath) {
const root = new FileNode(rootPath, true);
root.children = await scanDirectory(rootPath);
return root;
}
async function scanDirectory(dirPath) {
const entries = await readdir(dirPath);
// 过滤隐藏文件和配置
const filtered = entries.filter(name => {
if (name.startsWith('.')) return false;
if (['node_modules', 'dist', 'build'].includes(name)) return false;
return true;
});
// 排序:目录在前,文件在后
return filtered.sort((a, b) => {
const aIsDir = isDirectory(a);
const bIsDir = isDirectory(b);
if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
return a.localeCompare(b);
});
}
文件树交互
// 文件树交互处理
class FileTreeView {
handleClick(node, event) {
if (node.isDirectory) {
// 切换展开/折叠
node.isExpanded = !node.isExpanded;
this.render();
} else {
// 打开文件
workspace.tabManager.open(node.path);
}
}
handleContextMenu(node, event) {
const menu = new ContextMenu([
{ label: '新建文件', action: () => createFile(node.path) },
{ label: '新建文件夹', action: () => createFolder(node.path) },
{ label: '重命名', action: () => rename(node.path) },
{ label: '删除', action: () => deleteNode(node.path) },
]);
menu.show(event.clientX, event.clientY);
}
handleDrag(node, event) {
const data = { type: 'file', path: node.path };
event.dataTransfer.setData('text/plain', JSON.stringify(data));
}
}
面板管理
底部面板
// 底部面板:终端、输出、问题面板
class PanelManager {
constructor(this.panels = new Map());
constructor(this.activePanel = null);
constructor(this.height = 200;) {}
register(id, { title, icon, content }) {
this.panels.set(id, { title, icon, content, isVisible: true });
}
show(panelId) {
this.activePanel = panelId;
for (const [id, panel] of this.panels) {
panel.isVisible = (id === panelId);
}
this.render();
}
toggle(panelId) {
if (this.activePanel === panelId) {
this.activePanel = null;
} else {
this.show(panelId);
}
this.render();
}
resize(height) {
this.height = Math.max(100, Math.min(500, height));
this.container.style.height = this.height + 'px';
}
}
可调整大小的面板
// 面板 resize 拖拽手柄
class PanelResizer {
setup(panel, handle) {
let startY, startHeight;
handle.addEventListener('mousedown', (e) => {
startY = e.clientY;
startHeight = panel.offsetHeight;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
});
const onMouseMove = (e) => {
const delta = startY - e.clientY;
panel.style.height = (startHeight + delta) + 'px';
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
}
}
/* Resize 手柄样式 */
.panel-resizer {
height: 4px;
background: transparent;
cursor: row-resize;
position: absolute;
top: -2px;
left: 0;
right: 0;
z-index: 10;
}
.panel-resizer:hover {
background: var(--accent);
}
会话管理
工作区状态保存与恢复
// 会话状态模型
class WorkspaceSession {
toJSON() {
return {
openFiles: this.tabs.map(t => t.path),
activeFile: this.activeTab?.path,
sidebarWidth: this.sidebar.width,
panelHeight: this.panel.height,
panelVisible: this.panel.isVisible,
scrollState: {
top: editor.scrollTop,
left: editor.scrollLeft,
}
};
}
static fromJSON(data) {
const session = new WorkspaceSession();
// 恢复打开的文件
for (const path of data.openFiles) {
session.tabManager.open(path);
}
// 恢复活动文件
if (data.activeFile) {
session.tabManager.activateByPath(data.activeFile);
}
return session;
}
}
自动保存与恢复
// 会话自动保存
class SessionManager {
constructor(this.storageKey = 'workspace-session') {
this.setupAutoSave();
}
setupAutoSave() {
// 保存当前会话
const save = () => {
const session = workspace.serialize();
localStorage.setItem(this.storageKey, JSON.stringify(session));
};
// 定期保存(每30秒)
setInterval(save, 30000);
// 标签页变化时保存
workspace.tabManager.on('change', save);
}
restore() {
const data = localStorage.getItem(this.storageKey);
if (!data) return;
try {
const session = JSON.parse(data);
return WorkspaceSession.fromJSON(session);
} catch (e) {
console.error('Failed to restore session:', e);
}
}
clear() {
localStorage.removeItem(this.storageKey);
}
}
布局配置
用户自定义布局
// 布局配置
class LayoutConfig {
constructor() {
this.config = {
sidebar: { visible: true, width: 250 },
panel: { visible: true, height: 200 },
minimap: { visible: true },
statusbar: { visible: true },
};
}
toggle(section) {
if (section in this.config) {
this.config[section].visible = !this.config[section].visible;
this.apply(section);
this.save();
}
}
apply(section) {
const el = document.querySelector(`.${section}`);
if (el) {
el.style.display = this.config[section].visible ? '' : 'none';
}
}
}
总结
- 布局结构:使用Flexbox构建可调整的工作区布局
- 多标签页:支持打开、关闭、切换、固定标签页
- 文件树:展示项目结构,支持展开、折叠、右键菜单
- 面板管理:底部面板(终端、输出、问题)支持显示/隐藏和调整大小
- 会话管理:保存和恢复工作区状态,包括打开的文件、窗口布局
- 布局配置:允许用户自定义侧边栏、面板、最小图的显示
良好的工作区设计能够显著提升开发效率,让用户能够专注于代码本身。