feat: add collapsible workspace sidebar supporting Local PC, SSH, and Serial Dev Boards
This commit is contained in:
@@ -0,0 +1,627 @@
|
||||
/**
|
||||
* 工作区管理器 (WorkspaceManager)
|
||||
* 支持管理多个工作环境(本地 PC、SSH 远程开发机、串口/开发板)
|
||||
* 每个工作区拥有独立的标签页会话集合,并提供可展开/收起的高性能左侧栏
|
||||
*/
|
||||
|
||||
export const WORKSPACE_TYPES = {
|
||||
LOCAL: 'local',
|
||||
SSH: 'ssh',
|
||||
SERIAL: 'serial',
|
||||
};
|
||||
|
||||
const ICONS = {
|
||||
local: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
|
||||
<line x1="8" y1="21" x2="16" y2="21"></line>
|
||||
<line x1="12" y1="17" x2="12" y2="21"></line>
|
||||
</svg>`,
|
||||
ssh: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||
</svg>`,
|
||||
serial: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
|
||||
<rect x="9" y="9" width="6" height="6"></rect>
|
||||
<line x1="9" y1="1" x2="9" y2="4"></line>
|
||||
<line x1="15" y1="1" x2="15" y2="4"></line>
|
||||
<line x1="9" y1="20" x2="9" y2="23"></line>
|
||||
<line x1="15" y1="20" x2="15" y2="23"></line>
|
||||
<line x1="20" y1="9" x2="23" y2="9"></line>
|
||||
<line x1="20" y1="14" x2="23" y2="14"></line>
|
||||
<line x1="1" y1="9" x2="4" y2="9"></line>
|
||||
<line x1="1" y1="14" x2="4" y2="14"></line>
|
||||
</svg>`,
|
||||
};
|
||||
|
||||
const DEFAULT_WORKSPACES = [
|
||||
{
|
||||
id: 'ws_local',
|
||||
name: '本机电脑 (Local PC)',
|
||||
type: WORKSPACE_TYPES.LOCAL,
|
||||
desc: '本地 Linux 环境与项目开发',
|
||||
cwd: '~',
|
||||
shell: '',
|
||||
args: [],
|
||||
},
|
||||
{
|
||||
id: 'ws_ssh',
|
||||
name: '开发服务器 (SSH)',
|
||||
type: WORKSPACE_TYPES.SSH,
|
||||
desc: '远程 Linux 开发机',
|
||||
host: '192.168.1.100',
|
||||
user: 'dev',
|
||||
port: 22,
|
||||
extraArgs: '',
|
||||
},
|
||||
{
|
||||
id: 'ws_serial',
|
||||
name: '嵌入式开发板 (Serial)',
|
||||
type: WORKSPACE_TYPES.SERIAL,
|
||||
desc: '串口终端 /dev/ttyUSB0 (115200)',
|
||||
device: '/dev/ttyUSB0',
|
||||
baudRate: 115200,
|
||||
tool: 'picocom',
|
||||
},
|
||||
];
|
||||
|
||||
export class WorkspaceManager {
|
||||
constructor(options = {}) {
|
||||
this.tabManager = options.tabManager;
|
||||
this.sidebar = document.getElementById('workspace-sidebar');
|
||||
this.listContainer = document.getElementById('sidebar-workspace-list');
|
||||
this.modalOverlay = document.getElementById('workspace-modal-overlay');
|
||||
|
||||
this.workspaces = [];
|
||||
this.activeWorkspaceId = 'ws_local';
|
||||
this.isCollapsed = false;
|
||||
this.editingWorkspaceId = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loadState();
|
||||
this.initEventListeners();
|
||||
this.renderSidebar();
|
||||
|
||||
// 订阅标签页状态变更,以便实时刷新侧边栏标签列表
|
||||
this.tabManager.onTabListChange = () => {
|
||||
this.renderSidebar();
|
||||
};
|
||||
|
||||
// 激活初始工作区
|
||||
this.switchWorkspace(this.activeWorkspaceId, true);
|
||||
}
|
||||
|
||||
loadState() {
|
||||
try {
|
||||
const saved = localStorage.getItem('hatty:workspaces');
|
||||
if (saved) {
|
||||
this.workspaces = JSON.parse(saved);
|
||||
} else {
|
||||
this.workspaces = JSON.parse(JSON.stringify(DEFAULT_WORKSPACES));
|
||||
this.saveWorkspaces();
|
||||
}
|
||||
|
||||
const savedActive = localStorage.getItem('hatty:active-workspace');
|
||||
if (savedActive && this.workspaces.some((w) => w.id === savedActive)) {
|
||||
this.activeWorkspaceId = savedActive;
|
||||
} else if (this.workspaces.length > 0) {
|
||||
this.activeWorkspaceId = this.workspaces[0].id;
|
||||
}
|
||||
|
||||
this.isCollapsed = localStorage.getItem('hatty:sidebar-collapsed') === 'true';
|
||||
if (this.isCollapsed) {
|
||||
this.sidebar?.classList.add('collapsed');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load workspace state:', e);
|
||||
this.workspaces = JSON.parse(JSON.stringify(DEFAULT_WORKSPACES));
|
||||
}
|
||||
}
|
||||
|
||||
saveWorkspaces() {
|
||||
try {
|
||||
localStorage.setItem('hatty:workspaces', JSON.stringify(this.workspaces));
|
||||
localStorage.setItem('hatty:active-workspace', this.activeWorkspaceId);
|
||||
localStorage.setItem('hatty:sidebar-collapsed', String(this.isCollapsed));
|
||||
} catch (e) {
|
||||
console.warn('Failed to save workspaces:', e);
|
||||
}
|
||||
}
|
||||
|
||||
initEventListeners() {
|
||||
// 侧边栏折叠/展开按钮
|
||||
document.getElementById('btn-toggle-sidebar')?.addEventListener('click', () => {
|
||||
this.toggleSidebar();
|
||||
});
|
||||
|
||||
document.getElementById('btn-collapse-sidebar')?.addEventListener('click', () => {
|
||||
this.setCollapsed(true);
|
||||
});
|
||||
|
||||
// 新建工作区按钮
|
||||
document.getElementById('btn-add-workspace')?.addEventListener('click', () => {
|
||||
this.openCreateModal();
|
||||
});
|
||||
|
||||
document.getElementById('btn-footer-add-workspace')?.addEventListener('click', () => {
|
||||
this.openCreateModal();
|
||||
});
|
||||
|
||||
// 模态框按钮
|
||||
document.getElementById('btn-close-workspace-modal')?.addEventListener('click', () => {
|
||||
this.closeModal();
|
||||
});
|
||||
|
||||
document.getElementById('btn-cancel-workspace')?.addEventListener('click', () => {
|
||||
this.closeModal();
|
||||
});
|
||||
|
||||
document.getElementById('btn-save-workspace')?.addEventListener('click', () => {
|
||||
this.saveModalWorkspace();
|
||||
});
|
||||
|
||||
this.modalOverlay?.addEventListener('click', (e) => {
|
||||
if (e.target === this.modalOverlay) {
|
||||
this.closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleSidebar() {
|
||||
this.setCollapsed(!this.isCollapsed);
|
||||
}
|
||||
|
||||
setCollapsed(collapsed) {
|
||||
this.isCollapsed = collapsed;
|
||||
if (this.sidebar) {
|
||||
this.sidebar.classList.toggle('collapsed', collapsed);
|
||||
}
|
||||
this.saveWorkspaces();
|
||||
|
||||
// 触发当前终端自适应尺寸
|
||||
setTimeout(() => {
|
||||
const activeTerm = this.tabManager.getActiveTerminal();
|
||||
if (activeTerm) {
|
||||
activeTerm.fit();
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
|
||||
getActiveWorkspace() {
|
||||
return this.workspaces.find((w) => w.id === this.activeWorkspaceId) || this.workspaces[0];
|
||||
}
|
||||
|
||||
getSpawnOptions(workspaceId) {
|
||||
const ws = this.workspaces.find((w) => w.id === workspaceId) || this.getActiveWorkspace();
|
||||
if (!ws) return {};
|
||||
|
||||
if (ws.type === WORKSPACE_TYPES.SSH) {
|
||||
const targetHost = ws.user ? `${ws.user}@${ws.host}` : ws.host;
|
||||
const portArgs = ws.port && Number(ws.port) !== 22 ? ['-p', String(ws.port)] : [];
|
||||
const extra = ws.extraArgs ? ws.extraArgs.split(' ').filter(Boolean) : [];
|
||||
return {
|
||||
shell: 'ssh',
|
||||
args: [...portArgs, ...extra, targetHost],
|
||||
title: `ssh: ${ws.name || targetHost}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (ws.type === WORKSPACE_TYPES.SERIAL) {
|
||||
const baud = String(ws.baudRate || 115200);
|
||||
const dev = ws.device || '/dev/ttyUSB0';
|
||||
return {
|
||||
shell: 'picocom',
|
||||
args: ['-b', baud, dev],
|
||||
title: `serial: ${dev}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 本地主机
|
||||
return {
|
||||
cwd: ws.cwd || '~',
|
||||
shell: ws.shell || undefined,
|
||||
args: ws.args || [],
|
||||
title: ws.name || 'bash',
|
||||
};
|
||||
}
|
||||
|
||||
switchWorkspace(workspaceId, isInit = false) {
|
||||
const ws = this.workspaces.find((w) => w.id === workspaceId);
|
||||
if (!ws) return;
|
||||
|
||||
this.activeWorkspaceId = workspaceId;
|
||||
this.saveWorkspaces();
|
||||
|
||||
// 切换 TabManager 的活跃工作区
|
||||
const spawnOpts = this.getSpawnOptions(workspaceId);
|
||||
this.tabManager.setActiveWorkspace(workspaceId, spawnOpts);
|
||||
|
||||
this.renderSidebar();
|
||||
|
||||
// 更新底部状态栏信息
|
||||
const statusHost = document.getElementById('status-host');
|
||||
if (statusHost) {
|
||||
if (ws.type === WORKSPACE_TYPES.SSH) {
|
||||
statusHost.textContent = `SSH: ${ws.user ? ws.user + '@' : ''}${ws.host}`;
|
||||
} else if (ws.type === WORKSPACE_TYPES.SERIAL) {
|
||||
statusHost.textContent = `Serial: ${ws.device} (${ws.baudRate})`;
|
||||
} else {
|
||||
statusHost.textContent = `Local: ${ws.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderSidebar() {
|
||||
if (!this.listContainer) return;
|
||||
this.listContainer.innerHTML = '';
|
||||
|
||||
for (const ws of this.workspaces) {
|
||||
const isActive = ws.id === this.activeWorkspaceId;
|
||||
const card = document.createElement('div');
|
||||
card.className = `workspace-card ${isActive ? 'active' : ''}`;
|
||||
|
||||
const iconSvg = ICONS[ws.type] || ICONS.local;
|
||||
const tabs = this.tabManager.getTabsForWorkspace(ws.id);
|
||||
const tabCountText = `${tabs.length} 标签`;
|
||||
|
||||
let subText = '';
|
||||
if (ws.type === WORKSPACE_TYPES.SSH) {
|
||||
subText = `${ws.user ? ws.user + '@' : ''}${ws.host}:${ws.port || 22}`;
|
||||
} else if (ws.type === WORKSPACE_TYPES.SERIAL) {
|
||||
subText = `${ws.device} @ ${ws.baudRate}`;
|
||||
} else {
|
||||
subText = ws.cwd || '~';
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="ws-card-header">
|
||||
<div class="ws-card-info">
|
||||
<div class="ws-icon">${iconSvg}</div>
|
||||
<div class="ws-meta">
|
||||
<span class="ws-name" title="${ws.name}">${ws.name}</span>
|
||||
<span class="ws-badge">${tabCountText}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ws-actions">
|
||||
<button class="btn-icon ws-btn-edit" title="编辑工作区">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 20h9"></path>
|
||||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
${
|
||||
this.workspaces.length > 1
|
||||
? `<button class="btn-icon ws-btn-del" title="删除工作区">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
</button>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ws-desc" title="${subText}">${subText}</div>
|
||||
`;
|
||||
|
||||
// 绑定编辑和删除按钮
|
||||
card.querySelector('.ws-btn-edit')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.openEditModal(ws.id);
|
||||
});
|
||||
|
||||
card.querySelector('.ws-btn-del')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.deleteWorkspace(ws.id);
|
||||
});
|
||||
|
||||
// 点击工作区卡片切换工作区
|
||||
card.addEventListener('click', () => {
|
||||
if (ws.id !== this.activeWorkspaceId) {
|
||||
this.switchWorkspace(ws.id);
|
||||
}
|
||||
});
|
||||
|
||||
// 如果是当前选中的活跃工作区,展开展示其中的标签页列表
|
||||
if (isActive) {
|
||||
const tabsContainer = document.createElement('div');
|
||||
tabsContainer.className = 'ws-tabs-container';
|
||||
tabsContainer.innerHTML = `<div class="ws-tabs-title">已开启会话 (${tabs.length})</div>`;
|
||||
|
||||
for (const tab of tabs) {
|
||||
const tabItem = document.createElement('div');
|
||||
tabItem.className = `ws-tab-item ${tab.isActive ? 'active' : ''}`;
|
||||
tabItem.innerHTML = `
|
||||
<div class="ws-tab-left">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="4 17 10 11 4 5"></polyline>
|
||||
<line x1="12" y1="19" x2="20" y2="19"></line>
|
||||
</svg>
|
||||
<span class="ws-tab-title" title="${tab.title}">${tab.title}</span>
|
||||
</div>
|
||||
<span class="ws-tab-close" title="关闭">×</span>
|
||||
`;
|
||||
|
||||
tabItem.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('ws-tab-close')) {
|
||||
e.stopPropagation();
|
||||
this.tabManager.closeTab(tab.tabId);
|
||||
} else {
|
||||
this.tabManager.switchTab(tab.tabId);
|
||||
}
|
||||
});
|
||||
|
||||
tabsContainer.appendChild(tabItem);
|
||||
}
|
||||
|
||||
const addTabBtn = document.createElement('button');
|
||||
addTabBtn.className = 'ws-add-tab-btn';
|
||||
addTabBtn.innerHTML = `
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
<span>新建 ${ws.name} 标签页</span>
|
||||
`;
|
||||
addTabBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const spawnOpts = this.getSpawnOptions(ws.id);
|
||||
this.tabManager.createTab({
|
||||
workspaceId: ws.id,
|
||||
title: spawnOpts.title,
|
||||
cwd: spawnOpts.cwd,
|
||||
shell: spawnOpts.shell,
|
||||
args: spawnOpts.args,
|
||||
});
|
||||
});
|
||||
|
||||
tabsContainer.appendChild(addTabBtn);
|
||||
card.appendChild(tabsContainer);
|
||||
}
|
||||
|
||||
this.listContainer.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
openCreateModal() {
|
||||
this.editingWorkspaceId = null;
|
||||
const titleEl = document.getElementById('workspace-modal-title');
|
||||
if (titleEl) titleEl.textContent = '新建工作区 (PC / SSH / 开发板)';
|
||||
|
||||
this.renderModalForm({
|
||||
name: '',
|
||||
type: WORKSPACE_TYPES.LOCAL,
|
||||
cwd: '~',
|
||||
host: '',
|
||||
user: '',
|
||||
port: 22,
|
||||
device: '/dev/ttyUSB0',
|
||||
baudRate: 115200,
|
||||
});
|
||||
|
||||
this.modalOverlay?.classList.add('active');
|
||||
}
|
||||
|
||||
openEditModal(workspaceId) {
|
||||
const ws = this.workspaces.find((w) => w.id === workspaceId);
|
||||
if (!ws) return;
|
||||
|
||||
this.editingWorkspaceId = workspaceId;
|
||||
const titleEl = document.getElementById('workspace-modal-title');
|
||||
if (titleEl) titleEl.textContent = `编辑工作区 - ${ws.name}`;
|
||||
|
||||
this.renderModalForm(ws);
|
||||
this.modalOverlay?.classList.add('active');
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.modalOverlay?.classList.remove('active');
|
||||
this.editingWorkspaceId = null;
|
||||
}
|
||||
|
||||
renderModalForm(data) {
|
||||
const body = document.getElementById('workspace-modal-body');
|
||||
if (!body) return;
|
||||
|
||||
body.innerHTML = `
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">工作区类型</span>
|
||||
<span class="setting-desc">选择此工作区的连接目标与运行环境</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="type-selector-group" id="ws-type-group">
|
||||
<div class="type-option-btn ${data.type === WORKSPACE_TYPES.LOCAL ? 'selected' : ''}" data-type="local">
|
||||
${ICONS.local}
|
||||
<span>本机电脑 (Local)</span>
|
||||
</div>
|
||||
<div class="type-option-btn ${data.type === WORKSPACE_TYPES.SSH ? 'selected' : ''}" data-type="ssh">
|
||||
${ICONS.ssh}
|
||||
<span>SSH 远程机</span>
|
||||
</div>
|
||||
<div class="type-option-btn ${data.type === WORKSPACE_TYPES.SERIAL ? 'selected' : ''}" data-type="serial">
|
||||
${ICONS.serial}
|
||||
<span>串口 / 开发板</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row" style="margin-top: 12px;">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">工作区名称</span>
|
||||
<span class="setting-desc">自定义显示名称</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<input type="text" id="ws-input-name" value="${data.name || ''}" placeholder="例如:Ubuntu-Server 或 ESP32" style="width: 220px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 本地特定配置 -->
|
||||
<div id="ws-config-local" style="display: ${data.type === WORKSPACE_TYPES.LOCAL ? 'block' : 'none'};">
|
||||
<div class="setting-row" style="margin-top: 12px;">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">工作目录 (CWD)</span>
|
||||
<span class="setting-desc">启动终端时的起始目录</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<input type="text" id="ws-input-cwd" value="${data.cwd || '~'}" style="width: 220px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSH 特定配置 -->
|
||||
<div id="ws-config-ssh" style="display: ${data.type === WORKSPACE_TYPES.SSH ? 'block' : 'none'};">
|
||||
<div class="setting-row" style="margin-top: 12px;">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">主机地址 (Host)</span>
|
||||
<span class="setting-desc">IP 或域名</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<input type="text" id="ws-input-host" value="${data.host || ''}" placeholder="192.168.1.100" style="width: 220px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row" style="margin-top: 12px;">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">用户名 (Username)</span>
|
||||
<span class="setting-desc">SSH 登录账号</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<input type="text" id="ws-input-user" value="${data.user || ''}" placeholder="root" style="width: 220px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row" style="margin-top: 12px;">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">端口号 (Port)</span>
|
||||
<span class="setting-desc">默认 22</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<input type="number" id="ws-input-port" value="${data.port || 22}" style="width: 100px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 串口开发板特定配置 -->
|
||||
<div id="ws-config-serial" style="display: ${data.type === WORKSPACE_TYPES.SERIAL ? 'block' : 'none'};">
|
||||
<div class="setting-row" style="margin-top: 12px;">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">串口设备路径 (Device)</span>
|
||||
<span class="setting-desc">Linux 串口设备路径</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<input type="text" id="ws-input-device" value="${data.device || '/dev/ttyUSB0'}" placeholder="/dev/ttyUSB0" style="width: 220px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row" style="margin-top: 12px;">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">波特率 (Baud Rate)</span>
|
||||
<span class="setting-desc">串口通信波特率</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<select id="ws-input-baud" style="width: 130px;">
|
||||
${[9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1500000]
|
||||
.map((b) => `<option value="${b}" ${Number(data.baudRate) === b ? 'selected' : ''}>${b}</option>`)
|
||||
.join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 绑定类型切换点击事件
|
||||
let currentType = data.type || WORKSPACE_TYPES.LOCAL;
|
||||
const typeBtns = body.querySelectorAll('.type-option-btn');
|
||||
typeBtns.forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
typeBtns.forEach((b) => b.classList.remove('selected'));
|
||||
btn.classList.add('selected');
|
||||
currentType = btn.dataset.type;
|
||||
|
||||
document.getElementById('ws-config-local').style.display = currentType === WORKSPACE_TYPES.LOCAL ? 'block' : 'none';
|
||||
document.getElementById('ws-config-ssh').style.display = currentType === WORKSPACE_TYPES.SSH ? 'block' : 'none';
|
||||
document.getElementById('ws-config-serial').style.display = currentType === WORKSPACE_TYPES.SERIAL ? 'block' : 'none';
|
||||
|
||||
// 默认名字补全
|
||||
const nameInput = document.getElementById('ws-input-name');
|
||||
if (!nameInput.value) {
|
||||
if (currentType === WORKSPACE_TYPES.SSH) nameInput.value = '远程服务器';
|
||||
else if (currentType === WORKSPACE_TYPES.SERIAL) nameInput.value = '开发板终端';
|
||||
else nameInput.value = '本机电脑';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
saveModalWorkspace() {
|
||||
const nameInput = document.getElementById('ws-input-name');
|
||||
const name = nameInput ? nameInput.value.trim() : '';
|
||||
if (!name) {
|
||||
alert('请输入工作区名称');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedTypeBtn = document.querySelector('.type-option-btn.selected');
|
||||
const type = selectedTypeBtn ? selectedTypeBtn.dataset.type : WORKSPACE_TYPES.LOCAL;
|
||||
|
||||
const wsData = {
|
||||
name,
|
||||
type,
|
||||
};
|
||||
|
||||
if (type === WORKSPACE_TYPES.LOCAL) {
|
||||
wsData.cwd = document.getElementById('ws-input-cwd')?.value.trim() || '~';
|
||||
} else if (type === WORKSPACE_TYPES.SSH) {
|
||||
wsData.host = document.getElementById('ws-input-host')?.value.trim() || 'localhost';
|
||||
wsData.user = document.getElementById('ws-input-user')?.value.trim() || '';
|
||||
wsData.port = parseInt(document.getElementById('ws-input-port')?.value, 10) || 22;
|
||||
} else if (type === WORKSPACE_TYPES.SERIAL) {
|
||||
wsData.device = document.getElementById('ws-input-device')?.value.trim() || '/dev/ttyUSB0';
|
||||
wsData.baudRate = parseInt(document.getElementById('ws-input-baud')?.value, 10) || 115200;
|
||||
}
|
||||
|
||||
if (this.editingWorkspaceId) {
|
||||
const idx = this.workspaces.findIndex((w) => w.id === this.editingWorkspaceId);
|
||||
if (idx !== -1) {
|
||||
this.workspaces[idx] = { ...this.workspaces[idx], ...wsData };
|
||||
}
|
||||
} else {
|
||||
const id = 'ws_' + Math.random().toString(36).substring(2, 9);
|
||||
const newWs = { id, ...wsData };
|
||||
this.workspaces.push(newWs);
|
||||
this.activeWorkspaceId = id;
|
||||
}
|
||||
|
||||
this.saveWorkspaces();
|
||||
this.closeModal();
|
||||
this.switchWorkspace(this.activeWorkspaceId);
|
||||
}
|
||||
|
||||
deleteWorkspace(workspaceId) {
|
||||
if (this.workspaces.length <= 1) {
|
||||
alert('至少需要保留一个工作区。');
|
||||
return;
|
||||
}
|
||||
|
||||
const ws = this.workspaces.find((w) => w.id === workspaceId);
|
||||
if (!confirm(`确定要删除工作区「${ws ? ws.name : workspaceId}」吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 关闭属于该工作区的所有标签页
|
||||
const tabs = this.tabManager.getTabsForWorkspace(workspaceId);
|
||||
for (const t of tabs) {
|
||||
this.tabManager.closeTab(t.tabId);
|
||||
}
|
||||
|
||||
this.workspaces = this.workspaces.filter((w) => w.id !== workspaceId);
|
||||
if (this.activeWorkspaceId === workspaceId) {
|
||||
this.activeWorkspaceId = this.workspaces[0].id;
|
||||
}
|
||||
|
||||
this.saveWorkspaces();
|
||||
this.switchWorkspace(this.activeWorkspaceId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user