feat(frontend): 侧边栏紧凑化与挤压式重设计,移除工作区面板功能拆分
- 对话侧边栏:行高/字号/圆角紧凑化,fixed 覆盖式改为文档流挤压式展开,移除工作区/虚拟显示器按钮 - 新增 WorkspaceSwitcher 浮层组件:列表即切换器,内联重命名/删除确认/新建表单动画,「…」菜单 fixed 逃逸裁切,刚好 7 行内部滚动 - 移除 LeftPanel 及两个旧工作区对话框:管理工作区迁入侧边栏,文件管理迁入输入栏 + 菜单(最多 4 项滚动) - 个人空间移除默认隐藏工作区/隐藏快捷窗口设置项;删除 panelMode 门控轮询(QuickDock 常驻轮询 + socket 推送替代) - 修复 flex 压缩导致菜单行高失效、分组对话 viewport 裁切最后一行、action-wrap 撑高对话项、/new 残留最近对话待办(无对话 id 不再请求 todo-list) - 工作区入口按钮改内联 layers 图标并统一 18px,host/docker 模式 localStorage 缓存避免按钮首屏延迟出现
This commit is contained in:
parent
15779be939
commit
b593d63856
496
demo/workspace_menu_redesign/app.js
Normal file
496
demo/workspace_menu_redesign/app.js
Normal file
@ -0,0 +1,496 @@
|
|||||||
|
/* ============================================================
|
||||||
|
工作区菜单重设计 Demo — 交互
|
||||||
|
数据字段对齐真实结构:workspace_id / label / path / running_task_count
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* ---------- 示例数据(字段语义与 HostWorkspaceManageDialog 一致) ---------- */
|
||||||
|
const DATA = {
|
||||||
|
host: [
|
||||||
|
{ workspace_id: 'default', label: '默认工作区', path: '~/Desktop/agents', running_task_count: 2 },
|
||||||
|
{ workspace_id: 'ws-client-a', label: '客户 A 项目', path: '~/Desktop/work/client-a', running_task_count: 0 },
|
||||||
|
{ workspace_id: 'ws-research', label: '论文实验', path: '~/Documents/research/thesis', running_task_count: 1 },
|
||||||
|
{ workspace_id: 'ws-playground', label: '临时测试', path: '/tmp/agent-playground', running_task_count: 0 }
|
||||||
|
],
|
||||||
|
docker: [
|
||||||
|
{ workspace_id: 'default', label: '默认项目', path: '', running_task_count: 0 },
|
||||||
|
{ workspace_id: 'ws-client-a', label: '客户 A 项目', path: '', running_task_count: 0 },
|
||||||
|
{ workspace_id: 'ws-research', label: '论文实验', path: '', running_task_count: 0 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
mode: 'host', // host=宿主机(工作区) / docker=项目
|
||||||
|
workspaces: [],
|
||||||
|
currentId: 'default',
|
||||||
|
defaultId: 'default',
|
||||||
|
menuOpenId: null, // 「…」菜单展开的行
|
||||||
|
renamingId: null, // 重命名内联编辑的行
|
||||||
|
deletingId: null, // 删除确认内联的行
|
||||||
|
popoverOpen: true,
|
||||||
|
createOpen: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const cloneData = (mode) => DATA[mode].map((w) => ({ ...w }));
|
||||||
|
state.workspaces = cloneData('host');
|
||||||
|
|
||||||
|
/* ---------- DOM ---------- */
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
const popover = document.getElementById('wsPopover');
|
||||||
|
const listEl = document.getElementById('wsList');
|
||||||
|
const countEl = document.getElementById('wsCount');
|
||||||
|
const headerTitle = document.getElementById('headerTitle');
|
||||||
|
const entryBtn = document.getElementById('workspaceEntry');
|
||||||
|
const entryLabel = document.getElementById('entryLabel');
|
||||||
|
const entryCurrentName = document.getElementById('entryCurrentName');
|
||||||
|
const bottomEl = document.getElementById('wsBottom');
|
||||||
|
const footerEl = document.getElementById('wsFooter');
|
||||||
|
const createBtn = document.getElementById('createBtn');
|
||||||
|
const createBtnText = document.getElementById('createBtnText');
|
||||||
|
const createForm = document.getElementById('createForm');
|
||||||
|
const createName = document.getElementById('createName');
|
||||||
|
const createPath = document.getElementById('createPath');
|
||||||
|
const createCancel = document.getElementById('createCancel');
|
||||||
|
const statusEl = document.getElementById('stageStatus');
|
||||||
|
|
||||||
|
const esc = (s) =>
|
||||||
|
String(s == null ? '' : s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
|
||||||
|
const setStatus = (msg) => { statusEl.textContent = msg; };
|
||||||
|
|
||||||
|
const isDocker = () => state.mode === 'docker';
|
||||||
|
const noun = () => (isDocker() ? '项目' : '工作区');
|
||||||
|
|
||||||
|
/* 内联 SVG 图标(file:// 协议下 <use> 会被 Chrome 拦截,故内联) */
|
||||||
|
const ICON = {
|
||||||
|
folder: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>',
|
||||||
|
dots: '<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg>'
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
浮层定位:锚定「工作区」入口按钮右侧,并夹紧在画布内
|
||||||
|
============================================================ */
|
||||||
|
function positionPopover() {
|
||||||
|
const c = canvas.getBoundingClientRect();
|
||||||
|
const b = entryBtn.getBoundingClientRect();
|
||||||
|
const ph = popover.offsetHeight || 0;
|
||||||
|
let top = b.top - c.top - 6;
|
||||||
|
const maxTop = Math.max(8, c.height - ph - 42); /* 42 = 底部状态条 + 余量 */
|
||||||
|
top = Math.max(8, Math.min(top, maxTop));
|
||||||
|
popover.style.left = b.right - c.left + 8 + 'px';
|
||||||
|
popover.style.top = top + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPopoverOpen(open) {
|
||||||
|
state.popoverOpen = open;
|
||||||
|
popover.classList.toggle('open', open);
|
||||||
|
entryBtn.classList.toggle('active', open);
|
||||||
|
entryBtn.setAttribute('aria-expanded', String(open));
|
||||||
|
if (open) {
|
||||||
|
positionPopover();
|
||||||
|
} else {
|
||||||
|
closeTransient();
|
||||||
|
if (state.createOpen) closeCreateForm(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTransient() {
|
||||||
|
closeMenu();
|
||||||
|
state.renamingId = null;
|
||||||
|
state.deletingId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
「…」二级菜单:fixed 浮层,逃逸容器裁切
|
||||||
|
(与 PersonalizationDrawer 的 settings-floating-menu 同方案)
|
||||||
|
============================================================ */
|
||||||
|
let menuEl = null;
|
||||||
|
|
||||||
|
function ensureMenuEl() {
|
||||||
|
if (menuEl) return menuEl;
|
||||||
|
menuEl = document.createElement('div');
|
||||||
|
menuEl.className = 'ws-menu-floating';
|
||||||
|
document.body.appendChild(menuEl);
|
||||||
|
menuEl.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('[data-act]');
|
||||||
|
if (!btn || btn.disabled) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
handleMenuAction(btn.dataset.act);
|
||||||
|
});
|
||||||
|
return menuEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMenu(rowEl, id) {
|
||||||
|
state.menuOpenId = id;
|
||||||
|
state.renamingId = null;
|
||||||
|
state.deletingId = null;
|
||||||
|
renderRows();
|
||||||
|
|
||||||
|
const ws = state.workspaces.find((w) => w.workspace_id === id);
|
||||||
|
if (!ws) return;
|
||||||
|
const isDefault = id === state.defaultId;
|
||||||
|
|
||||||
|
const el = ensureMenuEl();
|
||||||
|
el.innerHTML =
|
||||||
|
'<button type="button" data-act="set-default"' + (isDefault ? ' disabled' : '') + '>设为默认</button>' +
|
||||||
|
'<button type="button" data-act="rename">重命名</button>' +
|
||||||
|
'<button type="button" data-act="reveal"' + (isDocker() ? ' disabled title="Docker 模式不可用"' : '') + '>在文件夹中打开</button>' +
|
||||||
|
'<div class="ws-menu-sep"></div>' +
|
||||||
|
'<button type="button" class="danger" data-act="delete">删除</button>';
|
||||||
|
|
||||||
|
el.style.display = 'flex';
|
||||||
|
/* renderRows 后旧行元素已摘除,必须按 data-id 重新查询再取坐标 */
|
||||||
|
const newRow = listEl.querySelector('.ws-row[data-id="' + id + '"]');
|
||||||
|
const btn = newRow ? newRow.querySelector('.ws-more') : null;
|
||||||
|
if (!btn) { closeMenu(); return; }
|
||||||
|
const rect = btn.getBoundingClientRect();
|
||||||
|
const mw = el.offsetWidth;
|
||||||
|
const mh = el.offsetHeight;
|
||||||
|
let left = rect.right - mw;
|
||||||
|
let top = rect.bottom + 4;
|
||||||
|
left = Math.max(8, Math.min(left, window.innerWidth - mw - 8));
|
||||||
|
if (top + mh > window.innerHeight - 8) top = rect.top - mh - 4;
|
||||||
|
el.style.left = Math.round(left) + 'px';
|
||||||
|
el.style.top = Math.round(top) + 'px';
|
||||||
|
requestAnimationFrame(() => el.classList.add('open'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu() {
|
||||||
|
state.menuOpenId = null;
|
||||||
|
if (menuEl) {
|
||||||
|
menuEl.classList.remove('open');
|
||||||
|
menuEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMenuAction(act) {
|
||||||
|
const id = state.menuOpenId;
|
||||||
|
const ws = state.workspaces.find((w) => w.workspace_id === id);
|
||||||
|
closeMenu();
|
||||||
|
if (!ws) return;
|
||||||
|
|
||||||
|
if (act === 'set-default') {
|
||||||
|
state.defaultId = id;
|
||||||
|
setStatus('已将「' + ws.label + '」设为默认' + noun());
|
||||||
|
renderRows();
|
||||||
|
} else if (act === 'rename') {
|
||||||
|
state.renamingId = id;
|
||||||
|
renderRows();
|
||||||
|
} else if (act === 'reveal') {
|
||||||
|
/* 实现参考 InputComposer.vue 的 openProjectInFileManager */
|
||||||
|
setStatus('在文件夹中打开:' + (ws.path || ws.label));
|
||||||
|
} else if (act === 'delete') {
|
||||||
|
state.deletingId = id;
|
||||||
|
renderRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
渲染列表行
|
||||||
|
============================================================ */
|
||||||
|
function renderRows() {
|
||||||
|
const current = state.workspaces.find((w) => w.workspace_id === state.currentId);
|
||||||
|
entryCurrentName.textContent = current ? current.label : '';
|
||||||
|
countEl.textContent = state.workspaces.length + ' 个';
|
||||||
|
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
for (const ws of state.workspaces) {
|
||||||
|
const li = document.createElement('div');
|
||||||
|
const isCurrent = ws.workspace_id === state.currentId;
|
||||||
|
const isDefault = ws.workspace_id === state.defaultId;
|
||||||
|
|
||||||
|
li.className = 'ws-row'
|
||||||
|
+ (isDocker() ? ' docker' : '')
|
||||||
|
+ (isCurrent ? ' current' : '')
|
||||||
|
+ (state.menuOpenId === ws.workspace_id ? ' menu-open' : '');
|
||||||
|
li.dataset.id = ws.workspace_id;
|
||||||
|
li.setAttribute('role', 'option');
|
||||||
|
li.setAttribute('aria-selected', String(isCurrent));
|
||||||
|
|
||||||
|
if (state.deletingId === ws.workspace_id) {
|
||||||
|
/* --- 删除确认内联 --- */
|
||||||
|
li.innerHTML =
|
||||||
|
'<div class="ws-confirm">' +
|
||||||
|
'<div class="ws-confirm-text">删除「' + esc(ws.label) + '」?</div>' +
|
||||||
|
'<div class="ws-confirm-actions">' +
|
||||||
|
'<button type="button" class="ws-btn ghost" data-act="cancel-delete">取消</button>' +
|
||||||
|
'<button type="button" class="ws-btn danger" data-act="confirm-delete">删除</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
} else {
|
||||||
|
const metaHtml =
|
||||||
|
(isDefault ? '<span class="ws-tag">默认</span>' : '') +
|
||||||
|
'<button type="button" class="ws-more" data-act="menu" aria-label="更多操作" aria-expanded="' +
|
||||||
|
String(state.menuOpenId === ws.workspace_id) + '">' + ICON.dots + '</button>';
|
||||||
|
|
||||||
|
const nameHtml = state.renamingId === ws.workspace_id
|
||||||
|
? '<input class="ws-rename-input" data-rename-input value="' + esc(ws.label) + '" />'
|
||||||
|
: '<span class="ws-row-name">' + esc(ws.label) + '</span>';
|
||||||
|
|
||||||
|
const pathHtml = isDocker()
|
||||||
|
? ''
|
||||||
|
: '<span class="ws-row-path">' + esc(ws.path || '(未配置路径)') + '</span>';
|
||||||
|
|
||||||
|
li.innerHTML =
|
||||||
|
'<span class="ws-row-icon">' + ICON.folder + '</span>' +
|
||||||
|
'<span class="ws-row-text">' + nameHtml + pathHtml + '</span>' +
|
||||||
|
'<span class="ws-row-meta">' + metaHtml + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
listEl.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 重命名输入框自动聚焦 */
|
||||||
|
const renameInput = listEl.querySelector('[data-rename-input]');
|
||||||
|
if (renameInput) {
|
||||||
|
renameInput.focus();
|
||||||
|
renameInput.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
行为
|
||||||
|
============================================================ */
|
||||||
|
function switchWorkspace(id) {
|
||||||
|
if (id === state.currentId) return;
|
||||||
|
state.currentId = id;
|
||||||
|
const ws = state.workspaces.find((w) => w.workspace_id === id);
|
||||||
|
setStatus('已切换到「' + (ws ? ws.label : id) + '」');
|
||||||
|
renderRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitRename(id, value) {
|
||||||
|
const label = value.trim();
|
||||||
|
if (label) {
|
||||||
|
const ws = state.workspaces.find((w) => w.workspace_id === id);
|
||||||
|
if (ws && ws.label !== label) {
|
||||||
|
ws.label = label;
|
||||||
|
setStatus('已重命名为「' + label + '」');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.renamingId = null;
|
||||||
|
renderRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeWorkspace(id) {
|
||||||
|
const idx = state.workspaces.findIndex((w) => w.workspace_id === id);
|
||||||
|
if (idx === -1) return;
|
||||||
|
const removed = state.workspaces[idx];
|
||||||
|
state.workspaces.splice(idx, 1);
|
||||||
|
if (state.currentId === id && state.workspaces.length) {
|
||||||
|
state.currentId = state.workspaces[0].workspace_id;
|
||||||
|
}
|
||||||
|
if (state.defaultId === id && state.workspaces.length) {
|
||||||
|
state.defaultId = state.workspaces[0].workspace_id;
|
||||||
|
}
|
||||||
|
state.deletingId = null;
|
||||||
|
setStatus('已删除「' + removed.label + '」');
|
||||||
|
renderRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
新建表单:底部区域高度动画 + 交叉淡入淡出
|
||||||
|
============================================================ */
|
||||||
|
let bottomAnimating = false;
|
||||||
|
|
||||||
|
function swapBottom(toForm, done) {
|
||||||
|
if (bottomAnimating) return;
|
||||||
|
bottomAnimating = true;
|
||||||
|
|
||||||
|
const fromEl = toForm ? footerEl : createForm;
|
||||||
|
const toEl = toForm ? createForm : footerEl;
|
||||||
|
const h0 = fromEl.offsetHeight;
|
||||||
|
|
||||||
|
bottomEl.style.height = h0 + 'px';
|
||||||
|
fromEl.classList.add('fading');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fromEl.hidden = true;
|
||||||
|
fromEl.classList.remove('fading');
|
||||||
|
toEl.hidden = false;
|
||||||
|
toEl.classList.add('fading');
|
||||||
|
|
||||||
|
const h1 = toEl.offsetHeight;
|
||||||
|
bottomEl.style.height = h1 + 'px';
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => toEl.classList.remove('fading'));
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
bottomEl.style.height = '';
|
||||||
|
bottomAnimating = false;
|
||||||
|
if (typeof done === 'function') done();
|
||||||
|
}, 210);
|
||||||
|
}, 130);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateForm() {
|
||||||
|
if (state.createOpen) return;
|
||||||
|
state.createOpen = true;
|
||||||
|
closeMenu();
|
||||||
|
createName.value = '';
|
||||||
|
createPath.value = '';
|
||||||
|
swapBottom(true, () => createName.focus());
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCreateForm(instant) {
|
||||||
|
if (!state.createOpen) return;
|
||||||
|
state.createOpen = false;
|
||||||
|
if (instant) {
|
||||||
|
createForm.hidden = true;
|
||||||
|
footerEl.hidden = false;
|
||||||
|
bottomEl.style.height = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
swapBottom(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
createBtn.addEventListener('click', openCreateForm);
|
||||||
|
createCancel.addEventListener('click', () => closeCreateForm(false));
|
||||||
|
|
||||||
|
createForm.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const label = createName.value.trim() || (isDocker() ? '未命名项目' : '未命名工作区');
|
||||||
|
const path = createPath.value.trim();
|
||||||
|
const id = 'ws-' + Date.now().toString(36);
|
||||||
|
state.workspaces.push({ workspace_id: id, label: label, path: path, running_task_count: 0 });
|
||||||
|
setStatus('已创建「' + label + '」');
|
||||||
|
closeCreateForm(false);
|
||||||
|
renderRows();
|
||||||
|
listEl.scrollTop = listEl.scrollHeight;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
事件:列表行(事件委托)
|
||||||
|
============================================================ */
|
||||||
|
listEl.addEventListener('click', (e) => {
|
||||||
|
const actBtn = e.target.closest('[data-act]');
|
||||||
|
const row = e.target.closest('.ws-row');
|
||||||
|
const id = row ? row.dataset.id : null;
|
||||||
|
|
||||||
|
if (actBtn) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const act = actBtn.dataset.act;
|
||||||
|
if (act === 'menu') {
|
||||||
|
if (state.menuOpenId === id) {
|
||||||
|
closeMenu();
|
||||||
|
renderRows();
|
||||||
|
} else {
|
||||||
|
openMenu(row, id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (act === 'cancel-delete') { state.deletingId = null; renderRows(); return; }
|
||||||
|
if (act === 'confirm-delete') { removeWorkspace(id); return; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 点击行本体:切换工作区(重命名/删除确认态除外) */
|
||||||
|
if (row && id && !state.renamingId && !state.deletingId) {
|
||||||
|
if (state.menuOpenId) { closeMenu(); renderRows(); return; }
|
||||||
|
switchWorkspace(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* 重命名输入:Enter 提交 / Esc 取消 */
|
||||||
|
listEl.addEventListener('keydown', (e) => {
|
||||||
|
const input = e.target.closest('[data-rename-input]');
|
||||||
|
if (!input) return;
|
||||||
|
const row = input.closest('.ws-row');
|
||||||
|
const id = row ? row.dataset.id : null;
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); commitRename(id, input.value); }
|
||||||
|
if (e.key === 'Escape') { e.preventDefault(); state.renamingId = null; renderRows(); }
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* 列表滚动时收起浮动菜单 */
|
||||||
|
listEl.addEventListener('scroll', () => {
|
||||||
|
if (state.menuOpenId) { closeMenu(); renderRows(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
事件:入口按钮 / 外部点击 / Esc
|
||||||
|
============================================================ */
|
||||||
|
entryBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setPopoverOpen(!state.popoverOpen);
|
||||||
|
});
|
||||||
|
|
||||||
|
popover.addEventListener('click', (e) => { e.stopPropagation(); });
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (state.menuOpenId && !(menuEl && menuEl.contains(e.target))) {
|
||||||
|
closeMenu();
|
||||||
|
renderRows();
|
||||||
|
}
|
||||||
|
if (state.popoverOpen && !popover.contains(e.target) && !entryBtn.contains(e.target)) {
|
||||||
|
setPopoverOpen(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key !== 'Escape') return;
|
||||||
|
if (state.menuOpenId) { closeMenu(); renderRows(); return; }
|
||||||
|
if (state.renamingId || state.deletingId) { state.renamingId = null; state.deletingId = null; renderRows(); return; }
|
||||||
|
if (state.createOpen) { closeCreateForm(false); return; }
|
||||||
|
if (state.popoverOpen) setPopoverOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (state.popoverOpen) positionPopover();
|
||||||
|
if (state.menuOpenId) { closeMenu(); renderRows(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Demo 外框:主题切换 / 模式切换
|
||||||
|
============================================================ */
|
||||||
|
const themeBtns = document.querySelectorAll('[data-theme-btn]');
|
||||||
|
themeBtns.forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.documentElement.setAttribute('data-theme', btn.dataset.themeBtn);
|
||||||
|
themeBtns.forEach((b) => b.classList.toggle('active', b === btn));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const modeBtns = document.querySelectorAll('[data-mode-btn]');
|
||||||
|
modeBtns.forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const mode = btn.dataset.modeBtn;
|
||||||
|
if (mode === state.mode) return;
|
||||||
|
state.mode = mode;
|
||||||
|
modeBtns.forEach((b) => b.classList.toggle('active', b === btn));
|
||||||
|
|
||||||
|
/* 切换模式:重置数据与瞬时态 */
|
||||||
|
state.workspaces = cloneData(mode);
|
||||||
|
state.currentId = state.workspaces[0] ? state.workspaces[0].workspace_id : '';
|
||||||
|
state.defaultId = state.currentId;
|
||||||
|
closeTransient();
|
||||||
|
if (state.createOpen) closeCreateForm(true);
|
||||||
|
|
||||||
|
/* 文案切换 */
|
||||||
|
headerTitle.textContent = noun();
|
||||||
|
entryLabel.textContent = noun();
|
||||||
|
createBtnText.textContent = '新建' + noun();
|
||||||
|
createName.placeholder = isDocker() ? '项目名称' : '工作区名称(可选)';
|
||||||
|
createPath.style.display = isDocker() ? 'none' : '';
|
||||||
|
popover.setAttribute('aria-label', noun());
|
||||||
|
popover.classList.toggle('docker', isDocker());
|
||||||
|
|
||||||
|
setStatus(isDocker()
|
||||||
|
? 'Docker 模式:叫「项目」,不显示路径,「在文件夹中打开」禁用'
|
||||||
|
: '宿主机模式:显示路径,可在文件夹中打开');
|
||||||
|
renderRows();
|
||||||
|
positionPopover();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- 初始化 ---------- */
|
||||||
|
renderRows();
|
||||||
|
setPopoverOpen(true);
|
||||||
|
})();
|
||||||
157
demo/workspace_menu_redesign/index.html
Normal file
157
demo/workspace_menu_redesign/index.html
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN" data-theme="classic">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>工作区菜单重设计 Demo</title>
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- ========== Demo 外框(仅 demo 用,非产品部分) ========== -->
|
||||||
|
<header class="demo-chrome">
|
||||||
|
<div class="demo-chrome-title">
|
||||||
|
<strong>管理工作区菜单 · 重设计 Demo</strong>
|
||||||
|
<span>对话侧边栏内嵌的工作区切换 + 管理浮层</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-chrome-controls">
|
||||||
|
<div class="demo-theme-switch" role="group" aria-label="运行模式切换">
|
||||||
|
<button type="button" data-mode-btn="host" class="active">宿主机</button>
|
||||||
|
<button type="button" data-mode-btn="docker">Docker</button>
|
||||||
|
</div>
|
||||||
|
<div class="demo-theme-switch" role="group" aria-label="主题切换">
|
||||||
|
<button type="button" data-theme-btn="classic" class="active">经典</button>
|
||||||
|
<button type="button" data-theme-btn="light">浅色</button>
|
||||||
|
<button type="button" data-theme-btn="dark">深色</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="demo-stage">
|
||||||
|
<!-- ========== 产品画布:侧边栏 + 浮层 ========== -->
|
||||||
|
<section class="stage-canvas" id="canvas">
|
||||||
|
<aside class="sidebar-mock">
|
||||||
|
<nav class="side-nav">
|
||||||
|
<button type="button" class="side-nav-row">
|
||||||
|
<span class="side-icon">
|
||||||
|
<svg viewBox="0 0 28 28" fill="none" stroke="currentColor" stroke-width="1.45" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5 6.5c0-1.38 1.12-2.5 2.5-2.5h13c1.38 0 2.5 1.12 2.5 2.5v8.5c0 1.38-1.12 2.5-2.5 2.5h-5.6l-3.4 3.2.6-3.2H7.5c-1.38 0-2.5-1.12-2.5-2.5V6.5z" />
|
||||||
|
<path d="M9 9.5h10" /><path d="M9 13h6" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="side-label">对话记录</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="side-nav-row">
|
||||||
|
<span class="side-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" />
|
||||||
|
<path d="m15 5 4 4" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="side-label">新建对话</span>
|
||||||
|
</button>
|
||||||
|
<!-- 新入口:工作区(替代原「工作区 / 虚拟显示器」两个按钮) -->
|
||||||
|
<button type="button" class="side-nav-row workspace-entry active" id="workspaceEntry" aria-expanded="true">
|
||||||
|
<span class="side-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z" />
|
||||||
|
<path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65" />
|
||||||
|
<path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="side-label" id="entryLabel">工作区</span>
|
||||||
|
<span class="side-current" id="entryCurrentName">默认工作区</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="side-search">
|
||||||
|
<span class="side-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="11" cy="11" r="7" /><path d="m21 21-4.3-4.3" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input type="text" placeholder="搜索对话" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="side-list">
|
||||||
|
<div class="conv-row active"><span class="conv-title">前端侧边栏紧凑化改造</span><span class="conv-dot"></span></div>
|
||||||
|
<div class="conv-row"><span class="conv-title">工作区菜单交互稿评审</span></div>
|
||||||
|
<div class="conv-row"><span class="conv-title">API 轮询延迟排查记录</span></div>
|
||||||
|
<div class="conv-row"><span class="conv-title">Android 客户端发版 checklist</span></div>
|
||||||
|
<div class="conv-row"><span class="conv-title">子智能体通知池设计</span></div>
|
||||||
|
<div class="conv-row"><span class="conv-title">主题 token 收敛方案</span></div>
|
||||||
|
<div class="conv-row"><span class="conv-title">部署脚本 venv 修复</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="side-footer">
|
||||||
|
<button type="button" class="side-nav-row">
|
||||||
|
<span class="side-icon">
|
||||||
|
<svg viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-7 9a7 7 0 1 1 14 0H3z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="side-label">个人空间</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- ========== 工作区浮层(本次重设计主体) ========== -->
|
||||||
|
<div class="ws-popover" id="wsPopover" role="dialog" aria-label="工作区">
|
||||||
|
<div class="ws-header">
|
||||||
|
<span class="ws-header-title" id="headerTitle">工作区</span>
|
||||||
|
<span class="ws-header-count" id="wsCount"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ws-list" id="wsList"><!-- 行由 JS 渲染 --></div>
|
||||||
|
|
||||||
|
<!-- 底部区域:新建按钮 ⇄ 新建表单,容器高度动画切换 -->
|
||||||
|
<div class="ws-bottom" id="wsBottom">
|
||||||
|
<div class="ws-footer" id="wsFooter">
|
||||||
|
<button type="button" class="ws-create-btn" id="createBtn">
|
||||||
|
<span class="ws-create-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5 12h14" /><path d="M12 5v14" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span id="createBtnText">新建工作区</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="ws-create-form" id="createForm" hidden>
|
||||||
|
<input type="text" id="createName" placeholder="工作区名称(可选)" autocomplete="off" />
|
||||||
|
<input type="text" id="createPath" placeholder="路径:绝对路径或相对仓库路径" autocomplete="off" />
|
||||||
|
<div class="ws-create-actions">
|
||||||
|
<button type="button" class="ws-btn ghost" id="createCancel">取消</button>
|
||||||
|
<button type="submit" class="ws-btn primary">创建</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- demo 操作反馈条 -->
|
||||||
|
<div class="stage-status" id="stageStatus">点击工作区行可切换;hover 行尾出现「…」菜单</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ========== 设计说明 ========== -->
|
||||||
|
<aside class="stage-notes">
|
||||||
|
<h2>设计决策</h2>
|
||||||
|
<ul>
|
||||||
|
<li><b>入口合并</b>:侧边栏新增「工作区」入口(行尾常显当前工作区名),替代原「工作区 / 虚拟显示器」按钮与左侧面板里的管理入口。</li>
|
||||||
|
<li><b>列表即切换器</b>:工作区行是可选项,单击整行切换;当前行用中性底色标识。</li>
|
||||||
|
<li><b>管理动作收拢</b>:设为默认 / 重命名 / 在文件夹中打开 / 删除收进每行 hover 的「…」菜单;菜单用 fixed 浮层定位(同个人空间二级菜单),不被浮层裁切。</li>
|
||||||
|
<li><b>在文件夹中打开</b>:复用输入栏 git 状态区的现成能力,仅宿主机模式可用,Docker 模式下禁用。</li>
|
||||||
|
<li><b>全内联操作</b>:新建 / 重命名 / 删除确认都在浮层内联完成;新建区域带高度展开动画。</li>
|
||||||
|
<li><b>反套娃</b>:单层扁平容器 + 细发丝分隔线;行内不再出现任何边框卡片。</li>
|
||||||
|
<li><b>对齐</b>:行结构统一;名称 13px / 路径 12px 单行截断;图标与首行文字视觉对齐。</li>
|
||||||
|
<li><b>token 化</b>:颜色全部走三主题语义 token,hover / 选中用中性灰,accent 仅用于「创建」CTA。</li>
|
||||||
|
</ul>
|
||||||
|
<h2>模式差异</h2>
|
||||||
|
<ul class="notes-compare">
|
||||||
|
<li>宿主机模式:显示路径行,菜单含「在文件夹中打开」。</li>
|
||||||
|
<li>Docker 模式:叫「项目」,不显示路径,「在文件夹中打开」禁用,新建只需名称。</li>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
666
demo/workspace_menu_redesign/style.css
Normal file
666
demo/workspace_menu_redesign/style.css
Normal file
@ -0,0 +1,666 @@
|
|||||||
|
/* ============================================================
|
||||||
|
工作区菜单重设计 Demo — 样式
|
||||||
|
颜色全部走三主题语义 token(与 static/src/styles/base/_tokens.scss 对齐)
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* ---------- 主题 token(取自项目真实值) ---------- */
|
||||||
|
:root[data-theme='classic'] {
|
||||||
|
--surface-base: #faf9f5;
|
||||||
|
--surface-panel: #faf9f5;
|
||||||
|
--surface-rail: #f5f0e8;
|
||||||
|
--surface-sidebar: #f5f0e8;
|
||||||
|
--surface-card: #efe9de;
|
||||||
|
--surface-raised: #ffffff;
|
||||||
|
--surface-soft: #f5f0e8;
|
||||||
|
--surface-muted: #e8e0d2;
|
||||||
|
--text-primary: #141413;
|
||||||
|
--text-secondary: #3d3d3a;
|
||||||
|
--text-tertiary: #6c6a64;
|
||||||
|
--text-muted: rgba(108, 106, 100, 0.55);
|
||||||
|
--border-default: #e6dfd8;
|
||||||
|
--border-strong: #d8cfc4;
|
||||||
|
--border-card-strong: #e6dfd8;
|
||||||
|
--accent: #cc785c;
|
||||||
|
--accent-strong: #a9583e;
|
||||||
|
--accent-hover: #bd6a4f;
|
||||||
|
--accent-active: #a9583e;
|
||||||
|
--on-accent: #ffffff;
|
||||||
|
--state-success: #10b981;
|
||||||
|
--state-warning: #f59e0b;
|
||||||
|
--state-danger: #dc2626;
|
||||||
|
--state-danger-strong: #b91c1c;
|
||||||
|
--shadow-pop: 0 1px 2px rgba(60, 50, 40, 0.06), 0 12px 32px rgba(60, 50, 40, 0.1);
|
||||||
|
--shadow-card: 0 1px 2px rgba(60, 50, 40, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='light'] {
|
||||||
|
--surface-base: #ffffff;
|
||||||
|
--surface-panel: #ffffff;
|
||||||
|
--surface-rail: #f9f9f9;
|
||||||
|
--surface-sidebar: #f9f9f9;
|
||||||
|
--surface-card: #ffffff;
|
||||||
|
--surface-raised: #ffffff;
|
||||||
|
--surface-soft: #f9f9f9;
|
||||||
|
--surface-muted: #f3f3f3;
|
||||||
|
--text-primary: #0d0d0d;
|
||||||
|
--text-secondary: #5d5d5d;
|
||||||
|
--text-tertiary: #8f8f8f;
|
||||||
|
--text-muted: rgba(13, 13, 13, 0.35);
|
||||||
|
--border-default: rgba(13, 13, 13, 0.1);
|
||||||
|
--border-strong: rgba(13, 13, 13, 0.15);
|
||||||
|
--border-card-strong: rgba(13, 13, 13, 0.1);
|
||||||
|
--accent: #181818;
|
||||||
|
--accent-strong: #181818;
|
||||||
|
--accent-hover: #303030;
|
||||||
|
--accent-active: #414141;
|
||||||
|
--on-accent: #ffffff;
|
||||||
|
--state-success: #10b981;
|
||||||
|
--state-warning: #f59e0b;
|
||||||
|
--state-danger: #dc2626;
|
||||||
|
--state-danger-strong: #b91c1c;
|
||||||
|
--shadow-pop: 0 1px 2px rgba(0, 0, 0, 0.05), 0 12px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='dark'] {
|
||||||
|
--surface-base: #1a1a1a;
|
||||||
|
--surface-panel: #1a1a1a;
|
||||||
|
--surface-rail: #181818;
|
||||||
|
--surface-sidebar: #181818;
|
||||||
|
--surface-card: #0a0a0a;
|
||||||
|
--surface-raised: #242424;
|
||||||
|
--surface-soft: #0f0f0f;
|
||||||
|
--surface-muted: #141414;
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
--text-secondary: #a0a0a0;
|
||||||
|
--text-tertiary: #707070;
|
||||||
|
--text-muted: rgba(255, 255, 255, 0.3);
|
||||||
|
--border-default: rgba(255, 255, 255, 0.08);
|
||||||
|
--border-strong: rgba(255, 255, 255, 0.12);
|
||||||
|
--border-card-strong: rgba(255, 255, 255, 0.1);
|
||||||
|
--accent: #606060;
|
||||||
|
--accent-strong: #505050;
|
||||||
|
--accent-hover: #707070;
|
||||||
|
--accent-active: #505050;
|
||||||
|
--on-accent: #ffffff;
|
||||||
|
--state-success: #10b981;
|
||||||
|
--state-warning: #f59e0b;
|
||||||
|
--state-danger: #dc2626;
|
||||||
|
--state-danger-strong: #b91c1c;
|
||||||
|
--shadow-pop: 0 1px 2px rgba(0, 0, 0, 0.4), 0 12px 32px rgba(0, 0, 0, 0.5);
|
||||||
|
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 基础 ---------- */
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Segoe UI', 'Helvetica Neue', sans-serif;
|
||||||
|
background: var(--surface-base);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
transition: background 160ms ease, color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font: inherit;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg { display: block; width: 100%; height: 100%; }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Demo 外框(非产品部分)
|
||||||
|
============================================================ */
|
||||||
|
.demo-chrome {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 14px 20px;
|
||||||
|
border-bottom: 1px solid var(--border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-chrome-title { display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
.demo-chrome-title strong { font-size: 14px; font-weight: 600; }
|
||||||
|
.demo-chrome-title span { font-size: 12px; color: var(--text-tertiary); }
|
||||||
|
|
||||||
|
.demo-chrome-controls { display: flex; align-items: center; gap: 10px; }
|
||||||
|
|
||||||
|
.demo-theme-switch {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-theme-switch button {
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-theme-switch button.active {
|
||||||
|
background: var(--surface-raised);
|
||||||
|
color: var(--text-primary);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-stage {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 300px;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-canvas {
|
||||||
|
position: relative;
|
||||||
|
min-height: 560px;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--surface-panel);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-status {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 34px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-top: 1px solid var(--border-default);
|
||||||
|
background: var(--surface-soft);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
侧边栏 Mock(紧凑化后的未来形态)
|
||||||
|
============================================================ */
|
||||||
|
.sidebar-mock {
|
||||||
|
width: 232px;
|
||||||
|
height: 526px; /* 560 - 状态条 34 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--surface-sidebar);
|
||||||
|
border-right: 1px solid var(--border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav { padding: 8px; display: flex; flex-direction: column; gap: 1px; }
|
||||||
|
|
||||||
|
.side-nav-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
height: 30px; /* 紧凑化:原 ~36px */
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 6px; /* 紧凑化:原 ~10px */
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12.5px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav-row:hover { background: color-mix(in srgb, var(--text-primary) 5%, transparent); }
|
||||||
|
|
||||||
|
.side-nav-row.active {
|
||||||
|
background: color-mix(in srgb, var(--text-primary) 7%, transparent);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-icon {
|
||||||
|
flex: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-icon svg { width: 15px; height: 15px; }
|
||||||
|
|
||||||
|
.side-label { flex: none; }
|
||||||
|
|
||||||
|
.side-current {
|
||||||
|
margin-left: auto;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 88px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
margin: 0 8px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-search .side-icon svg { width: 13px; height: 13px; }
|
||||||
|
.side-search input { flex: 1; min-width: 0; font-size: 12px; }
|
||||||
|
.side-search input::placeholder { color: var(--text-muted); }
|
||||||
|
|
||||||
|
.side-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px; /* 与左右间距一致,hover 圆角四边均匀 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conv-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
height: 27px; /* 紧凑化:原 ~34px */
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conv-row:hover { background: color-mix(in srgb, var(--text-primary) 4%, transparent); }
|
||||||
|
|
||||||
|
.conv-row.active {
|
||||||
|
background: color-mix(in srgb, var(--text-primary) 7%, transparent);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conv-title {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conv-dot {
|
||||||
|
flex: none;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--state-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-footer { padding: 8px; border-top: 1px solid var(--border-default); }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
工作区浮层(重设计主体)
|
||||||
|
============================================================ */
|
||||||
|
.ws-popover {
|
||||||
|
--ws-row-h: 50px;
|
||||||
|
position: absolute;
|
||||||
|
width: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--surface-raised);
|
||||||
|
box-shadow: var(--shadow-pop);
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px) scale(0.98);
|
||||||
|
transform-origin: top left;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 120ms ease, transform 150ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-popover.open {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-popover.docker { --ws-row-h: 38px; }
|
||||||
|
|
||||||
|
/* 头部:安静的类型标题,发丝线分隔 */
|
||||||
|
.ws-header {
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-bottom: 1px solid var(--border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-header-title { font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
|
||||||
|
.ws-header-count { font-size: 11px; color: var(--text-tertiary); }
|
||||||
|
|
||||||
|
/* 列表:最多显示 5 个工作区,超出内部滚动;padding 四边一致保证 hover 圆角均匀 */
|
||||||
|
.ws-list {
|
||||||
|
flex: none;
|
||||||
|
max-height: calc(var(--ws-row-h) * 5 + 16px); /* 5行 + 4条1px间隔 + 上下各6px padding */
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--border-strong) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-list::-webkit-scrollbar { width: 8px; }
|
||||||
|
.ws-list::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-strong);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid var(--surface-raised);
|
||||||
|
}
|
||||||
|
.ws-list::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
|
||||||
|
/* 工作区行:可选项,固定高度,宿主机两行 / Docker 单行 */
|
||||||
|
.ws-row {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--ws-row-h);
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row:hover { background: color-mix(in srgb, var(--text-primary) 4%, transparent); }
|
||||||
|
|
||||||
|
.ws-row.current { background: color-mix(in srgb, var(--text-primary) 6%, transparent); }
|
||||||
|
.ws-row.current:hover { background: color-mix(in srgb, var(--text-primary) 7%, transparent); }
|
||||||
|
|
||||||
|
.ws-row-icon {
|
||||||
|
flex: none;
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
margin-top: -13px; /* 与首行文字视觉对齐,而非两行整体居中 */
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row.docker .ws-row-icon { margin-top: 0; }
|
||||||
|
|
||||||
|
.ws-row.current .ws-row-icon { color: var(--text-secondary); }
|
||||||
|
|
||||||
|
.ws-row-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row-path {
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row-meta {
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 行尾「…」:仅 hover / 菜单展开时出现 */
|
||||||
|
.ws-more {
|
||||||
|
flex: none;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-more svg { width: 15px; height: 15px; }
|
||||||
|
.ws-more:hover { color: var(--text-primary); background: color-mix(in srgb, var(--text-primary) 7%, transparent); }
|
||||||
|
|
||||||
|
.ws-row:hover .ws-more,
|
||||||
|
.ws-row.menu-open .ws-more { display: flex; }
|
||||||
|
|
||||||
|
/* 「…」二级菜单:fixed 浮层,逃逸浮层裁切(同个人空间二级菜单方案) */
|
||||||
|
.ws-menu-floating {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 3000;
|
||||||
|
min-width: 136px;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--surface-raised);
|
||||||
|
box-shadow: var(--shadow-pop);
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-3px) scale(0.98);
|
||||||
|
transform-origin: top right;
|
||||||
|
transition: opacity 110ms ease, transform 140ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-menu-floating.open { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
|
||||||
|
.ws-menu-floating button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-menu-floating button:hover { background: color-mix(in srgb, var(--text-primary) 5%, transparent); color: var(--text-primary); }
|
||||||
|
.ws-menu-floating button.danger { color: var(--state-danger); }
|
||||||
|
.ws-menu-floating button.danger:hover { background: color-mix(in srgb, var(--state-danger) 8%, transparent); }
|
||||||
|
.ws-menu-floating button:disabled { color: var(--text-muted); cursor: default; }
|
||||||
|
.ws-menu-floating button:disabled:hover { background: none; color: var(--text-muted); }
|
||||||
|
|
||||||
|
.ws-menu-sep { height: 1px; margin: 4px 6px; background: var(--border-default); }
|
||||||
|
|
||||||
|
/* 重命名内联输入:替换名称文字 */
|
||||||
|
.ws-rename-input {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 1px solid var(--accent);
|
||||||
|
padding: 1px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除确认内联:整行内容替换 */
|
||||||
|
.ws-confirm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-confirm-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-confirm-actions { display: flex; gap: 8px; }
|
||||||
|
|
||||||
|
/* 通用小按钮 */
|
||||||
|
.ws-btn {
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-btn:hover { background: color-mix(in srgb, var(--text-primary) 5%, transparent); }
|
||||||
|
|
||||||
|
.ws-btn.primary {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--on-accent);
|
||||||
|
}
|
||||||
|
.ws-btn.primary:hover { background: var(--accent-hover); }
|
||||||
|
|
||||||
|
.ws-btn.danger {
|
||||||
|
background: var(--state-danger);
|
||||||
|
border-color: var(--state-danger);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.ws-btn.danger:hover { background: var(--state-danger-strong); }
|
||||||
|
|
||||||
|
.ws-btn.ghost { border-color: var(--border-default); }
|
||||||
|
|
||||||
|
/* 底部区域:新建按钮 ⇄ 新建表单,容器高度动画 */
|
||||||
|
.ws-bottom {
|
||||||
|
flex: none;
|
||||||
|
border-top: 1px solid var(--border-default);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: height 200ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-footer { padding: 6px; transition: opacity 130ms ease; }
|
||||||
|
.ws-footer.fading { opacity: 0; }
|
||||||
|
|
||||||
|
.ws-create-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-btn:hover { background: color-mix(in srgb, var(--text-primary) 4%, transparent); color: var(--text-primary); }
|
||||||
|
|
||||||
|
.ws-create-icon { flex: none; width: 15px; height: 15px; color: var(--text-tertiary); }
|
||||||
|
.ws-create-btn:hover .ws-create-icon { color: var(--text-secondary); }
|
||||||
|
|
||||||
|
/* 新建表单:内联,平铺无卡片 */
|
||||||
|
.ws-create-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
.ws-create-form.fading { opacity: 0; }
|
||||||
|
|
||||||
|
.ws-create-form input {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
background: var(--surface-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-form input:focus { border-color: var(--accent); }
|
||||||
|
.ws-create-form input::placeholder { color: var(--text-muted); }
|
||||||
|
|
||||||
|
.ws-create-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||||
|
|
||||||
|
/* 隐藏互斥切换 */
|
||||||
|
.ws-footer[hidden], .ws-create-form[hidden] { display: none; }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
设计说明栏
|
||||||
|
============================================================ */
|
||||||
|
.stage-notes {
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--surface-soft);
|
||||||
|
padding: 16px 18px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-notes h2 {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin: 14px 0 8px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-notes h2:first-child { margin-top: 0; }
|
||||||
|
|
||||||
|
.stage-notes ul { list-style: none; display: flex; flex-direction: column; gap: 7px; }
|
||||||
|
.stage-notes li { padding-left: 12px; position: relative; line-height: 1.55; }
|
||||||
|
.stage-notes li::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 8px;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--text-muted);
|
||||||
|
}
|
||||||
|
.stage-notes li b { color: var(--text-primary); font-weight: 600; }
|
||||||
|
|
||||||
|
.notes-compare li { color: var(--text-tertiary); }
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.demo-stage { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
@ -66,11 +66,13 @@
|
|||||||
:current-task-in-progress="taskInProgress"
|
:current-task-in-progress="taskInProgress"
|
||||||
:current-conversation-id="currentConversationId"
|
:current-conversation-id="currentConversationId"
|
||||||
:current-workspace-id="currentHostWorkspaceId"
|
:current-workspace-id="currentHostWorkspaceId"
|
||||||
:display-mode="chatDisplayMode"
|
:default-workspace-id="defaultHostWorkspaceId"
|
||||||
:display-mode-disabled="displayModeSwitchDisabled"
|
|
||||||
:workspaces="hostWorkspaces"
|
:workspaces="hostWorkspaces"
|
||||||
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
|
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
|
||||||
:host-workspace-enabled="versioningHostMode || dockerProjectMode"
|
:host-workspace-enabled="versioningHostMode || dockerProjectMode"
|
||||||
|
:workspace-busy="hostWorkspaceSwitching || hostWorkspaceManageSubmitting"
|
||||||
|
:workspace-create-submitting="hostWorkspaceCreateSubmitting"
|
||||||
|
:workspace-error="hostWorkspaceCreateError"
|
||||||
:group-by-workspace="personalizationStore?.form?.group_sidebar_by_workspace"
|
:group-by-workspace="personalizationStore?.form?.group_sidebar_by_workspace"
|
||||||
:versioning-host-mode="versioningHostMode"
|
:versioning-host-mode="versioningHostMode"
|
||||||
@toggle="toggleSidebar"
|
@toggle="toggleSidebar"
|
||||||
@ -84,8 +86,10 @@
|
|||||||
@personal="openPersonalPage"
|
@personal="openPersonalPage"
|
||||||
@delete="deleteConversation"
|
@delete="deleteConversation"
|
||||||
@duplicate="duplicateConversation"
|
@duplicate="duplicateConversation"
|
||||||
@toggle-workspace="handleWorkspaceToggle"
|
@switch-workspace="handleHostWorkspaceSwitch"
|
||||||
@toggle-display-mode="handleDisplayModeToggle"
|
@create-workspace="submitHostWorkspaceCreateFromManage"
|
||||||
|
@delete-workspace="handleDeleteWorkspaceFromSwitcher"
|
||||||
|
@set-default-workspace="setDefaultHostWorkspace"
|
||||||
@select-workspace-conversation="handleSelectWorkspaceConversation"
|
@select-workspace-conversation="handleSelectWorkspaceConversation"
|
||||||
@create-workspace-conversation="createWorkspaceConversation"
|
@create-workspace-conversation="createWorkspaceConversation"
|
||||||
@reveal-workspace="revealHostWorkspace"
|
@reveal-workspace="revealHostWorkspace"
|
||||||
@ -93,47 +97,9 @@
|
|||||||
@pin-workspace="handlePinWorkspaceFromSidebar"
|
@pin-workspace="handlePinWorkspaceFromSidebar"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="!isMobileViewport" class="workspace-region">
|
|
||||||
<LeftPanel
|
|
||||||
ref="leftPanel"
|
|
||||||
class="workspace-panel"
|
|
||||||
:class="{ 'workspace-panel--collapsed': workspaceCollapsed }"
|
|
||||||
:width="leftWidth"
|
|
||||||
:collapsed="workspaceCollapsed"
|
|
||||||
:icon-style="iconStyle"
|
|
||||||
:agent-version="agentVersion"
|
|
||||||
:thinking-mode="thinkingMode"
|
|
||||||
:run-mode="resolvedRunMode"
|
|
||||||
:is-connected="isConnected"
|
|
||||||
:panel-menu-open="panelMenuOpen"
|
|
||||||
:panel-mode="panelMode"
|
|
||||||
:file-manager-disabled="policyUiBlocks.block_file_manager"
|
|
||||||
:host-workspace-enabled="versioningHostMode || dockerProjectMode"
|
|
||||||
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
|
|
||||||
:host-workspaces="hostWorkspaces"
|
|
||||||
:host-workspace-id="currentHostWorkspaceId"
|
|
||||||
:host-workspace-default-id="defaultHostWorkspaceId"
|
|
||||||
:host-workspace-switching="hostWorkspaceSwitching"
|
|
||||||
@toggle-panel-menu="togglePanelMenu"
|
|
||||||
@select-panel="selectPanelMode"
|
|
||||||
@open-file-manager="openGuiFileManager"
|
|
||||||
@toggle-thinking-mode="handleQuickModeToggle"
|
|
||||||
@switch-host-workspace="handleHostWorkspaceSwitch"
|
|
||||||
@create-host-workspace="handleCreateHostWorkspace"
|
|
||||||
@manage-host-workspace="openHostWorkspaceManageDialog"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="!workspaceCollapsed"
|
|
||||||
class="resize-handle"
|
|
||||||
@mousedown="startResize('left', $event)"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main
|
<main
|
||||||
class="chat-container"
|
class="chat-container"
|
||||||
:class="{
|
:class="{
|
||||||
'chat-container--immersive': workspaceCollapsed,
|
|
||||||
'chat-container--mobile': isMobileViewport,
|
'chat-container--mobile': isMobileViewport,
|
||||||
'chat-container--app-shell': isAppShell,
|
'chat-container--app-shell': isAppShell,
|
||||||
'chat-container--monitor': chatDisplayMode === 'monitor',
|
'chat-container--monitor': chatDisplayMode === 'monitor',
|
||||||
@ -351,6 +317,7 @@
|
|||||||
@send-message="sendMessage"
|
@send-message="sendMessage"
|
||||||
@send-or-stop="handleSendOrStop"
|
@send-or-stop="handleSendOrStop"
|
||||||
@quick-upload="handleQuickUpload"
|
@quick-upload="handleQuickUpload"
|
||||||
|
@open-file-manager="openGuiFileManager"
|
||||||
@toggle-tool-menu="toggleToolMenu"
|
@toggle-tool-menu="toggleToolMenu"
|
||||||
@toggle-mode-menu="toggleModeMenu"
|
@toggle-mode-menu="toggleModeMenu"
|
||||||
@toggle-model-menu="toggleModelMenu"
|
@toggle-model-menu="toggleModelMenu"
|
||||||
@ -386,11 +353,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<QuickDock
|
<QuickDock
|
||||||
v-if="!isMobileViewport && !personalizationStore?.form?.hide_quick_dock"
|
v-if="!isMobileViewport"
|
||||||
:host-mode="versioningHostMode"
|
:host-mode="versioningHostMode"
|
||||||
/>
|
/>
|
||||||
<FilePreviewPanel
|
<FilePreviewPanel
|
||||||
v-if="!isMobileViewport && !personalizationStore?.form?.hide_quick_dock"
|
v-if="!isMobileViewport"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="!isMobileViewport && (terminalPanelOpen || gitChangesPanelOpen)"
|
v-if="!isMobileViewport && (terminalPanelOpen || gitChangesPanelOpen)"
|
||||||
@ -556,38 +523,6 @@
|
|||||||
@confirm="confirmVersioningRestore"
|
@confirm="confirmVersioningRestore"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="overlay-fade">
|
|
||||||
<HostWorkspaceCreateDialog
|
|
||||||
v-if="hostWorkspaceCreateDialogOpen"
|
|
||||||
:open="hostWorkspaceCreateDialogOpen"
|
|
||||||
:path-value="hostWorkspaceCreatePath"
|
|
||||||
:label-value="hostWorkspaceCreateLabel"
|
|
||||||
:submitting="hostWorkspaceCreateSubmitting"
|
|
||||||
:error-message="hostWorkspaceCreateError"
|
|
||||||
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
|
|
||||||
@close="closeHostWorkspaceCreateDialog"
|
|
||||||
@submit="submitHostWorkspaceCreate"
|
|
||||||
@update:path-value="hostWorkspaceCreatePath = $event"
|
|
||||||
@update:label-value="hostWorkspaceCreateLabel = $event"
|
|
||||||
/>
|
|
||||||
</transition>
|
|
||||||
<transition name="overlay-fade">
|
|
||||||
<HostWorkspaceManageDialog
|
|
||||||
v-if="hostWorkspaceManageDialogOpen"
|
|
||||||
:workspaces="hostWorkspaces"
|
|
||||||
:current-workspace-id="currentHostWorkspaceId"
|
|
||||||
:default-workspace-id="defaultHostWorkspaceId"
|
|
||||||
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
|
|
||||||
:busy="hostWorkspaceCreateSubmitting || hostWorkspaceManageSubmitting"
|
|
||||||
:create-submitting="hostWorkspaceCreateSubmitting"
|
|
||||||
:error-message="hostWorkspaceCreateError"
|
|
||||||
@close="closeHostWorkspaceManageDialog"
|
|
||||||
@create="submitHostWorkspaceCreateFromManage"
|
|
||||||
@rename="renameHostWorkspace"
|
|
||||||
@delete="deleteHostWorkspace"
|
|
||||||
@set-default="setDefaultHostWorkspace"
|
|
||||||
/>
|
|
||||||
</transition>
|
|
||||||
<TutorialOverlay v-if="tutorialStore.running" />
|
<TutorialOverlay v-if="tutorialStore.running" />
|
||||||
<NewUserTutorialPrompt
|
<NewUserTutorialPrompt
|
||||||
:visible="tutorialPromptVisible"
|
:visible="tutorialPromptVisible"
|
||||||
@ -664,21 +599,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<div class="item-label">对话记录</div>
|
<div class="item-label">对话记录</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="dropdown-item mobile-menu-item"
|
|
||||||
data-tutorial="mobile-menu-workspace"
|
|
||||||
aria-label="工作文件"
|
|
||||||
@click="openMobileOverlay('workspace')"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="mobile-menu-icon"
|
|
||||||
:src="mobileMenuIcons.workspace"
|
|
||||||
alt=""
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<div class="item-label">工作文件</div>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="dropdown-item mobile-menu-item mobile-menu-item--personal"
|
class="dropdown-item mobile-menu-item mobile-menu-item--personal"
|
||||||
@ -790,11 +710,13 @@
|
|||||||
:current-task-in-progress="taskInProgress"
|
:current-task-in-progress="taskInProgress"
|
||||||
:current-conversation-id="currentConversationId"
|
:current-conversation-id="currentConversationId"
|
||||||
:current-workspace-id="currentHostWorkspaceId"
|
:current-workspace-id="currentHostWorkspaceId"
|
||||||
:display-mode="chatDisplayMode"
|
:default-workspace-id="defaultHostWorkspaceId"
|
||||||
:display-mode-disabled="displayModeSwitchDisabled"
|
|
||||||
:workspaces="hostWorkspaces"
|
:workspaces="hostWorkspaces"
|
||||||
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
|
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
|
||||||
:host-workspace-enabled="versioningHostMode || dockerProjectMode"
|
:host-workspace-enabled="versioningHostMode || dockerProjectMode"
|
||||||
|
:workspace-busy="hostWorkspaceSwitching || hostWorkspaceManageSubmitting"
|
||||||
|
:workspace-create-submitting="hostWorkspaceCreateSubmitting"
|
||||||
|
:workspace-error="hostWorkspaceCreateError"
|
||||||
:group-by-workspace="personalizationStore?.form?.group_sidebar_by_workspace"
|
:group-by-workspace="personalizationStore?.form?.group_sidebar_by_workspace"
|
||||||
:versioning-host-mode="versioningHostMode"
|
:versioning-host-mode="versioningHostMode"
|
||||||
:show-collapse-button="true"
|
:show-collapse-button="true"
|
||||||
@ -810,7 +732,10 @@
|
|||||||
@personal="openPersonalPage"
|
@personal="openPersonalPage"
|
||||||
@delete="deleteConversation"
|
@delete="deleteConversation"
|
||||||
@duplicate="duplicateConversation"
|
@duplicate="duplicateConversation"
|
||||||
@toggle-display-mode="handleDisplayModeToggle"
|
@switch-workspace="handleHostWorkspaceSwitch"
|
||||||
|
@create-workspace="submitHostWorkspaceCreateFromManage"
|
||||||
|
@delete-workspace="handleDeleteWorkspaceFromSwitcher"
|
||||||
|
@set-default-workspace="setDefaultHostWorkspace"
|
||||||
@select-workspace-conversation="handleSelectWorkspaceConversation"
|
@select-workspace-conversation="handleSelectWorkspaceConversation"
|
||||||
@create-workspace-conversation="createWorkspaceConversation"
|
@create-workspace-conversation="createWorkspaceConversation"
|
||||||
@reveal-workspace="revealHostWorkspace"
|
@reveal-workspace="revealHostWorkspace"
|
||||||
@ -821,49 +746,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<transition name="mobile-panel-overlay">
|
|
||||||
<div
|
|
||||||
v-if="isMobileViewport && activeMobileOverlay === 'workspace'"
|
|
||||||
class="mobile-panel-overlay mobile-panel-overlay--left"
|
|
||||||
@click.self="closeMobileOverlay"
|
|
||||||
>
|
|
||||||
<div class="mobile-panel-sheet mobile-panel-sheet--workspace">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="mobile-overlay-close"
|
|
||||||
data-tutorial="mobile-workspace-close"
|
|
||||||
aria-label="关闭面板"
|
|
||||||
@click="closeMobileOverlay"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
<LeftPanel
|
|
||||||
class="mobile-overlay-content"
|
|
||||||
:width="Math.min(leftWidth, 420)"
|
|
||||||
:icon-style="iconStyle"
|
|
||||||
:agent-version="agentVersion"
|
|
||||||
:thinking-mode="thinkingMode"
|
|
||||||
:run-mode="resolvedRunMode"
|
|
||||||
:is-connected="isConnected"
|
|
||||||
:panel-menu-open="panelMenuOpen"
|
|
||||||
:panel-mode="panelMode"
|
|
||||||
:host-workspace-enabled="versioningHostMode || dockerProjectMode"
|
|
||||||
:workspace-kind="versioningHostMode ? 'workspace' : 'project'"
|
|
||||||
:host-workspaces="hostWorkspaces"
|
|
||||||
:host-workspace-id="currentHostWorkspaceId"
|
|
||||||
:host-workspace-default-id="defaultHostWorkspaceId"
|
|
||||||
:host-workspace-switching="hostWorkspaceSwitching"
|
|
||||||
@toggle-panel-menu="togglePanelMenu"
|
|
||||||
@select-panel="selectPanelMode"
|
|
||||||
@open-file-manager="openGuiFileManager"
|
|
||||||
@switch-host-workspace="handleHostWorkspaceSwitch"
|
|
||||||
@create-host-workspace="handleCreateHostWorkspace"
|
|
||||||
@manage-host-workspace="openHostWorkspaceManageDialog"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<transition name="mobile-panel-overlay">
|
<transition name="mobile-panel-overlay">
|
||||||
<div
|
<div
|
||||||
v-if="isMobileViewport && activeMobileOverlay === 'approval'"
|
v-if="isMobileViewport && activeMobileOverlay === 'approval'"
|
||||||
@ -932,7 +814,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
const mobileMenuIcons = {
|
const mobileMenuIcons = {
|
||||||
workspace: new URL('../icons/folder.svg', import.meta.url).href,
|
|
||||||
personal: new URL('../icons/user.svg', import.meta.url).href,
|
personal: new URL('../icons/user.svg', import.meta.url).href,
|
||||||
newChat: new URL('../icons/pencil.svg', import.meta.url).href,
|
newChat: new URL('../icons/pencil.svg', import.meta.url).href,
|
||||||
refresh: new URL('../icons/refresh-cw.svg', import.meta.url).href
|
refresh: new URL('../icons/refresh-cw.svg', import.meta.url).href
|
||||||
|
|||||||
@ -56,12 +56,7 @@ const appOptions = {
|
|||||||
...mapActions(useUiStore, {
|
...mapActions(useUiStore, {
|
||||||
uiToggleSidebar: 'toggleSidebar',
|
uiToggleSidebar: 'toggleSidebar',
|
||||||
uiSetSidebarCollapsed: 'setSidebarCollapsed',
|
uiSetSidebarCollapsed: 'setSidebarCollapsed',
|
||||||
uiSetWorkspaceCollapsed: 'setWorkspaceCollapsed',
|
|
||||||
uiToggleWorkspaceCollapsed: 'toggleWorkspaceCollapsed',
|
|
||||||
uiSetChatDisplayMode: 'setChatDisplayMode',
|
uiSetChatDisplayMode: 'setChatDisplayMode',
|
||||||
uiSetPanelMode: 'setPanelMode',
|
|
||||||
uiSetPanelMenuOpen: 'setPanelMenuOpen',
|
|
||||||
uiTogglePanelMenu: 'togglePanelMenu',
|
|
||||||
uiSetMobileViewport: 'setIsMobileViewport',
|
uiSetMobileViewport: 'setIsMobileViewport',
|
||||||
uiSetMobileOverlayMenuOpen: 'setMobileOverlayMenuOpen',
|
uiSetMobileOverlayMenuOpen: 'setMobileOverlayMenuOpen',
|
||||||
uiToggleMobileOverlayMenu: 'toggleMobileOverlayMenu',
|
uiToggleMobileOverlayMenu: 'toggleMobileOverlayMenu',
|
||||||
@ -186,14 +181,10 @@ const appOptions = {
|
|||||||
forceUnlockMonitor: 'resetVisualState'
|
forceUnlockMonitor: 'resetVisualState'
|
||||||
}),
|
}),
|
||||||
...mapActions(useSubAgentStore, {
|
...mapActions(useSubAgentStore, {
|
||||||
subAgentFetch: 'fetchSubAgents',
|
subAgentFetch: 'fetchSubAgents'
|
||||||
subAgentStartPolling: 'startPolling',
|
|
||||||
subAgentStopPolling: 'stopPolling'
|
|
||||||
}),
|
}),
|
||||||
...mapActions(useBackgroundCommandStore, {
|
...mapActions(useBackgroundCommandStore, {
|
||||||
backgroundCommandFetch: 'fetchCommands',
|
backgroundCommandFetch: 'fetchCommands'
|
||||||
backgroundCommandStartPolling: 'startPolling',
|
|
||||||
backgroundCommandStopPolling: 'stopPolling'
|
|
||||||
}),
|
}),
|
||||||
...mapActions(useFocusStore, {
|
...mapActions(useFocusStore, {
|
||||||
focusFetchFiles: 'fetchFocusedFiles',
|
focusFetchFiles: 'fetchFocusedFiles',
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
import ChatArea from '../components/chat/ChatArea.vue';
|
import ChatArea from '../components/chat/ChatArea.vue';
|
||||||
import ConversationSidebar from '../components/sidebar/ConversationSidebar.vue';
|
import ConversationSidebar from '../components/sidebar/ConversationSidebar.vue';
|
||||||
import LeftPanel from '../components/panels/LeftPanel.vue';
|
|
||||||
import ToolApprovalPanel from '../components/panels/ToolApprovalPanel.vue';
|
import ToolApprovalPanel from '../components/panels/ToolApprovalPanel.vue';
|
||||||
import GitChangesPanel from '../components/panels/GitChangesPanel.vue';
|
import GitChangesPanel from '../components/panels/GitChangesPanel.vue';
|
||||||
import TerminalPanel from '../components/panels/TerminalPanel.vue';
|
import TerminalPanel from '../components/panels/TerminalPanel.vue';
|
||||||
@ -28,12 +27,6 @@ const BackgroundCommandDialog = defineAsyncComponent(
|
|||||||
const VersioningDialog = defineAsyncComponent(
|
const VersioningDialog = defineAsyncComponent(
|
||||||
() => import('../components/overlay/VersioningDialog.vue')
|
() => import('../components/overlay/VersioningDialog.vue')
|
||||||
);
|
);
|
||||||
const HostWorkspaceCreateDialog = defineAsyncComponent(
|
|
||||||
() => import('../components/overlay/HostWorkspaceCreateDialog.vue')
|
|
||||||
);
|
|
||||||
const HostWorkspaceManageDialog = defineAsyncComponent(
|
|
||||||
() => import('../components/overlay/HostWorkspaceManageDialog.vue')
|
|
||||||
);
|
|
||||||
const UserQuestionDialog = defineAsyncComponent(
|
const UserQuestionDialog = defineAsyncComponent(
|
||||||
() => import('../components/overlay/UserQuestionDialog.vue')
|
() => import('../components/overlay/UserQuestionDialog.vue')
|
||||||
);
|
);
|
||||||
@ -50,7 +43,6 @@ const GoalProgressDialog = defineAsyncComponent(
|
|||||||
export const appComponents = {
|
export const appComponents = {
|
||||||
ChatArea,
|
ChatArea,
|
||||||
ConversationSidebar,
|
ConversationSidebar,
|
||||||
LeftPanel,
|
|
||||||
ToolApprovalPanel,
|
ToolApprovalPanel,
|
||||||
GitChangesPanel,
|
GitChangesPanel,
|
||||||
TerminalPanel,
|
TerminalPanel,
|
||||||
@ -66,8 +58,6 @@ export const appComponents = {
|
|||||||
SubAgentActivityDialog,
|
SubAgentActivityDialog,
|
||||||
BackgroundCommandDialog,
|
BackgroundCommandDialog,
|
||||||
VersioningDialog,
|
VersioningDialog,
|
||||||
HostWorkspaceCreateDialog,
|
|
||||||
HostWorkspaceManageDialog,
|
|
||||||
UserQuestionDialog,
|
UserQuestionDialog,
|
||||||
TutorialOverlay,
|
TutorialOverlay,
|
||||||
NewUserTutorialPrompt,
|
NewUserTutorialPrompt,
|
||||||
|
|||||||
@ -67,11 +67,7 @@ export const computed = {
|
|||||||
...mapState(useFileStore, ['contextMenu', 'fileTree', 'expandedFolders', 'todoList']),
|
...mapState(useFileStore, ['contextMenu', 'fileTree', 'expandedFolders', 'todoList']),
|
||||||
...mapWritableState(useUiStore, [
|
...mapWritableState(useUiStore, [
|
||||||
'sidebarCollapsed',
|
'sidebarCollapsed',
|
||||||
'workspaceCollapsed',
|
|
||||||
'chatDisplayMode',
|
'chatDisplayMode',
|
||||||
'panelMode',
|
|
||||||
'panelMenuOpen',
|
|
||||||
'leftWidth',
|
|
||||||
'rightWidth',
|
'rightWidth',
|
||||||
'rightCollapsed',
|
'rightCollapsed',
|
||||||
'isResizing',
|
'isResizing',
|
||||||
|
|||||||
@ -70,7 +70,6 @@ export async function mounted() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('click', this.handleClickOutsideQuickMenu);
|
document.addEventListener('click', this.handleClickOutsideQuickMenu);
|
||||||
document.addEventListener('click', this.handleClickOutsidePanelMenu);
|
|
||||||
document.addEventListener('click', this.handleClickOutsideHeaderMenu);
|
document.addEventListener('click', this.handleClickOutsideHeaderMenu);
|
||||||
document.addEventListener('click', this.handleClickOutsideMobileMenu);
|
document.addEventListener('click', this.handleClickOutsideMobileMenu);
|
||||||
document.addEventListener('click', this.handleCopyCodeClick);
|
document.addEventListener('click', this.handleCopyCodeClick);
|
||||||
@ -80,9 +79,7 @@ export async function mounted() {
|
|||||||
this.setupMobileViewportWatcher();
|
this.setupMobileViewportWatcher();
|
||||||
|
|
||||||
this.subAgentFetch();
|
this.subAgentFetch();
|
||||||
this.subAgentStartPolling();
|
|
||||||
this.backgroundCommandFetch();
|
this.backgroundCommandFetch();
|
||||||
this.backgroundCommandStartPolling();
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.autoResizeInput();
|
this.autoResizeInput();
|
||||||
@ -112,7 +109,6 @@ export function beforeUnmount() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.removeEventListener('click', this.handleClickOutsideQuickMenu);
|
document.removeEventListener('click', this.handleClickOutsideQuickMenu);
|
||||||
document.removeEventListener('click', this.handleClickOutsidePanelMenu);
|
|
||||||
document.removeEventListener('click', this.handleClickOutsideHeaderMenu);
|
document.removeEventListener('click', this.handleClickOutsideHeaderMenu);
|
||||||
document.removeEventListener('click', this.handleClickOutsideMobileMenu);
|
document.removeEventListener('click', this.handleClickOutsideMobileMenu);
|
||||||
document.removeEventListener('click', this.handleCopyCodeClick);
|
document.removeEventListener('click', this.handleCopyCodeClick);
|
||||||
@ -120,8 +116,6 @@ export function beforeUnmount() {
|
|||||||
window.removeEventListener('keydown', this.handleMobileOverlayEscape);
|
window.removeEventListener('keydown', this.handleMobileOverlayEscape);
|
||||||
window.removeEventListener('beforeunload', this.handleBeforeUnloadDraftPersist);
|
window.removeEventListener('beforeunload', this.handleBeforeUnloadDraftPersist);
|
||||||
this.teardownMobileViewportWatcher();
|
this.teardownMobileViewportWatcher();
|
||||||
this.subAgentStopPolling();
|
|
||||||
this.backgroundCommandStopPolling();
|
|
||||||
this.stopProjectGitSummaryIdleRefresh?.();
|
this.stopProjectGitSummaryIdleRefresh?.();
|
||||||
this.stopTerminalCountIdleRefresh?.();
|
this.stopTerminalCountIdleRefresh?.();
|
||||||
this.resourceStopContainerStatsPolling();
|
this.resourceStopContainerStatsPolling();
|
||||||
|
|||||||
@ -30,20 +30,6 @@ export const dialogMethods = {
|
|||||||
}
|
}
|
||||||
this.personalizationOpenDrawer();
|
this.personalizationOpenDrawer();
|
||||||
},
|
},
|
||||||
closeHostWorkspaceManageDialog() {
|
|
||||||
if (this.hostWorkspaceCreateSubmitting || this.hostWorkspaceManageSubmitting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.hostWorkspaceManageDialogOpen = false;
|
|
||||||
this.hostWorkspaceCreateError = '';
|
|
||||||
},
|
|
||||||
closeHostWorkspaceCreateDialog() {
|
|
||||||
if (this.hostWorkspaceCreateSubmitting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.hostWorkspaceCreateDialogOpen = false;
|
|
||||||
this.hostWorkspaceCreateError = '';
|
|
||||||
},
|
|
||||||
minimizeUserQuestionDialog() {
|
minimizeUserQuestionDialog() {
|
||||||
if (!Array.isArray(this.pendingUserQuestions) || !this.pendingUserQuestions.length) {
|
if (!Array.isArray(this.pendingUserQuestions) || !this.pendingUserQuestions.length) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -154,26 +154,6 @@ export const hostWorkspaceMethods = {
|
|||||||
this.hostWorkspaceSwitching = false;
|
this.hostWorkspaceSwitching = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async handleCreateHostWorkspace() {
|
|
||||||
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.hostWorkspaceSwitching || this.hostWorkspaceCreateSubmitting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.hostWorkspaceCreatePath = '';
|
|
||||||
this.hostWorkspaceCreateLabel = '';
|
|
||||||
this.hostWorkspaceCreateError = '';
|
|
||||||
this.hostWorkspaceCreateDialogOpen = true;
|
|
||||||
},
|
|
||||||
async openHostWorkspaceManageDialog() {
|
|
||||||
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.hostWorkspaceCreateError = '';
|
|
||||||
await this.fetchHostWorkspaces();
|
|
||||||
this.hostWorkspaceManageDialogOpen = true;
|
|
||||||
},
|
|
||||||
async submitHostWorkspaceCreate() {
|
async submitHostWorkspaceCreate() {
|
||||||
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
||||||
return;
|
return;
|
||||||
@ -279,7 +259,11 @@ export const hostWorkspaceMethods = {
|
|||||||
this.hostWorkspaceManageSubmitting = false;
|
this.hostWorkspaceManageSubmitting = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async deleteHostWorkspace(item) {
|
async handleDeleteWorkspaceFromSwitcher(item) {
|
||||||
|
/* 切换浮层已内联确认,跳过全局确认弹窗 */
|
||||||
|
await this.deleteHostWorkspace(item, { skipConfirm: true });
|
||||||
|
},
|
||||||
|
async deleteHostWorkspace(item, opts = {}) {
|
||||||
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
if (!(this.versioningHostMode || this.dockerProjectMode)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -287,7 +271,7 @@ export const hostWorkspaceMethods = {
|
|||||||
if (!workspaceId) return;
|
if (!workspaceId) return;
|
||||||
const kindLabel = this.versioningHostMode ? '工作区' : '项目';
|
const kindLabel = this.versioningHostMode ? '工作区' : '项目';
|
||||||
const wasCurrent = workspaceId === this.currentHostWorkspaceId;
|
const wasCurrent = workspaceId === this.currentHostWorkspaceId;
|
||||||
const ok = await this.confirmAction({
|
const ok = opts.skipConfirm ? true : await this.confirmAction({
|
||||||
title: `删除${kindLabel}`,
|
title: `删除${kindLabel}`,
|
||||||
message: this.versioningHostMode
|
message: this.versioningHostMode
|
||||||
? `确定要从列表中删除“${item?.label || workspaceId}”吗?不会删除磁盘上的工作区文件夹。`
|
? `确定要从列表中删除“${item?.label || workspaceId}”吗?不会删除磁盘上的工作区文件夹。`
|
||||||
|
|||||||
@ -75,16 +75,5 @@ export const menuMethods = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.closeHeaderMenu();
|
this.closeHeaderMenu();
|
||||||
},
|
|
||||||
handleClickOutsidePanelMenu(event) {
|
|
||||||
if (!this.panelMenuOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const leftPanel = this.$refs.leftPanel;
|
|
||||||
const wrapper = leftPanel && leftPanel.panelMenuWrapper ? leftPanel.panelMenuWrapper : null;
|
|
||||||
if (wrapper && wrapper.contains(event.target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.uiSetPanelMenuOpen(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -31,24 +31,6 @@ export const panelMethods = {
|
|||||||
}
|
}
|
||||||
this.uiToggleSidebar();
|
this.uiToggleSidebar();
|
||||||
},
|
},
|
||||||
togglePanelMenu() {
|
|
||||||
this.uiTogglePanelMenu();
|
|
||||||
},
|
|
||||||
selectPanelMode(mode) {
|
|
||||||
this.uiSetPanelMode(mode);
|
|
||||||
this.uiSetPanelMenuOpen(false);
|
|
||||||
if (mode === 'subAgents') {
|
|
||||||
// 切换到子智能体面板时立即刷新,避免等待轮询间隔
|
|
||||||
this.subAgentFetch();
|
|
||||||
this.subAgentStartPolling();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mode === 'backgroundCommands') {
|
|
||||||
// 切换到后台指令面板时立即刷新,避免等待轮询间隔
|
|
||||||
this.backgroundCommandFetch();
|
|
||||||
this.backgroundCommandStartPolling();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleFocusPanel() {
|
toggleFocusPanel() {
|
||||||
this.rightCollapsed = !this.rightCollapsed;
|
this.rightCollapsed = !this.rightCollapsed;
|
||||||
if (!this.rightCollapsed && this.rightWidth < this.minPanelWidth) {
|
if (!this.rightCollapsed && this.rightWidth < this.minPanelWidth) {
|
||||||
@ -138,35 +120,6 @@ export const panelMethods = {
|
|||||||
console.log('[UI_DEBUG] handleTokenPanelToggleClick: calling toggleTokenPanel');
|
console.log('[UI_DEBUG] handleTokenPanelToggleClick: calling toggleTokenPanel');
|
||||||
this.toggleTokenPanel();
|
this.toggleTokenPanel();
|
||||||
},
|
},
|
||||||
handleWorkspaceToggle() {
|
|
||||||
if (this.isMobileViewport) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.isPolicyBlocked('collapse_workspace', '工作区已被管理员强制折叠')) {
|
|
||||||
this.uiSetWorkspaceCollapsed(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextState = !this.workspaceCollapsed;
|
|
||||||
this.uiSetWorkspaceCollapsed(nextState);
|
|
||||||
if (nextState) {
|
|
||||||
this.uiSetPanelMenuOpen(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleDisplayModeToggle() {
|
|
||||||
if (this.displayModeSwitchDisabled) {
|
|
||||||
// 显式提示管理员禁用
|
|
||||||
this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this.chatDisplayMode === 'chat' &&
|
|
||||||
this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const next = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat';
|
|
||||||
this.uiSetChatDisplayMode(next);
|
|
||||||
},
|
|
||||||
getMessagesAreaElement() {
|
getMessagesAreaElement() {
|
||||||
const ref = this.$refs.messagesArea;
|
const ref = this.$refs.messagesArea;
|
||||||
if (!ref) {
|
if (!ref) {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { debugLog } from '../common';
|
import { debugLog } from '../common';
|
||||||
|
import { persistWorkspaceMode } from '../../state';
|
||||||
import { usePolicyStore } from '../../../stores/policy';
|
import { usePolicyStore } from '../../../stores/policy';
|
||||||
import { useModelStore } from '../../../stores/model';
|
import { useModelStore } from '../../../stores/model';
|
||||||
import { usePersonalizationStore } from '../../../stores/personalization';
|
import { usePersonalizationStore } from '../../../stores/personalization';
|
||||||
@ -307,13 +308,13 @@ export const socketMethods = {
|
|||||||
const isHostMode = statusData?.container?.mode === 'host';
|
const isHostMode = statusData?.container?.mode === 'host';
|
||||||
this.versioningHostMode = !!isHostMode;
|
this.versioningHostMode = !!isHostMode;
|
||||||
this.dockerProjectMode = !isHostMode;
|
this.dockerProjectMode = !isHostMode;
|
||||||
|
persistWorkspaceMode(!!isHostMode);
|
||||||
if (isHostMode) {
|
if (isHostMode) {
|
||||||
this.fileMarkTreeUnavailable('宿主机模式下文件树不可用');
|
this.fileMarkTreeUnavailable('宿主机模式下文件树不可用');
|
||||||
await this.fetchHostWorkspaces();
|
await this.fetchHostWorkspaces();
|
||||||
} else {
|
} else {
|
||||||
this.fileMarkTreeUnavailable('Docker 模式下文件区已改为项目列表');
|
this.fileMarkTreeUnavailable('Docker 模式下文件区已改为项目列表');
|
||||||
await this.fetchHostWorkspaces();
|
await this.fetchHostWorkspaces();
|
||||||
this.hostWorkspaceCreateDialogOpen = false;
|
|
||||||
this.hostWorkspaceCreatePath = '';
|
this.hostWorkspaceCreatePath = '';
|
||||||
this.hostWorkspaceCreateLabel = '';
|
this.hostWorkspaceCreateLabel = '';
|
||||||
this.hostWorkspaceCreateError = '';
|
this.hostWorkspaceCreateError = '';
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { debugLog } from './common';
|
import { debugLog } from './common';
|
||||||
|
import { persistWorkspaceMode } from '../state';
|
||||||
|
|
||||||
const normalizeTrackingMode = (value: any): 'workspace_and_conversation' | 'conversation_only' => {
|
const normalizeTrackingMode = (value: any): 'workspace_and_conversation' | 'conversation_only' => {
|
||||||
return String(value || '').toLowerCase() === 'conversation_only'
|
return String(value || '').toLowerCase() === 'conversation_only'
|
||||||
@ -23,6 +24,7 @@ export const versioningMethods = {
|
|||||||
}
|
}
|
||||||
const payload = data.data || {};
|
const payload = data.data || {};
|
||||||
this.versioningHostMode = !!payload.host_mode;
|
this.versioningHostMode = !!payload.host_mode;
|
||||||
|
persistWorkspaceMode(!!payload.host_mode);
|
||||||
this.versioningEnabled = !!payload.enabled;
|
this.versioningEnabled = !!payload.enabled;
|
||||||
this.versioningMode = 'overwrite';
|
this.versioningMode = 'overwrite';
|
||||||
this.versioningRestoreMode = 'overwrite';
|
this.versioningRestoreMode = 'overwrite';
|
||||||
|
|||||||
@ -1,6 +1,30 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { ICONS, TOOL_CATEGORY_ICON_MAP } from '../utils/icons';
|
import { ICONS, TOOL_CATEGORY_ICON_MAP } from '../utils/icons';
|
||||||
|
|
||||||
|
/* host/docker 模式缓存:模式是部署级属性几乎不变,首屏用缓存值渲染
|
||||||
|
工作区入口按钮(按钮显隐依赖这两个 flag),socket 连接后校正并回写,
|
||||||
|
避免按钮在初始化完成前「延迟零点几秒才出现」。 */
|
||||||
|
const WORKSPACE_MODE_STORAGE_KEY = 'agents_workspace_mode';
|
||||||
|
|
||||||
|
const loadCachedWorkspaceMode = (): 'host' | 'docker' | null => {
|
||||||
|
try {
|
||||||
|
const v = window.localStorage.getItem(WORKSPACE_MODE_STORAGE_KEY);
|
||||||
|
return v === 'host' || v === 'docker' ? v : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const persistWorkspaceMode = (isHostMode: boolean): void => {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(WORKSPACE_MODE_STORAGE_KEY, isHostMode ? 'host' : 'docker');
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedWorkspaceMode = loadCachedWorkspaceMode();
|
||||||
|
|
||||||
export function dataState() {
|
export function dataState() {
|
||||||
return {
|
return {
|
||||||
// 路由相关
|
// 路由相关
|
||||||
@ -150,18 +174,16 @@ export function dataState() {
|
|||||||
pathAuthorizationReadableDraft: '',
|
pathAuthorizationReadableDraft: '',
|
||||||
pathAuthorizationDraft: '',
|
pathAuthorizationDraft: '',
|
||||||
pathAuthorizationSaving: false,
|
pathAuthorizationSaving: false,
|
||||||
versioningHostMode: false,
|
versioningHostMode: cachedWorkspaceMode === 'host',
|
||||||
dockerProjectMode: false,
|
dockerProjectMode: cachedWorkspaceMode === 'docker',
|
||||||
hostWorkspaces: [],
|
hostWorkspaces: [],
|
||||||
currentHostWorkspaceId: '',
|
currentHostWorkspaceId: '',
|
||||||
defaultHostWorkspaceId: '',
|
defaultHostWorkspaceId: '',
|
||||||
hostWorkspaceSwitching: false,
|
hostWorkspaceSwitching: false,
|
||||||
hostWorkspaceCreateDialogOpen: false,
|
|
||||||
hostWorkspaceCreatePath: '',
|
hostWorkspaceCreatePath: '',
|
||||||
hostWorkspaceCreateLabel: '',
|
hostWorkspaceCreateLabel: '',
|
||||||
hostWorkspaceCreateSubmitting: false,
|
hostWorkspaceCreateSubmitting: false,
|
||||||
hostWorkspaceCreateError: '',
|
hostWorkspaceCreateError: '',
|
||||||
hostWorkspaceManageDialogOpen: false,
|
|
||||||
hostWorkspaceManageSubmitting: false,
|
hostWorkspaceManageSubmitting: false,
|
||||||
versioningEnabled: false,
|
versioningEnabled: false,
|
||||||
versioningTrackingMode: 'conversation_only',
|
versioningTrackingMode: 'conversation_only',
|
||||||
|
|||||||
@ -364,6 +364,7 @@
|
|||||||
:multi-agent-mode="multiAgentMode"
|
:multi-agent-mode="multiAgentMode"
|
||||||
:has-running-task="hasRunningTask"
|
:has-running-task="hasRunningTask"
|
||||||
@quick-upload="triggerQuickUpload"
|
@quick-upload="triggerQuickUpload"
|
||||||
|
@open-file-manager="$emit('open-file-manager')"
|
||||||
@pick-images="$emit('pick-images')"
|
@pick-images="$emit('pick-images')"
|
||||||
@pick-video="$emit('pick-video')"
|
@pick-video="$emit('pick-video')"
|
||||||
@toggle-tool-menu="$emit('toggle-tool-menu')"
|
@toggle-tool-menu="$emit('toggle-tool-menu')"
|
||||||
@ -532,6 +533,7 @@ const emit = defineEmits([
|
|||||||
'send-message',
|
'send-message',
|
||||||
'send-or-stop',
|
'send-or-stop',
|
||||||
'quick-upload',
|
'quick-upload',
|
||||||
|
'open-file-manager',
|
||||||
'pick-images',
|
'pick-images',
|
||||||
'pick-video',
|
'pick-video',
|
||||||
'toggle-tool-menu',
|
'toggle-tool-menu',
|
||||||
@ -2432,7 +2434,7 @@ const hasComposerContent = computed(() => {
|
|||||||
|
|
||||||
const showStopIcon = computed(() => {
|
const showStopIcon = computed(() => {
|
||||||
// 停止按钮只看主对话是否在 streaming。后台任务(子智能体/后台指令)的停止
|
// 停止按钮只看主对话是否在 streaming。后台任务(子智能体/后台指令)的停止
|
||||||
// 有独立的“x 个子智能体运行中”按钮和 LeftPanel 后台指令停止按钮,不再集成在停止按钮上。
|
// 有独立的“x 个子智能体运行中”按钮和快捷窗口里的后台指令停止入口,不再集成在停止按钮上。
|
||||||
return !!props.streamingMessage && !hasComposerContent.value;
|
return !!props.streamingMessage && !hasComposerContent.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="quick-menu">
|
<transition name="quick-menu">
|
||||||
<div v-if="open" class="quick-menu" @click.stop>
|
<div v-if="open" class="quick-menu" @click.stop>
|
||||||
|
<div class="quick-menu-list">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="menu-entry"
|
class="menu-entry"
|
||||||
@ -10,6 +11,15 @@
|
|||||||
>
|
>
|
||||||
{{ uploading ? '上传中...' : '上传文件' }}
|
{{ uploading ? '上传中...' : '上传文件' }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="menu-entry"
|
||||||
|
data-tutorial="quick-file-manager"
|
||||||
|
@click.stop="$emit('open-file-manager')"
|
||||||
|
:disabled="!isConnected"
|
||||||
|
>
|
||||||
|
文件管理
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="menu-entry"
|
class="menu-entry"
|
||||||
@ -87,6 +97,7 @@
|
|||||||
设置
|
设置
|
||||||
<span class="entry-arrow">›</span>
|
<span class="entry-arrow">›</span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<transition name="submenu-slide">
|
<transition name="submenu-slide">
|
||||||
<div class="quick-submenu tool-submenu" v-if="toolMenuOpen">
|
<div class="quick-submenu tool-submenu" v-if="toolMenuOpen">
|
||||||
@ -220,6 +231,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(event: 'quick-upload'): void;
|
(event: 'quick-upload'): void;
|
||||||
|
(event: 'open-file-manager'): void;
|
||||||
(event: 'toggle-tool-menu'): void;
|
(event: 'toggle-tool-menu'): void;
|
||||||
(event: 'toggle-settings'): void;
|
(event: 'toggle-settings'): void;
|
||||||
(event: 'update-tool-category', id: string, enabled: boolean): void;
|
(event: 'update-tool-category', id: string, enabled: boolean): void;
|
||||||
@ -254,6 +266,25 @@ const goalCompleted = computed(() => String(props.goalProgress?.status || '').to
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.quick-menu-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
max-height: 194px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: color-mix(in srgb, var(--text-primary) 22%, transparent) transparent;
|
||||||
|
}
|
||||||
|
.quick-menu-list::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
.quick-menu-list::-webkit-scrollbar-thumb {
|
||||||
|
background: color-mix(in srgb, var(--text-primary) 22%, transparent);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.quick-menu-list::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
.submenu-desc {
|
.submenu-desc {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@ -1,220 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="host-workspace-dialog-overlay"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
:aria-label="workspaceKind === 'project' ? '新建项目' : '新建工作区'"
|
|
||||||
@click.self="handleOverlayClose"
|
|
||||||
>
|
|
||||||
<form class="host-workspace-dialog" @submit.prevent="$emit('submit')">
|
|
||||||
<div class="host-workspace-dialog__header">
|
|
||||||
<div class="host-workspace-dialog__title">
|
|
||||||
{{ workspaceKind === 'project' ? '新建项目' : '新建工作区' }}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="host-workspace-dialog__close"
|
|
||||||
:disabled="submitting"
|
|
||||||
@click="$emit('close')"
|
|
||||||
>
|
|
||||||
关闭
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="host-workspace-dialog__body">
|
|
||||||
<label v-if="workspaceKind !== 'project'" class="host-workspace-dialog__field">
|
|
||||||
<span class="host-workspace-dialog__label">
|
|
||||||
工作区路径
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
:value="pathValue"
|
|
||||||
type="text"
|
|
||||||
class="host-workspace-dialog__input"
|
|
||||||
placeholder="请输入绝对路径或相对仓库路径"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="submitting"
|
|
||||||
@input="$emit('update:pathValue', ($event.target as HTMLInputElement).value)"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="host-workspace-dialog__field">
|
|
||||||
<span class="host-workspace-dialog__label">
|
|
||||||
{{ workspaceKind === 'project' ? '项目名称' : '工作区名称(可选)' }}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
:value="labelValue"
|
|
||||||
type="text"
|
|
||||||
class="host-workspace-dialog__input"
|
|
||||||
:placeholder="workspaceKind === 'project' ? '例如:客户 A 项目' : '例如:客户A项目'"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="submitting"
|
|
||||||
@input="$emit('update:labelValue', ($event.target as HTMLInputElement).value)"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div v-if="errorMessage" class="host-workspace-dialog__error">{{ errorMessage }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="host-workspace-dialog__actions">
|
|
||||||
<button type="button" class="host-workspace-dialog__btn ghost" :disabled="submitting" @click="$emit('close')">
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="host-workspace-dialog__btn primary" :disabled="submitting">
|
|
||||||
{{ submitting ? '创建中...' : (workspaceKind === 'project' ? '创建项目' : '创建工作区') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({ name: 'HostWorkspaceCreateDialog' });
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
open: boolean;
|
|
||||||
pathValue: string;
|
|
||||||
labelValue: string;
|
|
||||||
submitting?: boolean;
|
|
||||||
errorMessage?: string;
|
|
||||||
workspaceKind?: 'workspace' | 'project';
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: 'close'): void;
|
|
||||||
(event: 'submit'): void;
|
|
||||||
(event: 'update:pathValue', value: string): void;
|
|
||||||
(event: 'update:labelValue', value: string): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const workspaceKind = props.workspaceKind || 'workspace';
|
|
||||||
|
|
||||||
const handleOverlayClose = () => {
|
|
||||||
if (props.submitting) return;
|
|
||||||
emits('close');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.host-workspace-dialog-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1300;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--theme-overlay-scrim);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog {
|
|
||||||
width: min(520px, 96vw);
|
|
||||||
border-radius: 18px;
|
|
||||||
border: 1px solid var(--theme-control-border-strong);
|
|
||||||
background: var(--theme-surface-card);
|
|
||||||
box-shadow: var(--theme-shadow-soft);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px 18px;
|
|
||||||
border-bottom: 1px solid var(--theme-control-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--claude-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__close {
|
|
||||||
border: 1px solid var(--theme-control-border-strong);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__close:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__body {
|
|
||||||
padding: 16px 18px;
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__field {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__input {
|
|
||||||
border: 1px solid var(--theme-control-border-strong);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--theme-surface-soft);
|
|
||||||
color: var(--claude-text);
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.4;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__input:focus {
|
|
||||||
border-color: var(--claude-accent);
|
|
||||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--claude-accent) 20%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__input:disabled {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__error {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--state-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__actions {
|
|
||||||
padding: 16px 18px;
|
|
||||||
border-top: 1px solid var(--theme-control-border);
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__btn {
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
border: 1px solid var(--theme-control-border-strong);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__btn.ghost {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__btn.primary {
|
|
||||||
background: var(--claude-accent);
|
|
||||||
border-color: var(--claude-accent-strong);
|
|
||||||
color: var(--on-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__btn:disabled {
|
|
||||||
opacity: 0.65;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,409 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="host-workspace-dialog-overlay"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
:aria-label="workspaceKind === 'project' ? '管理项目' : '管理工作区'"
|
|
||||||
@click.self="handleOverlayClose"
|
|
||||||
>
|
|
||||||
<form class="host-workspace-dialog host-workspace-manage" @submit.prevent="submitCreate">
|
|
||||||
<div class="host-workspace-dialog__header">
|
|
||||||
<div class="host-workspace-dialog__title">
|
|
||||||
{{ workspaceKind === 'project' ? '管理项目' : '管理工作区' }}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="host-workspace-dialog__close"
|
|
||||||
:disabled="busy"
|
|
||||||
@click="$emit('close')"
|
|
||||||
>
|
|
||||||
关闭
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="host-workspace-dialog__body">
|
|
||||||
<section class="workspace-manage-section">
|
|
||||||
<div class="workspace-manage-section__title">
|
|
||||||
{{ workspaceKind === 'project' ? '新建项目' : '新建工作区' }}
|
|
||||||
</div>
|
|
||||||
<label v-if="workspaceKind !== 'project'" class="host-workspace-dialog__field">
|
|
||||||
<span class="host-workspace-dialog__label">工作区路径</span>
|
|
||||||
<input
|
|
||||||
v-model="createPath"
|
|
||||||
type="text"
|
|
||||||
class="host-workspace-dialog__input"
|
|
||||||
placeholder="请输入绝对路径或相对仓库路径"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="busy"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label class="host-workspace-dialog__field">
|
|
||||||
<span class="host-workspace-dialog__label">
|
|
||||||
{{ workspaceKind === 'project' ? '项目名称' : '工作区名称(可选)' }}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
v-model="createLabel"
|
|
||||||
type="text"
|
|
||||||
class="host-workspace-dialog__input"
|
|
||||||
:placeholder="workspaceKind === 'project' ? '例如:客户 A 项目' : '例如:客户A项目'"
|
|
||||||
autocomplete="off"
|
|
||||||
:disabled="busy"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button type="submit" class="host-workspace-dialog__btn primary" :disabled="busy">
|
|
||||||
{{ createSubmitting ? '创建中...' : (workspaceKind === 'project' ? '新建项目' : '新建工作区') }}
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="workspace-manage-section">
|
|
||||||
<div class="workspace-manage-section__title">
|
|
||||||
{{ workspaceKind === 'project' ? '已有项目' : '已有工作区' }}
|
|
||||||
</div>
|
|
||||||
<div v-if="workspaces.length" class="workspace-manage-list">
|
|
||||||
<div
|
|
||||||
v-for="item in workspaces"
|
|
||||||
:key="item.workspace_id"
|
|
||||||
class="workspace-manage-row"
|
|
||||||
:class="{ current: item.workspace_id === currentWorkspaceId }"
|
|
||||||
>
|
|
||||||
<div class="workspace-manage-main">
|
|
||||||
<input
|
|
||||||
class="host-workspace-dialog__input workspace-manage-name"
|
|
||||||
:value="renameDrafts[item.workspace_id] ?? item.label"
|
|
||||||
:disabled="busy"
|
|
||||||
@input="setRenameDraft(item.workspace_id, ($event.target as HTMLInputElement).value)"
|
|
||||||
/>
|
|
||||||
<div v-if="workspaceKind !== 'project'" class="workspace-manage-path">
|
|
||||||
{{ item.path || '(未配置路径)' }}
|
|
||||||
</div>
|
|
||||||
<div class="workspace-manage-badges">
|
|
||||||
<span v-if="item.workspace_id === defaultWorkspaceId" class="workspace-manage-badge default">
|
|
||||||
默认
|
|
||||||
</span>
|
|
||||||
<span v-if="Number(item.running_task_count || 0) > 0" class="workspace-manage-badge running">
|
|
||||||
运行中
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="workspace-manage-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="host-workspace-dialog__btn ghost"
|
|
||||||
:class="{ active: item.workspace_id === defaultWorkspaceId }"
|
|
||||||
:disabled="busy || item.workspace_id === defaultWorkspaceId"
|
|
||||||
@click="submitSetDefault(item)"
|
|
||||||
>
|
|
||||||
{{ item.workspace_id === defaultWorkspaceId ? '已默认' : '设为默认' }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="host-workspace-dialog__btn ghost"
|
|
||||||
:disabled="busy || !hasRenameChange(item)"
|
|
||||||
@click="submitRename(item)"
|
|
||||||
>
|
|
||||||
重命名
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="host-workspace-dialog__btn danger"
|
|
||||||
:disabled="busy || !canDelete(item)"
|
|
||||||
@click="$emit('delete', item)"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="workspace-manage-empty">
|
|
||||||
{{ workspaceKind === 'project' ? '暂无项目' : '暂无工作区' }}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div v-if="errorMessage" class="host-workspace-dialog__error">{{ errorMessage }}</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
defineOptions({ name: 'HostWorkspaceManageDialog' });
|
|
||||||
|
|
||||||
type WorkspaceItem = {
|
|
||||||
workspace_id: string;
|
|
||||||
label: string;
|
|
||||||
path?: string;
|
|
||||||
running_task_count?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
workspaces: WorkspaceItem[];
|
|
||||||
currentWorkspaceId?: string;
|
|
||||||
defaultWorkspaceId?: string;
|
|
||||||
workspaceKind?: 'workspace' | 'project';
|
|
||||||
busy?: boolean;
|
|
||||||
createSubmitting?: boolean;
|
|
||||||
errorMessage?: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: 'close'): void;
|
|
||||||
(event: 'create', payload: { path: string; label: string }): void;
|
|
||||||
(event: 'rename', payload: { workspace_id: string; label: string }): void;
|
|
||||||
(event: 'delete', item: WorkspaceItem): void;
|
|
||||||
(event: 'setDefault', payload: { workspace_id: string }): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const workspaceKind = props.workspaceKind || 'workspace';
|
|
||||||
const createPath = ref('');
|
|
||||||
const createLabel = ref('');
|
|
||||||
const renameDrafts = reactive<Record<string, string>>({});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.workspaces,
|
|
||||||
(items) => {
|
|
||||||
for (const item of items || []) {
|
|
||||||
renameDrafts[item.workspace_id] = item.label || item.workspace_id;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOverlayClose = () => {
|
|
||||||
if (props.busy) return;
|
|
||||||
emits('close');
|
|
||||||
};
|
|
||||||
|
|
||||||
const setRenameDraft = (workspaceId: string, value: string) => {
|
|
||||||
renameDrafts[workspaceId] = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasRenameChange = (item: WorkspaceItem) => {
|
|
||||||
const draft = String(renameDrafts[item.workspace_id] ?? '').trim();
|
|
||||||
return !!draft && draft !== String(item.label || '').trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
const canDelete = (item: WorkspaceItem) => {
|
|
||||||
if (Number(item.running_task_count || 0) > 0) return false;
|
|
||||||
if (workspaceKind === 'project' && item.workspace_id === 'default') return false;
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitCreate = () => {
|
|
||||||
emits('create', {
|
|
||||||
path: createPath.value.trim(),
|
|
||||||
label: createLabel.value.trim()
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitRename = (item: WorkspaceItem) => {
|
|
||||||
emits('rename', {
|
|
||||||
workspace_id: item.workspace_id,
|
|
||||||
label: String(renameDrafts[item.workspace_id] ?? '').trim()
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitSetDefault = (item: WorkspaceItem) => {
|
|
||||||
if (item.workspace_id === props.defaultWorkspaceId) return;
|
|
||||||
emits('setDefault', { workspace_id: item.workspace_id });
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.host-workspace-dialog-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1300;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--theme-overlay-scrim);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog {
|
|
||||||
width: min(680px, 96vw);
|
|
||||||
height: min(760px, 92vh);
|
|
||||||
border-radius: 18px;
|
|
||||||
border: 1px solid var(--theme-control-border-strong);
|
|
||||||
background: var(--theme-surface-card);
|
|
||||||
box-shadow: var(--theme-shadow-soft);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__header,
|
|
||||||
.host-workspace-dialog__actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px 18px;
|
|
||||||
border-bottom: 1px solid var(--theme-control-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--claude-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__close,
|
|
||||||
.host-workspace-dialog__btn {
|
|
||||||
border: 1px solid var(--theme-control-border-strong);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 7px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__close:disabled,
|
|
||||||
.host-workspace-dialog__btn:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__body {
|
|
||||||
padding: 16px 18px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
||||||
gap: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-section {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-section__title {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__field {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__label,
|
|
||||||
.workspace-manage-path {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__input {
|
|
||||||
border: 1px solid var(--theme-control-border-strong);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--theme-surface-soft);
|
|
||||||
color: var(--claude-text);
|
|
||||||
padding: 9px 11px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.4;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__input:focus {
|
|
||||||
border-color: var(--claude-accent);
|
|
||||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--claude-accent) 20%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__btn.primary {
|
|
||||||
justify-self: flex-start;
|
|
||||||
background: var(--claude-accent);
|
|
||||||
border-color: var(--claude-accent-strong);
|
|
||||||
color: var(--on-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__btn.danger {
|
|
||||||
color: var(--state-danger);
|
|
||||||
border-color: color-mix(in srgb, var(--state-danger) 45%, var(--theme-control-border-strong));
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-list {
|
|
||||||
display: grid;
|
|
||||||
align-content: start;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 0;
|
|
||||||
max-height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-list::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid var(--theme-control-border);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--theme-surface-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-row.current {
|
|
||||||
border-color: color-mix(in srgb, var(--claude-accent) 45%, var(--theme-control-border));
|
|
||||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--claude-accent) 18%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-main {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-name {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-actions,
|
|
||||||
.workspace-manage-badges {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-badge {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
border: 1px solid var(--theme-control-border);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 2px 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-badge.running {
|
|
||||||
color: var(--claude-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-badge.default {
|
|
||||||
color: var(--state-success);
|
|
||||||
border-color: color-mix(in srgb, var(--state-success) 45%, var(--theme-control-border-strong));
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-manage-empty {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-workspace-dialog__error {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--state-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.workspace-manage-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,470 +0,0 @@
|
|||||||
<template>
|
|
||||||
<aside class="sidebar left-sidebar" :style="panelStyle">
|
|
||||||
<div class="sidebar-status">
|
|
||||||
<div class="compact-status-card">
|
|
||||||
<div class="status-line">
|
|
||||||
<div class="status-brand">
|
|
||||||
<span
|
|
||||||
class="status-logo-image status-logo-inline"
|
|
||||||
v-html="statusLogoSvg"
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
<div class="brand-text">
|
|
||||||
<span class="brand-name">Astrion</span>
|
|
||||||
<span class="agent-version" v-if="agentVersion">{{ agentVersion }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="status-indicators">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="mode-indicator"
|
|
||||||
data-tutorial="run-mode-indicator"
|
|
||||||
:class="modeIndicatorClass"
|
|
||||||
:title="modeIndicatorTitle"
|
|
||||||
@click="$emit('toggle-thinking-mode')"
|
|
||||||
>
|
|
||||||
<transition name="mode-icon" mode="out-in">
|
|
||||||
<span
|
|
||||||
class="icon icon-sm"
|
|
||||||
:style="iconStyle(modeIndicatorIcon)"
|
|
||||||
:key="modeIndicatorIcon"
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
</transition>
|
|
||||||
</button>
|
|
||||||
<span
|
|
||||||
class="connection-dot"
|
|
||||||
data-tutorial="connection-indicator"
|
|
||||||
:class="{ active: isConnected }"
|
|
||||||
:title="isConnected ? '已连接' : '未连接'"
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="sidebar-panel-card-wrapper">
|
|
||||||
<div class="sidebar-panel-card">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<div class="panel-menu-wrapper" ref="panelMenuWrapper">
|
|
||||||
<button
|
|
||||||
class="sidebar-view-toggle"
|
|
||||||
data-tutorial="panel-menu-toggle"
|
|
||||||
@click.stop="$emit('toggle-panel-menu')"
|
|
||||||
title="切换侧边栏"
|
|
||||||
>
|
|
||||||
<span class="icon icon-md" :style="iconStyle('menu')" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
<transition name="fade">
|
|
||||||
<div class="panel-menu" data-tutorial="panel-menu" v-if="panelMenuOpen">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:class="{ active: panelMode === 'files' }"
|
|
||||||
@click.stop="$emit('select-panel', 'files')"
|
|
||||||
:title="workspaceKind === 'project' ? '项目' : '项目文件'"
|
|
||||||
>
|
|
||||||
<span class="icon icon-md" :style="iconStyle('folder')" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:class="{ active: panelMode === 'todo' }"
|
|
||||||
@click.stop="$emit('select-panel', 'todo')"
|
|
||||||
title="待办列表"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="icon icon-md"
|
|
||||||
:style="iconStyle('stickyNote')"
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:class="{ active: panelMode === 'subAgents' }"
|
|
||||||
@click.stop="$emit('select-panel', 'subAgents')"
|
|
||||||
title="子智能体"
|
|
||||||
>
|
|
||||||
<span class="icon icon-md" :style="iconStyle('bot')" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:class="{ active: panelMode === 'backgroundCommands' }"
|
|
||||||
@click.stop="$emit('select-panel', 'backgroundCommands')"
|
|
||||||
title="后台指令"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="icon icon-md"
|
|
||||||
:style="iconStyle('terminal')"
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="panelMode === 'backgroundCommands' && runningBackgroundCommandCount > 0"
|
|
||||||
type="button"
|
|
||||||
class="background-stop-btn"
|
|
||||||
title="终止所有后台指令"
|
|
||||||
@click.stop="handleStopAllCommands"
|
|
||||||
>
|
|
||||||
<span class="icon icon-sm" :style="iconStyle('stop')" aria-hidden="true"></span>
|
|
||||||
<span class="background-stop-label">停止</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="sidebar-manage-btn"
|
|
||||||
@click="$emit('open-file-manager')"
|
|
||||||
:title="fileManagerDisabled ? '已被管理员禁用' : '打开桌面式文件管理器'"
|
|
||||||
>
|
|
||||||
管理
|
|
||||||
</button>
|
|
||||||
<h3>
|
|
||||||
<span v-if="panelMode === 'files'" class="icon-label">
|
|
||||||
<span class="icon icon-sm" :style="iconStyle('folder')" aria-hidden="true"></span>
|
|
||||||
<span>{{ workspaceKind === 'project' ? '项目' : '项目文件' }}</span>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="panelMode === 'todo'" class="icon-label">
|
|
||||||
<span class="icon icon-sm" :style="iconStyle('stickyNote')" aria-hidden="true"></span>
|
|
||||||
<span>待办列表</span>
|
|
||||||
</span>
|
|
||||||
<span v-else class="icon-label">
|
|
||||||
<template v-if="panelMode === 'subAgents'">
|
|
||||||
<span class="icon icon-sm" :style="iconStyle('bot')" aria-hidden="true"></span>
|
|
||||||
<span>子智能体</span>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<span class="icon icon-sm" :style="iconStyle('terminal')" aria-hidden="true"></span>
|
|
||||||
<span>后台指令</span>
|
|
||||||
</template>
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="sidebar-panel-content">
|
|
||||||
<div v-if="panelMode === 'todo'" class="todo-panel">
|
|
||||||
<div v-if="!todoList" class="todo-empty">暂无待办列表</div>
|
|
||||||
<div v-else>
|
|
||||||
<div
|
|
||||||
class="todo-task"
|
|
||||||
v-for="task in todoList.tasks || []"
|
|
||||||
:key="task.index"
|
|
||||||
:class="{ done: task.status === 'done' }"
|
|
||||||
>
|
|
||||||
<span class="todo-task-title">task{{ task.index }}:{{ task.title }}</span>
|
|
||||||
<span class="todo-task-status icon-label">
|
|
||||||
<span
|
|
||||||
class="icon icon-sm"
|
|
||||||
:style="iconStyle(task.status === 'done' ? 'check' : 'checkbox')"
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
<span>{{ task.status === 'done' ? '完成' : '未完成' }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="todo-instruction">{{ todoList.instruction }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="panelMode === 'subAgents'" class="sub-agent-panel">
|
|
||||||
<div v-if="!subAgents.length" class="sub-agent-empty">暂无运行中的子智能体</div>
|
|
||||||
<div v-else class="sub-agent-cards">
|
|
||||||
<div
|
|
||||||
class="sub-agent-card"
|
|
||||||
v-for="agent in subAgents"
|
|
||||||
:key="agent.task_id"
|
|
||||||
@click="openSubAgent(agent)"
|
|
||||||
>
|
|
||||||
<div class="sub-agent-header">
|
|
||||||
<span class="sub-agent-summary-text">{{ agent.summary || agent.display_name || `#${agent.agent_id}` }}</span>
|
|
||||||
<span class="sub-agent-status" :class="agent.status">{{ agent.status }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="sub-agent-meta">
|
|
||||||
<span class="sub-agent-name" v-if="agent.display_name">{{ agent.display_name }}</span>
|
|
||||||
<span class="sub-agent-tokens" v-if="agent.current_context_tokens">{{ formatTokens(agent.current_context_tokens) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="sub-agent-tool" v-if="agent.last_tool">当前:{{ agent.last_tool }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="panelMode === 'backgroundCommands'" class="sub-agent-panel">
|
|
||||||
<div v-if="!backgroundCommands.length" class="sub-agent-empty">暂无后台指令</div>
|
|
||||||
<div v-else class="sub-agent-cards">
|
|
||||||
<div
|
|
||||||
class="sub-agent-card"
|
|
||||||
v-for="command in backgroundCommands"
|
|
||||||
:key="command.command_id"
|
|
||||||
@click="openBackgroundCommand(command)"
|
|
||||||
>
|
|
||||||
<div class="sub-agent-header">
|
|
||||||
<span class="sub-agent-id">状态</span>
|
|
||||||
<span class="sub-agent-status" :class="command.status">{{
|
|
||||||
formatBackgroundCommandStatus(command.status)
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="sub-agent-summary background-command-summary">
|
|
||||||
{{ formatBackgroundCommandPreview(command.command) }}
|
|
||||||
</div>
|
|
||||||
<div class="sub-agent-tool">
|
|
||||||
{{ formatBackgroundCommandMeta(command) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="file-tree" @contextmenu.prevent>
|
|
||||||
<template v-if="fileTreeUnavailable">
|
|
||||||
<div v-if="hostWorkspaceEnabled" class="host-workspace-section">
|
|
||||||
<div class="host-workspace-header-box">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="host-workspace-add-btn"
|
|
||||||
:disabled="hostWorkspaceSwitching"
|
|
||||||
@click="handleManageHostWorkspace"
|
|
||||||
:title="workspaceKind === 'project' ? '管理项目' : '管理工作区'"
|
|
||||||
>
|
|
||||||
<span class="icon icon-sm" :style="iconStyle('settings')" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
<div class="host-workspace-title">
|
|
||||||
{{ workspaceKind === 'project' ? '管理项目' : '管理工作区' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="host-workspace-list" v-if="hostWorkspaceOptions.length">
|
|
||||||
<button
|
|
||||||
v-for="item in hostWorkspaceOptions"
|
|
||||||
:key="item.workspace_id"
|
|
||||||
type="button"
|
|
||||||
class="host-workspace-card"
|
|
||||||
:class="{ active: item.workspace_id === hostWorkspaceId }"
|
|
||||||
:disabled="hostWorkspaceSwitching || item.workspace_id === hostWorkspaceId"
|
|
||||||
@click="handleHostWorkspaceClick(item.workspace_id)"
|
|
||||||
>
|
|
||||||
<div class="host-workspace-card-head">
|
|
||||||
<span class="host-workspace-label">{{ item.label }}</span>
|
|
||||||
<span class="host-workspace-badges">
|
|
||||||
<span
|
|
||||||
v-if="item.workspace_id === hostWorkspaceDefaultId"
|
|
||||||
class="host-workspace-badge default"
|
|
||||||
>
|
|
||||||
默认
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="Number(item.running_task_count || 0) > 0"
|
|
||||||
class="host-workspace-badge running"
|
|
||||||
>
|
|
||||||
运行中
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="workspaceKind !== 'project'" class="host-workspace-path">
|
|
||||||
{{ item.path || (workspaceKind === 'project' ? '项目文件' : '(未配置路径)') }}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-else class="file-tree-empty">
|
|
||||||
{{ workspaceKind === 'project' ? '暂无项目,点击齿轮管理' : '暂无工作区,点击齿轮管理' }}
|
|
||||||
</div>
|
|
||||||
<div class="host-workspace-hint" v-if="hostWorkspaceSwitching">
|
|
||||||
正在切换,请稍候…
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="file-tree-empty">{{ fileTreeMessage }}</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div v-if="!fileTree.length" class="file-tree-empty">暂无文件</div>
|
|
||||||
<FileNode
|
|
||||||
v-for="node in fileTree"
|
|
||||||
:key="node.path"
|
|
||||||
:node="node"
|
|
||||||
:level="0"
|
|
||||||
:expanded-folders="expandedFolders"
|
|
||||||
:icon-style="iconStyle"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import FileNode from '@/components/files/FileNode.vue';
|
|
||||||
import { useFileStore } from '@/stores/file';
|
|
||||||
import { useSubAgentStore } from '@/stores/subAgent';
|
|
||||||
import { useBackgroundCommandStore } from '@/stores/backgroundCommand';
|
|
||||||
import statusLogoSvgRaw from '../../../logo/logo.svg?raw';
|
|
||||||
|
|
||||||
defineOptions({ name: 'LeftPanel' });
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
width: number;
|
|
||||||
collapsed?: boolean;
|
|
||||||
iconStyle: (key: string) => Record<string, string>;
|
|
||||||
agentVersion: string | null;
|
|
||||||
thinkingMode: boolean;
|
|
||||||
isConnected: boolean;
|
|
||||||
panelMenuOpen: boolean;
|
|
||||||
panelMode: 'files' | 'todo' | 'subAgents' | 'backgroundCommands';
|
|
||||||
runMode: 'fast' | 'thinking';
|
|
||||||
fileManagerDisabled?: boolean;
|
|
||||||
hostWorkspaceEnabled?: boolean;
|
|
||||||
hostWorkspaces?: Array<{
|
|
||||||
workspace_id: string;
|
|
||||||
label: string;
|
|
||||||
path?: string;
|
|
||||||
is_current?: boolean;
|
|
||||||
running_task_count?: number;
|
|
||||||
}>;
|
|
||||||
hostWorkspaceId?: string;
|
|
||||||
hostWorkspaceDefaultId?: string;
|
|
||||||
hostWorkspaceSwitching?: boolean;
|
|
||||||
workspaceKind?: 'workspace' | 'project';
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: 'toggle-panel-menu'): void;
|
|
||||||
(event: 'select-panel', mode: 'files' | 'todo' | 'subAgents' | 'backgroundCommands'): void;
|
|
||||||
(event: 'open-file-manager'): void;
|
|
||||||
(event: 'toggle-thinking-mode'): void;
|
|
||||||
(event: 'switch-host-workspace', workspaceId: string): void;
|
|
||||||
(event: 'create-host-workspace'): void;
|
|
||||||
(event: 'manage-host-workspace'): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const panelMenuWrapper = ref<HTMLElement | null>(null);
|
|
||||||
const statusLogoSvg = statusLogoSvgRaw.replace(/fill="#000000"/g, 'fill="currentColor"');
|
|
||||||
const workspaceKind = computed(() => props.workspaceKind || 'workspace');
|
|
||||||
const panelStyle = computed(() => {
|
|
||||||
const px = `${props.width}px`;
|
|
||||||
const layoutWidth = props.collapsed ? '0px' : px;
|
|
||||||
return {
|
|
||||||
'--workspace-content-width': px,
|
|
||||||
width: layoutWidth,
|
|
||||||
minWidth: layoutWidth
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const resolveRunMode = () => {
|
|
||||||
// 历史值 deep 映射为 thinking
|
|
||||||
const rawMode = props.runMode as string;
|
|
||||||
if (rawMode === 'deep' || rawMode === 'thinking') {
|
|
||||||
return 'thinking';
|
|
||||||
}
|
|
||||||
if (rawMode === 'fast') {
|
|
||||||
return 'fast';
|
|
||||||
}
|
|
||||||
return props.thinkingMode ? 'thinking' : 'fast';
|
|
||||||
};
|
|
||||||
|
|
||||||
const modeIndicatorClass = computed(() => {
|
|
||||||
const mode = resolveRunMode();
|
|
||||||
if (mode === 'thinking') {
|
|
||||||
return 'thinking';
|
|
||||||
}
|
|
||||||
return 'fast';
|
|
||||||
});
|
|
||||||
|
|
||||||
const modeIndicatorIcon = computed(() => {
|
|
||||||
const mode = resolveRunMode();
|
|
||||||
if (mode === 'thinking') {
|
|
||||||
return 'brain';
|
|
||||||
}
|
|
||||||
return 'zap';
|
|
||||||
});
|
|
||||||
|
|
||||||
const modeIndicatorTitle = computed(() => {
|
|
||||||
const mode = resolveRunMode();
|
|
||||||
if (mode === 'thinking') {
|
|
||||||
return '思考模式(点击切换)';
|
|
||||||
}
|
|
||||||
return '快速模式(点击切换)';
|
|
||||||
});
|
|
||||||
const fileStore = useFileStore();
|
|
||||||
const subAgentStore = useSubAgentStore();
|
|
||||||
const backgroundCommandStore = useBackgroundCommandStore();
|
|
||||||
const { fileTree, expandedFolders, todoList, fileTreeUnavailable, fileTreeMessage } =
|
|
||||||
storeToRefs(fileStore);
|
|
||||||
const { subAgents } = storeToRefs(subAgentStore);
|
|
||||||
const { commands: backgroundCommands } = storeToRefs(backgroundCommandStore);
|
|
||||||
|
|
||||||
const hostWorkspaceOptions = computed(() => {
|
|
||||||
const list = Array.isArray(props.hostWorkspaces) ? props.hostWorkspaces : [];
|
|
||||||
return list.filter((item) => item && item.workspace_id);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleHostWorkspaceClick = (workspaceId: string) => {
|
|
||||||
const targetId = String(workspaceId || '').trim();
|
|
||||||
if (!targetId || targetId === props.hostWorkspaceId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (props.hostWorkspaceSwitching) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emits('switch-host-workspace', targetId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateHostWorkspace = () => {
|
|
||||||
if (props.hostWorkspaceSwitching) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emits('create-host-workspace');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleManageHostWorkspace = () => {
|
|
||||||
if (props.hostWorkspaceSwitching) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emits('manage-host-workspace');
|
|
||||||
};
|
|
||||||
|
|
||||||
const openSubAgent = (agent: any) => {
|
|
||||||
subAgentStore.openSubAgent(agent);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openBackgroundCommand = (command: any) => {
|
|
||||||
backgroundCommandStore.openCommand(command);
|
|
||||||
};
|
|
||||||
|
|
||||||
const BG_COMMAND_TERMINAL = new Set(['completed', 'failed', 'timeout', 'cancelled']);
|
|
||||||
const runningBackgroundCommandCount = computed(() => {
|
|
||||||
return (backgroundCommands.value || []).filter((c: any) => {
|
|
||||||
const s = String(c?.status || '').toLowerCase();
|
|
||||||
return !BG_COMMAND_TERMINAL.has(s);
|
|
||||||
}).length;
|
|
||||||
});
|
|
||||||
const handleStopAllCommands = async () => {
|
|
||||||
const result = await backgroundCommandStore.stopAllCommands();
|
|
||||||
if (!result.success) {
|
|
||||||
console.error('[LeftPanel] 批量停止后台指令失败:', result.error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatBackgroundCommandMeta = (command: any) => {
|
|
||||||
const code = command?.return_code;
|
|
||||||
if (code === null || code === undefined) {
|
|
||||||
return '状态码:--';
|
|
||||||
}
|
|
||||||
return `状态码:${code}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatBackgroundCommandStatus = (status: any) => {
|
|
||||||
const normalized = (status || 'running').toString().toLowerCase();
|
|
||||||
return normalized;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatBackgroundCommandPreview = (rawCommand: any) => {
|
|
||||||
const content = (rawCommand || '(空命令)').toString().replace(/\s+/g, ' ').trim();
|
|
||||||
if (!content) {
|
|
||||||
return '(空命令)';
|
|
||||||
}
|
|
||||||
return content;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTokens = (tokens: number) => {
|
|
||||||
if (!tokens || tokens <= 0) return '';
|
|
||||||
if (tokens >= 1000) {
|
|
||||||
return `${(tokens / 1000).toFixed(1)}k ctx`;
|
|
||||||
}
|
|
||||||
return `${tokens} ctx`;
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
panelMenuWrapper
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@ -618,47 +618,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label class="settings-toggle-row"
|
|
||||||
><span class="settings-row-copy"
|
|
||||||
><span class="settings-row-title">默认隐藏工作区</span
|
|
||||||
><span class="settings-row-desc"
|
|
||||||
>打开页面时默认收起左侧工作区,仍可随时手动展开</span
|
|
||||||
></span
|
|
||||||
><input
|
|
||||||
type="checkbox"
|
|
||||||
:checked="form.default_hide_workspace"
|
|
||||||
@change="applyDefaultHideWorkspaceOption($event.target.checked)" /><span
|
|
||||||
class="fancy-check"
|
|
||||||
aria-hidden="true"
|
|
||||||
><svg viewBox="0 0 64 64">
|
|
||||||
<path
|
|
||||||
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
|
||||||
pathLength="575.0541381835938"
|
|
||||||
class="fancy-path"
|
|
||||||
></path></svg></span
|
|
||||||
></label>
|
|
||||||
<label class="settings-toggle-row"
|
|
||||||
><span class="settings-row-copy"
|
|
||||||
><span class="settings-row-title">隐藏快捷窗口</span
|
|
||||||
><span class="settings-row-desc"
|
|
||||||
>关闭对话区右侧的待办 / 子智能体 / 后台指令 / 文件窗口列</span
|
|
||||||
></span
|
|
||||||
><input
|
|
||||||
type="checkbox"
|
|
||||||
:checked="form.hide_quick_dock"
|
|
||||||
@change="
|
|
||||||
personalization.updateField({
|
|
||||||
key: 'hide_quick_dock',
|
|
||||||
value: $event.target.checked
|
|
||||||
})
|
|
||||||
" /><span class="fancy-check" aria-hidden="true"
|
|
||||||
><svg viewBox="0 0 64 64">
|
|
||||||
<path
|
|
||||||
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
|
||||||
pathLength="575.0541381835938"
|
|
||||||
class="fancy-path"
|
|
||||||
></path></svg></span
|
|
||||||
></label>
|
|
||||||
<label class="settings-toggle-row"
|
<label class="settings-toggle-row"
|
||||||
><span class="settings-row-copy"
|
><span class="settings-row-copy"
|
||||||
><span class="settings-row-title">按项目分组对话</span
|
><span class="settings-row-title">按项目分组对话</span
|
||||||
@ -2113,7 +2072,7 @@ import { ref, computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue'
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { usePersonalizationStore } from '@/stores/personalization';
|
import { usePersonalizationStore } from '@/stores/personalization';
|
||||||
import { useResourceStore } from '@/stores/resource';
|
import { useResourceStore } from '@/stores/resource';
|
||||||
import { useUiStore, persistDefaultHideWorkspace } from '@/stores/ui';
|
import { useUiStore } from '@/stores/ui';
|
||||||
import { usePolicyStore } from '@/stores/policy';
|
import { usePolicyStore } from '@/stores/policy';
|
||||||
import { useTutorialStore } from '@/stores/tutorial';
|
import { useTutorialStore } from '@/stores/tutorial';
|
||||||
import { useModelStore } from '@/stores/model';
|
import { useModelStore } from '@/stores/model';
|
||||||
@ -3341,28 +3300,6 @@ const applyThemeOption = async (theme: ThemeKey) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyDefaultHideWorkspaceOption = async (hidden: boolean) => {
|
|
||||||
const next = !!hidden;
|
|
||||||
persistDefaultHideWorkspace(next);
|
|
||||||
personalization.updateField({ key: 'default_hide_workspace', value: next });
|
|
||||||
if (next) {
|
|
||||||
uiStore.setWorkspaceCollapsed(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch('/api/personalization', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ default_hide_workspace: next })
|
|
||||||
});
|
|
||||||
const result = await resp.json();
|
|
||||||
if (!resp.ok || !result.success) {
|
|
||||||
console.warn('保存默认隐藏工作区失败:', result.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('保存默认隐藏工作区失败:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<aside class="conversation-sidebar" :class="{ collapsed, 'monitor-mode': monitorModeActive }">
|
<aside class="conversation-sidebar" :class="{ collapsed }">
|
||||||
<div class="conversation-sidebar-inner">
|
<div class="conversation-sidebar-inner">
|
||||||
<div class="conversation-primary-actions">
|
<div class="conversation-primary-actions">
|
||||||
<button
|
<button
|
||||||
@ -59,30 +59,41 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
v-if="hostWorkspaceEnabled"
|
||||||
type="button"
|
type="button"
|
||||||
class="sidebar-nav-row workspace-control-btn"
|
class="sidebar-nav-row workspace-entry-btn"
|
||||||
:class="{ active: workspaceCollapsed }"
|
:class="{ active: workspaceSwitcherOpen }"
|
||||||
:title="workspaceCollapsed ? '显示工作区' : '隐藏工作区'"
|
:title="workspaceKind === 'project' ? '项目' : '工作区'"
|
||||||
@click="$emit('toggle-workspace')"
|
data-tutorial="workspace-toggle"
|
||||||
|
ref="workspaceEntryBtn"
|
||||||
|
@click="toggleWorkspaceSwitcher"
|
||||||
>
|
>
|
||||||
<span class="sidebar-nav-icon" data-tutorial="workspace-toggle" aria-hidden="true">
|
<span class="sidebar-nav-icon layers-icon" aria-hidden="true">
|
||||||
<span class="icon icon-md" :style="iconStyle('layers')"></span>
|
<!-- 与其他导航按钮统一:内联 layers 图标,CSS 限到 18px 与铅笔一致,避免 mask 图标异步加载闪烁 -->
|
||||||
</span>
|
<svg
|
||||||
<span class="sidebar-nav-label">工作区</span>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</button>
|
width="24"
|
||||||
|
height="24"
|
||||||
<button
|
viewBox="0 0 24 24"
|
||||||
type="button"
|
fill="none"
|
||||||
class="sidebar-nav-row monitor-mode-btn"
|
stroke="currentColor"
|
||||||
:class="{ active: displayMode === 'monitor', blocked: displayModeDisabled }"
|
stroke-width="1.6"
|
||||||
:aria-disabled="displayModeDisabled"
|
stroke-linecap="round"
|
||||||
:title="displayMode === 'monitor' ? '退出显示器' : '虚拟显示器'"
|
stroke-linejoin="round"
|
||||||
@click="$emit('toggle-display-mode')"
|
|
||||||
>
|
>
|
||||||
<span class="sidebar-nav-icon" data-tutorial="monitor-toggle" aria-hidden="true">
|
<path
|
||||||
<span class="icon icon-md" :style="iconStyle('monitor')"></span>
|
d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="sidebar-nav-label">虚拟显示器</span>
|
<span class="sidebar-nav-label">{{ workspaceKind === 'project' ? '项目' : '工作区' }}</span>
|
||||||
|
<span class="workspace-entry-current">{{ currentWorkspaceLabel }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -493,6 +504,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<WorkspaceSwitcher
|
||||||
|
:open="workspaceSwitcherOpen"
|
||||||
|
:anchor-rect="workspaceAnchorRect"
|
||||||
|
:workspaces="workspaces"
|
||||||
|
:current-workspace-id="currentWorkspaceId"
|
||||||
|
:default-workspace-id="defaultWorkspaceId"
|
||||||
|
:workspace-kind="workspaceKind"
|
||||||
|
:busy="workspaceBusy"
|
||||||
|
:create-submitting="workspaceCreateSubmitting"
|
||||||
|
:error-message="workspaceError"
|
||||||
|
@close="workspaceSwitcherOpen = false"
|
||||||
|
@switch="$emit('switch-workspace', $event)"
|
||||||
|
@create="$emit('create-workspace', $event)"
|
||||||
|
@rename="$emit('rename-workspace', $event)"
|
||||||
|
@delete="$emit('delete-workspace', $event)"
|
||||||
|
@set-default="$emit('set-default-workspace', $event)"
|
||||||
|
@reveal="$emit('reveal-workspace', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="conversation-personal-entry" :class="{ active: personalPageVisible }">
|
<div class="conversation-personal-entry" :class="{ active: personalPageVisible }">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -525,6 +555,7 @@ import { storeToRefs } from 'pinia';
|
|||||||
import { useUiStore } from '@/stores/ui';
|
import { useUiStore } from '@/stores/ui';
|
||||||
import { useConversationStore } from '@/stores/conversation';
|
import { useConversationStore } from '@/stores/conversation';
|
||||||
import { usePersonalizationStore } from '@/stores/personalization';
|
import { usePersonalizationStore } from '@/stores/personalization';
|
||||||
|
import WorkspaceSwitcher from './WorkspaceSwitcher.vue';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@ -532,12 +563,14 @@ const props = withDefaults(
|
|||||||
iconStyle?: (key: string) => Record<string, string>;
|
iconStyle?: (key: string) => Record<string, string>;
|
||||||
showCollapseButton?: boolean;
|
showCollapseButton?: boolean;
|
||||||
collapseButtonVariant?: 'toggle' | 'close';
|
collapseButtonVariant?: 'toggle' | 'close';
|
||||||
displayMode?: 'chat' | 'monitor';
|
|
||||||
displayModeDisabled?: boolean;
|
|
||||||
runningTasks?: any[];
|
runningTasks?: any[];
|
||||||
currentWorkspaceId?: string;
|
currentWorkspaceId?: string;
|
||||||
|
defaultWorkspaceId?: string;
|
||||||
workspaces?: any[];
|
workspaces?: any[];
|
||||||
workspaceKind?: 'workspace' | 'project';
|
workspaceKind?: 'workspace' | 'project';
|
||||||
|
workspaceBusy?: boolean;
|
||||||
|
workspaceCreateSubmitting?: boolean;
|
||||||
|
workspaceError?: string;
|
||||||
hostWorkspaceEnabled?: boolean;
|
hostWorkspaceEnabled?: boolean;
|
||||||
groupByWorkspace?: boolean;
|
groupByWorkspace?: boolean;
|
||||||
versioningHostMode?: boolean;
|
versioningHostMode?: boolean;
|
||||||
@ -548,12 +581,14 @@ const props = withDefaults(
|
|||||||
{
|
{
|
||||||
showCollapseButton: true,
|
showCollapseButton: true,
|
||||||
collapseButtonVariant: 'toggle',
|
collapseButtonVariant: 'toggle',
|
||||||
displayMode: 'chat',
|
|
||||||
displayModeDisabled: false,
|
|
||||||
runningTasks: () => [],
|
runningTasks: () => [],
|
||||||
currentWorkspaceId: '',
|
currentWorkspaceId: '',
|
||||||
|
defaultWorkspaceId: '',
|
||||||
workspaces: () => [],
|
workspaces: () => [],
|
||||||
workspaceKind: 'workspace',
|
workspaceKind: 'workspace',
|
||||||
|
workspaceBusy: false,
|
||||||
|
workspaceCreateSubmitting: false,
|
||||||
|
workspaceError: '',
|
||||||
hostWorkspaceEnabled: false,
|
hostWorkspaceEnabled: false,
|
||||||
groupByWorkspace: false,
|
groupByWorkspace: false,
|
||||||
versioningHostMode: false,
|
versioningHostMode: false,
|
||||||
@ -574,8 +609,10 @@ const emit = defineEmits<{
|
|||||||
(event: 'personal'): void;
|
(event: 'personal'): void;
|
||||||
(event: 'delete', id: string): void;
|
(event: 'delete', id: string): void;
|
||||||
(event: 'duplicate', id: string): void;
|
(event: 'duplicate', id: string): void;
|
||||||
(event: 'toggle-workspace'): void;
|
(event: 'switch-workspace', workspaceId: string): void;
|
||||||
(event: 'toggle-display-mode'): void;
|
(event: 'create-workspace', payload: { path: string; label: string }): void;
|
||||||
|
(event: 'delete-workspace', item: any): void;
|
||||||
|
(event: 'set-default-workspace', workspaceId: string): void;
|
||||||
(event: 'select-workspace-conversation', payload: { conversationId: string; workspaceId: string }): void;
|
(event: 'select-workspace-conversation', payload: { conversationId: string; workspaceId: string }): void;
|
||||||
(event: 'create-workspace-conversation', workspaceId: string): void;
|
(event: 'create-workspace-conversation', workspaceId: string): void;
|
||||||
(event: 'reveal-workspace', workspaceId: string): void;
|
(event: 'reveal-workspace', workspaceId: string): void;
|
||||||
@ -587,11 +624,7 @@ const uiStore = useUiStore();
|
|||||||
const conversationStore = useConversationStore();
|
const conversationStore = useConversationStore();
|
||||||
const personalizationStore = usePersonalizationStore();
|
const personalizationStore = usePersonalizationStore();
|
||||||
|
|
||||||
const {
|
const { sidebarCollapsed: collapsed } = storeToRefs(uiStore);
|
||||||
sidebarCollapsed: collapsed,
|
|
||||||
workspaceCollapsed,
|
|
||||||
chatDisplayMode: uiChatDisplayMode
|
|
||||||
} = storeToRefs(uiStore);
|
|
||||||
const {
|
const {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
searchResults,
|
searchResults,
|
||||||
@ -610,11 +643,40 @@ const {
|
|||||||
const { visible: personalPageVisible } = storeToRefs(personalizationStore);
|
const { visible: personalPageVisible } = storeToRefs(personalizationStore);
|
||||||
|
|
||||||
const iconStyle = (key: string) => (props.iconStyle ? props.iconStyle(key) : {});
|
const iconStyle = (key: string) => (props.iconStyle ? props.iconStyle(key) : {});
|
||||||
const displayMode = computed(() => props.displayMode);
|
|
||||||
const monitorModeActive = computed(
|
/* ---------- 工作区切换浮层 ---------- */
|
||||||
() => displayMode.value === 'monitor' || uiChatDisplayMode.value === 'monitor'
|
const workspaceEntryBtn = ref<HTMLElement | null>(null);
|
||||||
);
|
const workspaceSwitcherOpen = ref(false);
|
||||||
const displayModeDisabled = computed(() => props.displayModeDisabled);
|
const workspaceAnchorRect = ref<{ left: number; top: number; right: number; bottom: number } | null>(null);
|
||||||
|
|
||||||
|
const currentWorkspaceLabel = computed(() => {
|
||||||
|
const list = Array.isArray(props.workspaces) ? props.workspaces : [];
|
||||||
|
const current = list.find((item: any) => String(item?.workspace_id || '') === props.currentWorkspaceId);
|
||||||
|
return current?.label || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleWorkspaceSwitcher = () => {
|
||||||
|
if (workspaceSwitcherOpen.value) {
|
||||||
|
workspaceSwitcherOpen.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const el = workspaceEntryBtn.value;
|
||||||
|
if (el) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
/* 折叠时 sidebar-inner 保持展开宽、超出部分被 overflow 裁切,
|
||||||
|
按钮布局右缘在可视区外;锚点右缘需夹紧到侧边栏可视右缘,
|
||||||
|
否则浮层会出现在「展开后的位置」而不是紧贴折叠栏 */
|
||||||
|
const sidebarEl = el.closest('.conversation-sidebar');
|
||||||
|
const visibleRight = sidebarEl ? sidebarEl.getBoundingClientRect().right : rect.right;
|
||||||
|
workspaceAnchorRect.value = {
|
||||||
|
left: rect.left,
|
||||||
|
top: rect.top,
|
||||||
|
right: Math.min(rect.right, visibleRight),
|
||||||
|
bottom: rect.bottom
|
||||||
|
};
|
||||||
|
}
|
||||||
|
workspaceSwitcherOpen.value = true;
|
||||||
|
};
|
||||||
const openActionMenuId = ref<string | null>(null);
|
const openActionMenuId = ref<string | null>(null);
|
||||||
const actionMenuPosition = ref<{ top: number; right: number }>({ top: 0, right: 0 });
|
const actionMenuPosition = ref<{ top: number; right: number }>({ top: 0, right: 0 });
|
||||||
const hoverWorkspaceId = ref<string | null>(null);
|
const hoverWorkspaceId = ref<string | null>(null);
|
||||||
|
|||||||
917
static/src/components/sidebar/WorkspaceSwitcher.vue
Normal file
917
static/src/components/sidebar/WorkspaceSwitcher.vue
Normal file
@ -0,0 +1,917 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<transition name="ws-switcher">
|
||||||
|
<div
|
||||||
|
v-if="open"
|
||||||
|
ref="popoverEl"
|
||||||
|
class="ws-switcher"
|
||||||
|
:class="{ 'ws-switcher--project': isProject }"
|
||||||
|
:style="popoverStyle"
|
||||||
|
role="dialog"
|
||||||
|
:aria-label="noun"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div class="ws-switcher__header">
|
||||||
|
<span class="ws-switcher__title">{{ noun }}</span>
|
||||||
|
<span class="ws-switcher__count">{{ workspaces.length }} 个</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ws-switcher__list" ref="listEl" @scroll="closeMenu">
|
||||||
|
<div
|
||||||
|
v-for="item in workspaces"
|
||||||
|
:key="item.workspace_id"
|
||||||
|
class="ws-row"
|
||||||
|
:data-ws-id="item.workspace_id"
|
||||||
|
:class="{
|
||||||
|
current: item.workspace_id === currentWorkspaceId,
|
||||||
|
'menu-open': menuOpenId === item.workspace_id
|
||||||
|
}"
|
||||||
|
role="option"
|
||||||
|
:aria-selected="item.workspace_id === currentWorkspaceId"
|
||||||
|
@click="handleRowClick(item)"
|
||||||
|
>
|
||||||
|
<!-- 删除确认内联 -->
|
||||||
|
<div v-if="deletingId === item.workspace_id" class="ws-confirm">
|
||||||
|
<div class="ws-confirm-text">
|
||||||
|
删除「{{ item.label }}」?{{ isProject ? '项目文件夹和对话记录将一并删除。' : '不会删除磁盘上的文件夹。' }}
|
||||||
|
</div>
|
||||||
|
<div class="ws-confirm-actions">
|
||||||
|
<button type="button" class="ws-btn ghost" :disabled="busy" @click.stop="deletingId = null">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button type="button" class="ws-btn danger" :disabled="busy" @click.stop="confirmDelete(item)">
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<span class="ws-row-icon" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="ws-row-text">
|
||||||
|
<input
|
||||||
|
v-if="renamingId === item.workspace_id"
|
||||||
|
ref="renameInputEl"
|
||||||
|
v-model="renameDraft"
|
||||||
|
class="ws-rename-input"
|
||||||
|
:disabled="busy"
|
||||||
|
@click.stop
|
||||||
|
@keydown.enter.prevent="commitRename(item)"
|
||||||
|
@keydown.esc.prevent="renamingId = null"
|
||||||
|
/>
|
||||||
|
<span v-else class="ws-row-name">{{ item.label }}</span>
|
||||||
|
<span v-if="!isProject" class="ws-row-path">{{ item.path || '(未配置路径)' }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="ws-row-meta">
|
||||||
|
<span v-if="item.workspace_id === defaultWorkspaceId" class="ws-tag">默认</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ws-more"
|
||||||
|
:aria-expanded="menuOpenId === item.workspace_id"
|
||||||
|
aria-label="更多操作"
|
||||||
|
@click.stop="toggleMenu(item)"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<circle cx="5" cy="12" r="1.6" /><circle cx="12" cy="12" r="1.6" /><circle cx="19" cy="12" r="1.6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-if="!workspaces.length" class="ws-empty">
|
||||||
|
{{ isProject ? '暂无项目' : '暂无工作区' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部:新建按钮 ⇄ 新建表单(高度动画容器) -->
|
||||||
|
<div class="ws-bottom" ref="bottomEl">
|
||||||
|
<div v-if="!createOpen" class="ws-footer" :class="{ fading: bottomFading }">
|
||||||
|
<button type="button" class="ws-create-btn" :disabled="busy" @click.stop="openCreateForm">
|
||||||
|
<span class="ws-create-icon" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5 12h14" /><path d="M12 5v14" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span>新建{{ noun }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form v-else class="ws-create-form" :class="{ fading: bottomFading }" @submit.prevent="submitCreate">
|
||||||
|
<input
|
||||||
|
ref="createNameEl"
|
||||||
|
v-model="createLabel"
|
||||||
|
type="text"
|
||||||
|
:placeholder="isProject ? '项目名称' : '工作区名称(可选)'"
|
||||||
|
autocomplete="off"
|
||||||
|
:disabled="createSubmitting"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-if="!isProject"
|
||||||
|
v-model="createPath"
|
||||||
|
type="text"
|
||||||
|
placeholder="路径:绝对路径或相对仓库路径"
|
||||||
|
autocomplete="off"
|
||||||
|
:disabled="createSubmitting"
|
||||||
|
/>
|
||||||
|
<div v-if="errorMessage" class="ws-create-error">{{ errorMessage }}</div>
|
||||||
|
<div class="ws-create-actions">
|
||||||
|
<button type="button" class="ws-btn ghost" :disabled="createSubmitting" @click.stop="closeCreateForm">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="ws-btn primary" :disabled="createSubmitting">
|
||||||
|
{{ createSubmitting ? '创建中...' : '创建' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- 「…」二级菜单:fixed 浮层,逃逸容器裁切(同个人空间二级菜单方案) -->
|
||||||
|
<transition name="ws-menu">
|
||||||
|
<div
|
||||||
|
v-if="menuOpenId"
|
||||||
|
class="ws-menu-floating"
|
||||||
|
:style="menuStyle"
|
||||||
|
role="menu"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:disabled="menuOpenId === defaultWorkspaceId || busy"
|
||||||
|
@click="handleMenuAction('set-default')"
|
||||||
|
>
|
||||||
|
设为默认
|
||||||
|
</button>
|
||||||
|
<button type="button" :disabled="busy" @click="handleMenuAction('rename')">重命名</button>
|
||||||
|
<button type="button" :disabled="isProject || busy" @click="handleMenuAction('reveal')">
|
||||||
|
在文件夹中打开
|
||||||
|
</button>
|
||||||
|
<div class="ws-menu-sep"></div>
|
||||||
|
<button type="button" class="danger" :disabled="busy" @click="handleMenuAction('delete')">删除</button>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'WorkspaceSwitcher' });
|
||||||
|
|
||||||
|
type WorkspaceItem = {
|
||||||
|
workspace_id: string;
|
||||||
|
label: string;
|
||||||
|
path?: string;
|
||||||
|
running_task_count?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AnchorRect = { left: number; top: number; right: number; bottom: number };
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
open: boolean;
|
||||||
|
anchorRect?: AnchorRect | null;
|
||||||
|
workspaces?: WorkspaceItem[];
|
||||||
|
currentWorkspaceId?: string;
|
||||||
|
defaultWorkspaceId?: string;
|
||||||
|
workspaceKind?: 'workspace' | 'project';
|
||||||
|
busy?: boolean;
|
||||||
|
createSubmitting?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
anchorRect: null,
|
||||||
|
workspaces: () => [],
|
||||||
|
currentWorkspaceId: '',
|
||||||
|
defaultWorkspaceId: '',
|
||||||
|
workspaceKind: 'workspace',
|
||||||
|
busy: false,
|
||||||
|
createSubmitting: false,
|
||||||
|
errorMessage: ''
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'close'): void;
|
||||||
|
(event: 'switch', workspaceId: string): void;
|
||||||
|
(event: 'create', payload: { path: string; label: string }): void;
|
||||||
|
(event: 'rename', payload: { workspace_id: string; label: string }): void;
|
||||||
|
(event: 'delete', item: WorkspaceItem): void;
|
||||||
|
(event: 'set-default', workspaceId: string): void;
|
||||||
|
(event: 'reveal', workspaceId: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isProject = computed(() => props.workspaceKind === 'project');
|
||||||
|
const noun = computed(() => (isProject.value ? '项目' : '工作区'));
|
||||||
|
|
||||||
|
const popoverEl = ref<HTMLElement | null>(null);
|
||||||
|
const listEl = ref<HTMLElement | null>(null);
|
||||||
|
const bottomEl = ref<HTMLElement | null>(null);
|
||||||
|
const createNameEl = ref<HTMLInputElement | null>(null);
|
||||||
|
const renameInputEl = ref<HTMLInputElement[] | null>(null);
|
||||||
|
|
||||||
|
const menuOpenId = ref<string | null>(null);
|
||||||
|
const renamingId = ref<string | null>(null);
|
||||||
|
const deletingId = ref<string | null>(null);
|
||||||
|
const renameDraft = ref('');
|
||||||
|
const createOpen = ref(false);
|
||||||
|
const createPath = ref('');
|
||||||
|
const createLabel = ref('');
|
||||||
|
const bottomFading = ref(false);
|
||||||
|
|
||||||
|
/* ---------- 浮层定位:锚定入口按钮,夹紧在视口内 ---------- */
|
||||||
|
const POPOVER_WIDTH = 300;
|
||||||
|
const popoverStyle = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
const updatePosition = () => {
|
||||||
|
const anchor = props.anchorRect;
|
||||||
|
if (!anchor || typeof window === 'undefined') return;
|
||||||
|
const vw = window.innerWidth;
|
||||||
|
const vh = window.innerHeight;
|
||||||
|
const popH = popoverEl.value?.offsetHeight || 360;
|
||||||
|
|
||||||
|
let left = anchor.right + 8;
|
||||||
|
let top = anchor.top - 6;
|
||||||
|
/* 右侧空间不足(如移动端全屏侧边栏)→ 改为在入口下方展开 */
|
||||||
|
if (left + POPOVER_WIDTH > vw - 8) {
|
||||||
|
left = Math.max(8, Math.min(anchor.left, vw - POPOVER_WIDTH - 8));
|
||||||
|
top = anchor.bottom + 6;
|
||||||
|
}
|
||||||
|
if (top + popH > vh - 8) top = Math.max(8, vh - popH - 8);
|
||||||
|
popoverStyle.value = {
|
||||||
|
left: `${Math.round(left)}px`,
|
||||||
|
top: `${Math.round(top)}px`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------- 「…」浮动菜单定位 ---------- */
|
||||||
|
const menuStyle = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
menuOpenId.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMenu = async (item: WorkspaceItem) => {
|
||||||
|
if (props.busy) return;
|
||||||
|
if (menuOpenId.value === item.workspace_id) {
|
||||||
|
closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renamingId.value = null;
|
||||||
|
deletingId.value = null;
|
||||||
|
menuOpenId.value = item.workspace_id;
|
||||||
|
await nextTick();
|
||||||
|
const id = item.workspace_id;
|
||||||
|
const rowEl = listEl.value?.querySelector(`.ws-row[data-ws-id="${CSS.escape(id)}"]`);
|
||||||
|
const btn = rowEl?.querySelector('.ws-more') as HTMLElement | null;
|
||||||
|
if (!btn) {
|
||||||
|
closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = btn.getBoundingClientRect();
|
||||||
|
const menuEl = document.querySelector('.ws-menu-floating') as HTMLElement | null;
|
||||||
|
const mw = menuEl?.offsetWidth || 150;
|
||||||
|
const mh = menuEl?.offsetHeight || 150;
|
||||||
|
let left = rect.right - mw;
|
||||||
|
let top = rect.bottom + 4;
|
||||||
|
left = Math.max(8, Math.min(left, window.innerWidth - mw - 8));
|
||||||
|
if (top + mh > window.innerHeight - 8) top = rect.top - mh - 4;
|
||||||
|
menuStyle.value = { left: `${Math.round(left)}px`, top: `${Math.round(top)}px` };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuAction = (act: 'set-default' | 'rename' | 'reveal' | 'delete') => {
|
||||||
|
const id = menuOpenId.value;
|
||||||
|
const item = props.workspaces.find((w) => w.workspace_id === id);
|
||||||
|
closeMenu();
|
||||||
|
if (!item) return;
|
||||||
|
if (act === 'set-default') {
|
||||||
|
emit('set-default', item.workspace_id);
|
||||||
|
} else if (act === 'rename') {
|
||||||
|
renamingId.value = item.workspace_id;
|
||||||
|
renameDraft.value = item.label;
|
||||||
|
nextTick(() => {
|
||||||
|
const el = renameInputEl.value?.[0];
|
||||||
|
el?.focus();
|
||||||
|
el?.select();
|
||||||
|
});
|
||||||
|
} else if (act === 'reveal') {
|
||||||
|
emit('reveal', item.workspace_id);
|
||||||
|
} else if (act === 'delete') {
|
||||||
|
deletingId.value = item.workspace_id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------- 行行为 ---------- */
|
||||||
|
const handleRowClick = (item: WorkspaceItem) => {
|
||||||
|
if (props.busy) return;
|
||||||
|
if (renamingId.value || deletingId.value) return;
|
||||||
|
if (menuOpenId.value) {
|
||||||
|
closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item.workspace_id !== props.currentWorkspaceId) {
|
||||||
|
emit('switch', item.workspace_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitRename = (item: WorkspaceItem) => {
|
||||||
|
const label = renameDraft.value.trim();
|
||||||
|
if (label && label !== item.label) {
|
||||||
|
emit('rename', { workspace_id: item.workspace_id, label });
|
||||||
|
}
|
||||||
|
renamingId.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = (item: WorkspaceItem) => {
|
||||||
|
deletingId.value = null;
|
||||||
|
emit('delete', item);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------- 新建表单:底部高度动画 + 交叉淡入淡出 ---------- */
|
||||||
|
let bottomAnimating = false;
|
||||||
|
|
||||||
|
const swapBottom = (toForm: boolean, done?: () => void) => {
|
||||||
|
if (bottomAnimating) return;
|
||||||
|
bottomAnimating = true;
|
||||||
|
const bottom = bottomEl.value;
|
||||||
|
if (!bottom) {
|
||||||
|
createOpen.value = toForm;
|
||||||
|
bottomAnimating = false;
|
||||||
|
if (done) done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const h0 = bottom.offsetHeight;
|
||||||
|
bottom.style.height = `${h0}px`;
|
||||||
|
bottomFading.value = true;
|
||||||
|
window.setTimeout(() => {
|
||||||
|
createOpen.value = toForm;
|
||||||
|
nextTick(() => {
|
||||||
|
/* scrollHeight 至少等于 clientHeight,固定高度下会被旧高度污染;
|
||||||
|
切 auto 测真实目标高度(此时新内容仍在淡出态,视觉无跳变) */
|
||||||
|
bottom.style.height = 'auto';
|
||||||
|
const h1 = bottom.offsetHeight;
|
||||||
|
bottom.style.height = `${h0}px`;
|
||||||
|
void bottom.offsetHeight; /* 强制 reflow,确保过渡从 h0 开始 */
|
||||||
|
bottom.style.height = `${h1}px`;
|
||||||
|
bottomFading.value = false;
|
||||||
|
window.setTimeout(() => {
|
||||||
|
bottom.style.height = '';
|
||||||
|
bottomAnimating = false;
|
||||||
|
if (done) done();
|
||||||
|
}, 210);
|
||||||
|
});
|
||||||
|
}, 130);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateForm = () => {
|
||||||
|
if (createOpen.value || props.busy) return;
|
||||||
|
closeMenu();
|
||||||
|
createPath.value = '';
|
||||||
|
createLabel.value = '';
|
||||||
|
swapBottom(true, () => createNameEl.value?.focus());
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeCreateForm = () => {
|
||||||
|
if (!createOpen.value) return;
|
||||||
|
swapBottom(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitCreate = () => {
|
||||||
|
emit('create', {
|
||||||
|
path: createPath.value.trim(),
|
||||||
|
label: createLabel.value.trim()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 创建成功后由父级刷新列表;成功后收起表单 */
|
||||||
|
watch(
|
||||||
|
() => props.createSubmitting,
|
||||||
|
(submitting, prev) => {
|
||||||
|
if (prev && !submitting && createOpen.value && !props.errorMessage) {
|
||||||
|
closeCreateForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ---------- 全局关闭 ---------- */
|
||||||
|
const onDocClick = (event: MouseEvent) => {
|
||||||
|
const target = event.target as Node | null;
|
||||||
|
if (!target) return;
|
||||||
|
if (popoverEl.value && !popoverEl.value.contains(target)) {
|
||||||
|
/* 点击入口按钮:交给按钮自身的 toggle 处理(已展开则收起),
|
||||||
|
避免 capture 阶段先 close、按钮 click 又重新打开导致重播动画 */
|
||||||
|
const anchor = props.anchorRect;
|
||||||
|
if (anchor) {
|
||||||
|
const x = event.clientX;
|
||||||
|
const y = event.clientY;
|
||||||
|
if (x >= anchor.left && x <= anchor.right && y >= anchor.top && y <= anchor.bottom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDocKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key !== 'Escape') return;
|
||||||
|
if (menuOpenId.value) {
|
||||||
|
closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (renamingId.value || deletingId.value) {
|
||||||
|
renamingId.value = null;
|
||||||
|
deletingId.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (createOpen.value) {
|
||||||
|
closeCreateForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWindowResize = () => {
|
||||||
|
closeMenu();
|
||||||
|
updatePosition();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.open,
|
||||||
|
async (open) => {
|
||||||
|
if (open) {
|
||||||
|
menuOpenId.value = null;
|
||||||
|
renamingId.value = null;
|
||||||
|
deletingId.value = null;
|
||||||
|
await nextTick();
|
||||||
|
updatePosition();
|
||||||
|
document.addEventListener('click', onDocClick, true);
|
||||||
|
document.addEventListener('keydown', onDocKeydown);
|
||||||
|
window.addEventListener('resize', onWindowResize);
|
||||||
|
} else {
|
||||||
|
if (createOpen.value) createOpen.value = false;
|
||||||
|
document.removeEventListener('click', onDocClick, true);
|
||||||
|
document.removeEventListener('keydown', onDocKeydown);
|
||||||
|
window.removeEventListener('resize', onWindowResize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', onDocClick, true);
|
||||||
|
document.removeEventListener('keydown', onDocKeydown);
|
||||||
|
window.removeEventListener('resize', onWindowResize);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* ---------- 浮层主体:单层扁平容器 ---------- */
|
||||||
|
.ws-switcher {
|
||||||
|
--ws-row-h: 50px;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1500;
|
||||||
|
width: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--surface-raised);
|
||||||
|
box-shadow: var(--theme-shadow-soft);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-switcher--project {
|
||||||
|
--ws-row-h: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-switcher-enter-active,
|
||||||
|
.ws-switcher-leave-active {
|
||||||
|
transition: opacity 120ms ease, transform 150ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-switcher-enter-from,
|
||||||
|
.ws-switcher-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部 */
|
||||||
|
.ws-switcher__header {
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-bottom: 1px solid var(--border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-switcher__title {
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-switcher__count {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表:刚好 7 行(7h + 6gap + 上下padding),超出内部滚动 */
|
||||||
|
.ws-switcher__list {
|
||||||
|
flex: none;
|
||||||
|
max-height: calc(var(--ws-row-h) * 7 + 18px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--border-strong) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-switcher__list::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-switcher__list::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-strong);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid var(--surface-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-switcher__list::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工作区行 */
|
||||||
|
.ws-row {
|
||||||
|
position: relative;
|
||||||
|
/* flex 子项默认 shrink:1,内容超出容器 max-height 时会被压到 min-content
|
||||||
|
(双行文字 35px),导致行高失效、可见行数虚多;必须禁止压缩 */
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--ws-row-h);
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row:hover {
|
||||||
|
background: color-mix(in srgb, var(--text-primary) 4%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row.current {
|
||||||
|
background: color-mix(in srgb, var(--text-primary) 6%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row.current:hover {
|
||||||
|
background: color-mix(in srgb, var(--text-primary) 7%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row-icon {
|
||||||
|
flex: none;
|
||||||
|
width: 17px;
|
||||||
|
height: 17px; /* 行容器 align-items: center,整行上下居中 */
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row.current .ws-row-icon {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row-path {
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row-meta {
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-more {
|
||||||
|
flex: none;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-more svg {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-more:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: color-mix(in srgb, var(--text-primary) 7%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-row:hover .ws-more,
|
||||||
|
.ws-row.menu-open .ws-more {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-empty {
|
||||||
|
padding: 18px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 重命名内联输入 */
|
||||||
|
.ws-rename-input {
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid var(--accent);
|
||||||
|
outline: none;
|
||||||
|
padding: 1px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除确认内联 */
|
||||||
|
.ws-confirm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-confirm-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用小按钮 */
|
||||||
|
.ws-btn {
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-btn:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--text-primary) 5%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-btn.primary {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--on-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-btn.primary:hover:not(:disabled) {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-btn.danger {
|
||||||
|
background: var(--state-danger);
|
||||||
|
border-color: var(--state-danger);
|
||||||
|
color: var(--on-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-btn.danger:hover:not(:disabled) {
|
||||||
|
background: var(--state-danger-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-btn.ghost {
|
||||||
|
border-color: var(--border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部区域:高度动画容器 */
|
||||||
|
.ws-bottom {
|
||||||
|
flex: none;
|
||||||
|
border-top: 1px solid var(--border-default);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: height 200ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-footer {
|
||||||
|
padding: 6px;
|
||||||
|
transition: opacity 130ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-footer.fading {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-btn:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--text-primary) 4%, transparent);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-icon {
|
||||||
|
flex: none;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-btn:hover:not(:disabled) .ws-create-icon {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新建表单 */
|
||||||
|
.ws-create-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-form.fading {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-form input {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface-soft);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12.5px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-form input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-form input::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-error {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--state-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-create-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 「…」浮动菜单(Teleport 到 body,fixed 定位逃逸裁切) */
|
||||||
|
.ws-menu-floating {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1600;
|
||||||
|
min-width: 136px;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--surface-raised);
|
||||||
|
box-shadow: var(--theme-shadow-soft);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-menu-enter-active,
|
||||||
|
.ws-menu-leave-active {
|
||||||
|
transition: opacity 110ms ease, transform 140ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-menu-enter-from,
|
||||||
|
.ws-menu-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-3px) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-menu-floating button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-menu-floating button:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--text-primary) 5%, transparent);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-menu-floating button.danger {
|
||||||
|
color: var(--state-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-menu-floating button.danger:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--state-danger) 8%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-menu-floating button:disabled {
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-menu-sep {
|
||||||
|
height: 1px;
|
||||||
|
margin: 4px 6px;
|
||||||
|
background: var(--border-default);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -9,7 +9,6 @@ interface ResizeContext {
|
|||||||
rightWidth?: number;
|
rightWidth?: number;
|
||||||
minPanelWidth?: number;
|
minPanelWidth?: number;
|
||||||
maxPanelWidth?: number;
|
maxPanelWidth?: number;
|
||||||
leftWidth?: number;
|
|
||||||
sidebarCollapsed?: boolean;
|
sidebarCollapsed?: boolean;
|
||||||
handleResize?: (event: MouseEvent) => void;
|
handleResize?: (event: MouseEvent) => void;
|
||||||
stopResize?: () => void;
|
stopResize?: () => void;
|
||||||
@ -51,12 +50,7 @@ export function handleResize(ctx: ResizeContext, event: MouseEvent) {
|
|||||||
const minWidth = ctx.minPanelWidth ?? 240;
|
const minWidth = ctx.minPanelWidth ?? 240;
|
||||||
const maxWidth = ctx.maxPanelWidth ?? 480;
|
const maxWidth = ctx.maxPanelWidth ?? 480;
|
||||||
|
|
||||||
if (ctx.resizingPanel === 'left') {
|
if (ctx.resizingPanel === 'right') {
|
||||||
const offset = ctx.sidebarCollapsed ? 60 : 300;
|
|
||||||
let newWidth = event.clientX - offset;
|
|
||||||
newWidth = clamp(newWidth, minWidth, maxWidth);
|
|
||||||
ctx.leftWidth = newWidth;
|
|
||||||
} else if (ctx.resizingPanel === 'right') {
|
|
||||||
let newWidth = containerWidth - event.clientX;
|
let newWidth = containerWidth - event.clientX;
|
||||||
newWidth = clamp(newWidth, minWidth, maxWidth);
|
newWidth = clamp(newWidth, minWidth, maxWidth);
|
||||||
ctx.rightWidth = newWidth;
|
ctx.rightWidth = newWidth;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useUiStore } from './ui';
|
|
||||||
import { useConversationStore } from './conversation';
|
import { useConversationStore } from './conversation';
|
||||||
|
|
||||||
interface BackgroundCommand {
|
interface BackgroundCommand {
|
||||||
@ -21,7 +20,6 @@ interface BackgroundCommandDetail extends BackgroundCommand {
|
|||||||
|
|
||||||
interface BackgroundCommandState {
|
interface BackgroundCommandState {
|
||||||
commands: BackgroundCommand[];
|
commands: BackgroundCommand[];
|
||||||
pollTimer: ReturnType<typeof setInterval> | null;
|
|
||||||
detailPollTimer: ReturnType<typeof setInterval> | null;
|
detailPollTimer: ReturnType<typeof setInterval> | null;
|
||||||
activeCommand: BackgroundCommand | null;
|
activeCommand: BackgroundCommand | null;
|
||||||
/** QuickDock 静默查看的 command_id:拉详情/轮询但不设 activeCommand(避免弹出旧详情窗口) */
|
/** QuickDock 静默查看的 command_id:拉详情/轮询但不设 activeCommand(避免弹出旧详情窗口) */
|
||||||
@ -44,7 +42,6 @@ const TERMINAL_STATUSES = new Set(['completed', 'failed', 'timeout', 'cancelled'
|
|||||||
export const useBackgroundCommandStore = defineStore('backgroundCommand', {
|
export const useBackgroundCommandStore = defineStore('backgroundCommand', {
|
||||||
state: (): BackgroundCommandState => ({
|
state: (): BackgroundCommandState => ({
|
||||||
commands: [],
|
commands: [],
|
||||||
pollTimer: null,
|
|
||||||
detailPollTimer: null,
|
detailPollTimer: null,
|
||||||
activeCommand: null,
|
activeCommand: null,
|
||||||
silentDetailCommandId: null,
|
silentDetailCommandId: null,
|
||||||
@ -84,23 +81,6 @@ export const useBackgroundCommandStore = defineStore('backgroundCommand', {
|
|||||||
console.error('获取后台指令列表失败:', error);
|
console.error('获取后台指令列表失败:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startPolling() {
|
|
||||||
if (this.pollTimer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const uiStore = useUiStore();
|
|
||||||
this.pollTimer = setInterval(() => {
|
|
||||||
if (uiStore.panelMode === 'backgroundCommands') {
|
|
||||||
this.fetchCommands();
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
},
|
|
||||||
stopPolling() {
|
|
||||||
if (this.pollTimer) {
|
|
||||||
clearInterval(this.pollTimer);
|
|
||||||
this.pollTimer = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openCommand(command: BackgroundCommand, options?: { silent?: boolean }) {
|
openCommand(command: BackgroundCommand, options?: { silent?: boolean }) {
|
||||||
if (!command || !command.command_id) {
|
if (!command || !command.command_id) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -166,13 +166,16 @@ export const useFileStore = defineStore('file', {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
async fetchTodoList() {
|
async fetchTodoList() {
|
||||||
try {
|
// /new 等无对话场景:待办是对话级数据,直接清空不发请求。
|
||||||
// 必须携带 conversation_id:todo 数据保存在对话级 terminal 的内存中,
|
// 不带 id 时后端返回工作区级服务 terminal——它可能被 /new 首条消息的
|
||||||
// 不带 id 时后端返回工作区级服务 terminal(永远为空)。
|
// 任务污染而持有最近对话的待办,会导致 /new 页面残留老待办。
|
||||||
const convId = useConversationStore().currentConversationId;
|
const convId = useConversationStore().currentConversationId;
|
||||||
const url = convId
|
if (!convId) {
|
||||||
? `/api/todo-list?conversation_id=${encodeURIComponent(convId)}`
|
this.setTodoList(null);
|
||||||
: '/api/todo-list';
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = `/api/todo-list?conversation_id=${encodeURIComponent(convId)}`;
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data && data.success) {
|
if (data && data.success) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useModelStore } from './model';
|
import { useModelStore } from './model';
|
||||||
import { persistDefaultHideWorkspace, useUiStore } from './ui';
|
|
||||||
|
|
||||||
export type BlockDisplayMode = 'traditional' | 'stacked' | 'minimal';
|
export type BlockDisplayMode = 'traditional' | 'stacked' | 'minimal';
|
||||||
export type CompactMessageDisplay = 'full' | 'brief';
|
export type CompactMessageDisplay = 'full' | 'brief';
|
||||||
@ -62,8 +62,6 @@ interface PersonalForm {
|
|||||||
deep_compress_form: 'file' | 'inject';
|
deep_compress_form: 'file' | 'inject';
|
||||||
agents_md_auto_inject: boolean;
|
agents_md_auto_inject: boolean;
|
||||||
allow_root_file_creation: boolean;
|
allow_root_file_creation: boolean;
|
||||||
default_hide_workspace: boolean;
|
|
||||||
hide_quick_dock: boolean;
|
|
||||||
group_sidebar_by_workspace: boolean;
|
group_sidebar_by_workspace: boolean;
|
||||||
sidebar_pinned_workspaces: string[];
|
sidebar_pinned_workspaces: string[];
|
||||||
sidebar_workspace_order: string[];
|
sidebar_workspace_order: string[];
|
||||||
@ -225,8 +223,6 @@ const defaultForm = (): PersonalForm => ({
|
|||||||
deep_compress_form: 'file',
|
deep_compress_form: 'file',
|
||||||
agents_md_auto_inject: false,
|
agents_md_auto_inject: false,
|
||||||
allow_root_file_creation: false,
|
allow_root_file_creation: false,
|
||||||
default_hide_workspace: false,
|
|
||||||
hide_quick_dock: false,
|
|
||||||
group_sidebar_by_workspace: false,
|
group_sidebar_by_workspace: false,
|
||||||
sidebar_pinned_workspaces: [],
|
sidebar_pinned_workspaces: [],
|
||||||
sidebar_workspace_order: [],
|
sidebar_workspace_order: [],
|
||||||
@ -466,8 +462,6 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
deep_compress_form: data.deep_compress_form === 'inject' ? 'inject' : 'file',
|
deep_compress_form: data.deep_compress_form === 'inject' ? 'inject' : 'file',
|
||||||
agents_md_auto_inject: !!data.agents_md_auto_inject,
|
agents_md_auto_inject: !!data.agents_md_auto_inject,
|
||||||
allow_root_file_creation: !!data.allow_root_file_creation,
|
allow_root_file_creation: !!data.allow_root_file_creation,
|
||||||
default_hide_workspace: !!data.default_hide_workspace,
|
|
||||||
hide_quick_dock: !!data.hide_quick_dock,
|
|
||||||
group_sidebar_by_workspace: !!data.group_sidebar_by_workspace,
|
group_sidebar_by_workspace: !!data.group_sidebar_by_workspace,
|
||||||
sidebar_pinned_workspaces: Array.isArray(data.sidebar_pinned_workspaces)
|
sidebar_pinned_workspaces: Array.isArray(data.sidebar_pinned_workspaces)
|
||||||
? data.sidebar_pinned_workspaces.filter((item: unknown) => typeof item === 'string')
|
? data.sidebar_pinned_workspaces.filter((item: unknown) => typeof item === 'string')
|
||||||
@ -497,12 +491,8 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
if (this.form.theme !== currentTheme) {
|
if (this.form.theme !== currentTheme) {
|
||||||
this.applyTheme(this.form.theme);
|
this.applyTheme(this.form.theme);
|
||||||
}
|
}
|
||||||
persistDefaultHideWorkspace(this.form.default_hide_workspace);
|
|
||||||
persistStackedHideBorders(this.form.stacked_hide_borders);
|
persistStackedHideBorders(this.form.stacked_hide_borders);
|
||||||
persistMinimalExpandHeightLimited(this.form.minimal_expand_height_limited);
|
persistMinimalExpandHeightLimited(this.form.minimal_expand_height_limited);
|
||||||
if (this.form.default_hide_workspace) {
|
|
||||||
useUiStore().setWorkspaceCollapsed(true);
|
|
||||||
}
|
|
||||||
// 简略消息显示:以配置文件为准,同步到旧版 localStorage 镜像供 ChatArea 读取。
|
// 简略消息显示:以配置文件为准,同步到旧版 localStorage 镜像供 ChatArea 读取。
|
||||||
// 一次性迁移:后端仍为默认 full,但本地缓存遗留 brief(旧版纯前端记录)时,回写到配置文件。
|
// 一次性迁移:后端仍为默认 full,但本地缓存遗留 brief(旧版纯前端记录)时,回写到配置文件。
|
||||||
const cachedCompact = this.experiments.compactMessageDisplay;
|
const cachedCompact = this.experiments.compactMessageDisplay;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useUiStore } from './ui';
|
|
||||||
import { useConversationStore } from './conversation';
|
import { useConversationStore } from './conversation';
|
||||||
|
|
||||||
interface SubAgent {
|
interface SubAgent {
|
||||||
@ -25,7 +24,6 @@ interface SubAgentActivityEntry {
|
|||||||
|
|
||||||
interface SubAgentState {
|
interface SubAgentState {
|
||||||
subAgents: SubAgent[];
|
subAgents: SubAgent[];
|
||||||
pollTimer: ReturnType<typeof setInterval> | null;
|
|
||||||
activityTimer: ReturnType<typeof setInterval> | null;
|
activityTimer: ReturnType<typeof setInterval> | null;
|
||||||
activeAgent: SubAgent | null;
|
activeAgent: SubAgent | null;
|
||||||
/** QuickDock 静默查看的 task_id(不设 activeAgent,避免弹出旧进度窗口) */
|
/** QuickDock 静默查看的 task_id(不设 activeAgent,避免弹出旧进度窗口) */
|
||||||
@ -41,7 +39,6 @@ const TERMINAL_STATUSES = new Set(['completed', 'failed', 'timeout', 'terminated
|
|||||||
export const useSubAgentStore = defineStore('subAgent', {
|
export const useSubAgentStore = defineStore('subAgent', {
|
||||||
state: (): SubAgentState => ({
|
state: (): SubAgentState => ({
|
||||||
subAgents: [],
|
subAgents: [],
|
||||||
pollTimer: null,
|
|
||||||
activityTimer: null,
|
activityTimer: null,
|
||||||
activeAgent: null,
|
activeAgent: null,
|
||||||
/** QuickDock 静默查看的 task_id:拉 activity/轮询但不设 activeAgent(避免弹出旧进度窗口) */
|
/** QuickDock 静默查看的 task_id:拉 activity/轮询但不设 activeAgent(避免弹出旧进度窗口) */
|
||||||
@ -92,23 +89,6 @@ export const useSubAgentStore = defineStore('subAgent', {
|
|||||||
console.error('获取子智能体列表失败:', error);
|
console.error('获取子智能体列表失败:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startPolling() {
|
|
||||||
if (this.pollTimer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const uiStore = useUiStore();
|
|
||||||
this.pollTimer = setInterval(() => {
|
|
||||||
if (uiStore.panelMode === 'subAgents') {
|
|
||||||
this.fetchSubAgents();
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
},
|
|
||||||
stopPolling() {
|
|
||||||
if (this.pollTimer) {
|
|
||||||
clearInterval(this.pollTimer);
|
|
||||||
this.pollTimer = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openSubAgent(agent: SubAgent, options?: { silent?: boolean }) {
|
openSubAgent(agent: SubAgent, options?: { silent?: boolean }) {
|
||||||
if (!agent || !agent.task_id) {
|
if (!agent || !agent.task_id) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -1,23 +1,7 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
type PanelMode = 'files' | 'todo' | 'subAgents' | 'backgroundCommands';
|
|
||||||
type ResizingPanel = 'left' | 'right' | null;
|
type ResizingPanel = 'left' | 'right' | null;
|
||||||
type MobileOverlayTarget = 'conversation' | 'workspace' | 'focus' | 'approval' | null;
|
type MobileOverlayTarget = 'conversation' | 'focus' | 'approval' | null;
|
||||||
const DEFAULT_HIDE_WORKSPACE_STORAGE_KEY = 'agents_default_hide_workspace';
|
|
||||||
|
|
||||||
export const loadDefaultHideWorkspace = (): boolean => {
|
|
||||||
if (typeof window === 'undefined' || !window.localStorage) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return window.localStorage.getItem(DEFAULT_HIDE_WORKSPACE_STORAGE_KEY) === '1';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const persistDefaultHideWorkspace = (hidden: boolean) => {
|
|
||||||
if (typeof window === 'undefined' || !window.localStorage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.localStorage.setItem(DEFAULT_HIDE_WORKSPACE_STORAGE_KEY, hidden ? '1' : '0');
|
|
||||||
};
|
|
||||||
|
|
||||||
interface QuotaToast {
|
interface QuotaToast {
|
||||||
message: string;
|
message: string;
|
||||||
@ -68,11 +52,7 @@ interface EasterEggState {
|
|||||||
|
|
||||||
interface UiState {
|
interface UiState {
|
||||||
sidebarCollapsed: boolean;
|
sidebarCollapsed: boolean;
|
||||||
workspaceCollapsed: boolean;
|
|
||||||
chatDisplayMode: 'chat' | 'monitor';
|
chatDisplayMode: 'chat' | 'monitor';
|
||||||
panelMode: PanelMode;
|
|
||||||
panelMenuOpen: boolean;
|
|
||||||
leftWidth: number;
|
|
||||||
rightWidth: number;
|
rightWidth: number;
|
||||||
rightCollapsed: boolean;
|
rightCollapsed: boolean;
|
||||||
rightSplitRatio: number;
|
rightSplitRatio: number;
|
||||||
@ -95,11 +75,7 @@ interface UiState {
|
|||||||
export const useUiStore = defineStore('ui', {
|
export const useUiStore = defineStore('ui', {
|
||||||
state: (): UiState => ({
|
state: (): UiState => ({
|
||||||
sidebarCollapsed: true,
|
sidebarCollapsed: true,
|
||||||
workspaceCollapsed: loadDefaultHideWorkspace(),
|
|
||||||
chatDisplayMode: 'chat',
|
chatDisplayMode: 'chat',
|
||||||
panelMode: 'files',
|
|
||||||
panelMenuOpen: false,
|
|
||||||
leftWidth: 350,
|
|
||||||
rightWidth: 420,
|
rightWidth: 420,
|
||||||
rightCollapsed: true,
|
rightCollapsed: true,
|
||||||
rightSplitRatio: 0.5,
|
rightSplitRatio: 0.5,
|
||||||
@ -133,30 +109,12 @@ export const useUiStore = defineStore('ui', {
|
|||||||
toggleSidebar() {
|
toggleSidebar() {
|
||||||
this.sidebarCollapsed = !this.sidebarCollapsed;
|
this.sidebarCollapsed = !this.sidebarCollapsed;
|
||||||
},
|
},
|
||||||
setWorkspaceCollapsed(collapsed: boolean) {
|
|
||||||
this.workspaceCollapsed = collapsed;
|
|
||||||
},
|
|
||||||
setChatDisplayMode(mode: 'chat' | 'monitor') {
|
setChatDisplayMode(mode: 'chat' | 'monitor') {
|
||||||
this.chatDisplayMode = mode;
|
this.chatDisplayMode = mode;
|
||||||
},
|
},
|
||||||
toggleChatDisplayMode() {
|
toggleChatDisplayMode() {
|
||||||
this.chatDisplayMode = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat';
|
this.chatDisplayMode = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat';
|
||||||
},
|
},
|
||||||
toggleWorkspaceCollapsed() {
|
|
||||||
this.workspaceCollapsed = !this.workspaceCollapsed;
|
|
||||||
},
|
|
||||||
setPanelMode(mode: PanelMode) {
|
|
||||||
this.panelMode = mode;
|
|
||||||
},
|
|
||||||
setPanelMenuOpen(open: boolean) {
|
|
||||||
this.panelMenuOpen = open;
|
|
||||||
},
|
|
||||||
togglePanelMenu() {
|
|
||||||
this.panelMenuOpen = !this.panelMenuOpen;
|
|
||||||
},
|
|
||||||
setLeftWidth(width: number) {
|
|
||||||
this.leftWidth = width;
|
|
||||||
},
|
|
||||||
setRightWidth(width: number) {
|
setRightWidth(width: number) {
|
||||||
this.rightWidth = width;
|
this.rightWidth = width;
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,13 +1,11 @@
|
|||||||
/* Modern reveal conversation sidebar */
|
/* Modern reveal conversation sidebar(挤压式:在文档流内推开主内容) */
|
||||||
.conversation-sidebar {
|
.conversation-sidebar {
|
||||||
--conversation-collapsed-width: 52px;
|
--conversation-collapsed-width: 44px;
|
||||||
--conversation-expanded-width: 306px;
|
--conversation-expanded-width: 306px;
|
||||||
--conversation-icon-cell: 40px;
|
--conversation-icon-cell: 32px;
|
||||||
--conversation-row-height: 40px;
|
--conversation-row-height: 32px;
|
||||||
position: fixed;
|
position: relative;
|
||||||
left: 0;
|
flex-shrink: 0;
|
||||||
top: 0;
|
|
||||||
z-index: 120;
|
|
||||||
width: var(--conversation-expanded-width);
|
width: var(--conversation-expanded-width);
|
||||||
height: var(--app-viewport) !important;
|
height: var(--app-viewport) !important;
|
||||||
min-height: var(--app-viewport) !important;
|
min-height: var(--app-viewport) !important;
|
||||||
@ -44,7 +42,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 18px 8px 14px 6px;
|
padding: 12px 6px 10px 6px;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +59,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: var(--conversation-row-height);
|
min-height: var(--conversation-row-height);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--claude-text);
|
color: var(--claude-text);
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -81,7 +79,7 @@
|
|||||||
.sidebar-nav-icon {
|
.sidebar-nav-icon {
|
||||||
width: var(--conversation-icon-cell);
|
width: var(--conversation-icon-cell);
|
||||||
height: var(--conversation-row-height);
|
height: var(--conversation-row-height);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
@ -119,6 +117,7 @@
|
|||||||
.sidebar-nav-label,
|
.sidebar-nav-label,
|
||||||
.conversation-search,
|
.conversation-search,
|
||||||
.conversation-list,
|
.conversation-list,
|
||||||
|
.workspace-entry-current,
|
||||||
.personal-label {
|
.personal-label {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 220ms ease;
|
transition: opacity 220ms ease;
|
||||||
@ -128,6 +127,7 @@
|
|||||||
.conversation-sidebar.collapsed .sidebar-nav-label,
|
.conversation-sidebar.collapsed .sidebar-nav-label,
|
||||||
.conversation-sidebar.collapsed .conversation-search,
|
.conversation-sidebar.collapsed .conversation-search,
|
||||||
.conversation-sidebar.collapsed .conversation-list,
|
.conversation-sidebar.collapsed .conversation-list,
|
||||||
|
.conversation-sidebar.collapsed .workspace-entry-current,
|
||||||
.conversation-sidebar.collapsed .personal-label {
|
.conversation-sidebar.collapsed .personal-label {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition-delay: 0ms;
|
transition-delay: 0ms;
|
||||||
@ -135,28 +135,33 @@
|
|||||||
|
|
||||||
.sidebar-nav-label {
|
.sidebar-nav-label {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding-left: 8px;
|
padding-left: 6px;
|
||||||
padding-right: 12px;
|
padding-right: 10px;
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
letter-spacing: -0.015em;
|
letter-spacing: -0.01em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-icon svg {
|
.chat-icon svg {
|
||||||
width: 29px;
|
width: 23px;
|
||||||
height: 29px;
|
height: 23px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-menu-btn .chat-icon svg {
|
.conversation-menu-btn .chat-icon svg {
|
||||||
transform: translateY(3px);
|
transform: translateY(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pencil-icon svg {
|
.pencil-icon svg {
|
||||||
width: 22px;
|
width: 18px;
|
||||||
height: 22px;
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layers-icon svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.monitor-mode-btn.blocked {
|
.monitor-mode-btn.blocked {
|
||||||
@ -166,7 +171,7 @@
|
|||||||
|
|
||||||
.conversation-search {
|
.conversation-search {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin-top: 10px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input-wrap {
|
.search-input-wrap {
|
||||||
@ -194,7 +199,7 @@
|
|||||||
padding: 0 12px 0 8px;
|
padding: 0 12px 0 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--claude-text);
|
color: var(--claude-text);
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +212,7 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding-top: 12px;
|
padding-top: 8px;
|
||||||
scrollbar-width: none !important;
|
scrollbar-width: none !important;
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
}
|
}
|
||||||
@ -243,9 +248,9 @@
|
|||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
margin: 1px 0;
|
margin: 1px 0;
|
||||||
padding: 8px 10px 8px 12px;
|
padding: 6px 10px 6px 12px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--claude-text);
|
color: var(--claude-text);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@ -273,7 +278,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
color: var(--claude-text);
|
color: var(--claude-text);
|
||||||
@ -299,9 +304,13 @@ body[data-theme='dark'] .conversation-sidebar .running-task-item.active {
|
|||||||
|
|
||||||
.conversation-sidebar .conversation-item {
|
.conversation-sidebar .conversation-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
grid-template-columns: minmax(0, 1fr) 40px;
|
grid-template-columns: minmax(0, 1fr) 32px;
|
||||||
margin: 1px 0;
|
margin: 1px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
/* 固定行高(规范:带文字容器禁止被内容撑高):min-height 会被 34px 的
|
||||||
|
action-wrap 撑破,导致分组 viewport 的 N 行计算失准、最后一行被裁 */
|
||||||
|
height: var(--conversation-row-height);
|
||||||
|
flex: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
@ -439,7 +448,7 @@ body[data-theme='dark'] .conversation-sidebar .conversation-item.active:focus-wi
|
|||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
color: var(--claude-text);
|
color: var(--claude-text);
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -450,7 +459,7 @@ body[data-theme='dark'] .conversation-sidebar .conversation-item.active:focus-wi
|
|||||||
.conversation-sidebar .conversation-action-wrap {
|
.conversation-sidebar .conversation-action-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 34px;
|
height: var(--conversation-row-height);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
@ -599,9 +608,25 @@ body[data-theme='dark'] .conversation-sidebar .conversation-item.active:focus-wi
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 工作区入口行:图标 + 名称 + 当前工作区名(第三列) */
|
||||||
|
.sidebar-nav-row.workspace-entry-btn {
|
||||||
|
grid-template-columns: var(--conversation-icon-cell) minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-entry-current {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 96px;
|
||||||
|
padding-right: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--claude-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
.personal-page-btn svg {
|
.personal-page-btn svg {
|
||||||
width: 22px;
|
width: 18px;
|
||||||
height: 22px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-conversations,
|
.loading-conversations,
|
||||||
@ -707,7 +732,7 @@ body[data-theme='dark'] .conversation-sidebar .conversation-item.active:focus-wi
|
|||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: var(--conversation-row-height);
|
height: var(--conversation-row-height);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
transition: background 140ms ease;
|
transition: background 140ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -722,13 +747,13 @@ body[data-theme='dark'] .workspace-group-header:hover {
|
|||||||
|
|
||||||
.workspace-group-toggle {
|
.workspace-group-toggle {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 24px minmax(0, 1fr) auto;
|
grid-template-columns: 22px minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 10px;
|
padding: 0 8px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--claude-text);
|
color: var(--claude-text);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@ -741,7 +766,7 @@ body[data-theme='dark'] .workspace-group-header:hover {
|
|||||||
|
|
||||||
.workspace-group-label {
|
.workspace-group-label {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
@use './layout/app-shell';
|
@use './layout/app-shell';
|
||||||
@use './layout/panels';
|
@use './layout/panels';
|
||||||
@use './components/sidebar/conversation';
|
@use './components/sidebar/conversation';
|
||||||
@use './components/panels/left-panel';
|
|
||||||
@use './components/panels/right-panel';
|
@use './components/panels/right-panel';
|
||||||
@use './components/panels/resource-panel';
|
@use './components/panels/resource-panel';
|
||||||
@use './components/chat/chat-area';
|
@use './components/chat/chat-area';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user