feat: redesign todo window animation and layout

This commit is contained in:
JOJO 2025-12-14 21:20:29 +08:00
parent 9183e0caf0
commit 8b250c5c6b
3 changed files with 260 additions and 100 deletions

View File

@ -138,6 +138,11 @@
<div class="memory-body"> <div class="memory-body">
<div class="memory-list" ref="memoryList"></div> <div class="memory-list" ref="memoryList"></div>
</div> </div>
<div class="memory-footer hidden" aria-hidden="true">
<span><strong ref="memoryCount"></strong></span>
<span><strong ref="memoryTime"></strong></span>
<span ref="memoryStatus"></span>
</div>
</div> </div>
<div class="window todo-window" ref="todoWindow"> <div class="window todo-window" ref="todoWindow">
@ -145,21 +150,11 @@
<span class="traffic-dot red"></span> <span class="traffic-dot red"></span>
<span class="traffic-dot yellow"></span> <span class="traffic-dot yellow"></span>
<span class="traffic-dot green"></span> <span class="traffic-dot green"></span>
<span>任务看板</span> <span class="window-title">待办事项</span>
</div>
<div class="todo-columns">
<div class="todo-column">
<h4>Backlog</h4>
<div class="todo-list" ref="todoBacklog"></div>
</div>
<div class="todo-column">
<h4>进行中</h4>
<div class="todo-list" ref="todoDoing"></div>
</div>
<div class="todo-column">
<h4>完成</h4>
<div class="todo-list" ref="todoDone"></div>
</div> </div>
<div class="todo-body">
<div class="todo-summary" ref="todoSummary"></div>
<div class="todo-progress" ref="todoList"></div>
</div> </div>
</div> </div>
@ -256,9 +251,8 @@ const memoryStatus = ref<HTMLElement | null>(null);
const memoryCount = ref<HTMLElement | null>(null); const memoryCount = ref<HTMLElement | null>(null);
const memoryTime = ref<HTMLElement | null>(null); const memoryTime = ref<HTMLElement | null>(null);
const todoWindow = ref<HTMLElement | null>(null); const todoWindow = ref<HTMLElement | null>(null);
const todoBacklog = ref<HTMLElement | null>(null); const todoSummary = ref<HTMLElement | null>(null);
const todoDoing = ref<HTMLElement | null>(null); const todoList = ref<HTMLElement | null>(null);
const todoDone = ref<HTMLElement | null>(null);
const desktopMenu = ref<HTMLElement | null>(null); const desktopMenu = ref<HTMLElement | null>(null);
const folderMenu = ref<HTMLElement | null>(null); const folderMenu = ref<HTMLElement | null>(null);
const fileMenu = ref<HTMLElement | null>(null); const fileMenu = ref<HTMLElement | null>(null);
@ -338,9 +332,8 @@ const mountDirector = async () => {
memoryCount: memoryCount.value!, memoryCount: memoryCount.value!,
memoryTime: memoryTime.value!, memoryTime: memoryTime.value!,
todoWindow: todoWindow.value!, todoWindow: todoWindow.value!,
todoBacklog: todoBacklog.value!, todoSummary: todoSummary.value!,
todoDoing: todoDoing.value!, todoList: todoList.value!,
todoDone: todoDone.value!,
desktopMenu: desktopMenu.value!, desktopMenu: desktopMenu.value!,
folderMenu: folderMenu.value!, folderMenu: folderMenu.value!,
fileMenu: fileMenu.value!, fileMenu: fileMenu.value!,

View File

@ -63,9 +63,8 @@ export interface MonitorElements {
memoryCount: HTMLElement; memoryCount: HTMLElement;
memoryTime: HTMLElement; memoryTime: HTMLElement;
todoWindow: HTMLElement; todoWindow: HTMLElement;
todoBacklog: HTMLElement; todoSummary: HTMLElement;
todoDoing: HTMLElement; todoList: HTMLElement;
todoDone: HTMLElement;
desktopMenu: HTMLElement; desktopMenu: HTMLElement;
folderMenu: HTMLElement; folderMenu: HTMLElement;
fileMenu: HTMLElement; fileMenu: HTMLElement;
@ -140,7 +139,7 @@ const DESKTOP_APPS: Array<{ id: string; label: string; assetKey: string }> = [
{ id: 'command', label: '命令行', assetKey: 'command' }, { id: 'command', label: '命令行', assetKey: 'command' },
{ id: 'python', label: 'Python', assetKey: 'python' }, { id: 'python', label: 'Python', assetKey: 'python' },
{ id: 'memory', label: '记忆', assetKey: 'memory' }, { id: 'memory', label: '记忆', assetKey: 'memory' },
{ id: 'todo', label: '看板', assetKey: 'todo' }, { id: 'todo', label: '待办事项', assetKey: 'todo' },
{ id: 'subagent', label: '子代理', assetKey: 'subagent' } { id: 'subagent', label: '子代理', assetKey: 'subagent' }
]; ];
@ -344,9 +343,8 @@ export class MonitorDirector implements MonitorDriver {
this.terminalLastFocusedAt = 0; this.terminalLastFocusedAt = 0;
this.elements.readerLines.innerHTML = ''; this.elements.readerLines.innerHTML = '';
this.elements.readerOcr.innerHTML = ''; this.elements.readerOcr.innerHTML = '';
this.elements.todoBacklog.innerHTML = ''; this.elements.todoSummary.textContent = '';
this.elements.todoDoing.innerHTML = ''; this.elements.todoList.innerHTML = '';
this.elements.todoDone.innerHTML = '';
this.folderIcons.clear(); this.folderIcons.clear();
this.fileIcons.clear(); this.fileIcons.clear();
} }
@ -2770,34 +2768,56 @@ export class MonitorDirector implements MonitorDriver {
this.sceneHandlers.todoCreate = async (payload, runtime) => { this.sceneHandlers.todoCreate = async (payload, runtime) => {
this.applySceneStatus(runtime, 'todoCreate', '正在更新待办'); this.applySceneStatus(runtime, 'todoCreate', '正在更新待办');
await this.movePointerToApp('todo'); const summary =
await this.click({ count: 2 }); payload?.arguments?.summary ||
this.showWindow(this.elements.todoWindow); payload?.arguments?.overview ||
const title = payload?.arguments?.title || payload?.arguments?.task || '新的待办'; payload?.arguments?.title ||
this.pushTodoCard('backlog', title); payload?.arguments?.task ||
await sleep(400); '待办摘要';
const tasks = this.normalizeTodoTasks(payload?.arguments?.tasks || summary);
await this.ensureTodoWindowVisible();
await this.typeTodoSummary(summary);
for (const task of tasks) {
await this.animateTodoAppend(task);
}
await sleep(320);
}; };
this.sceneHandlers.todoUpdate = async (payload, runtime) => { this.sceneHandlers.todoUpdate = async (payload, runtime) => {
await this.sceneHandlers.todoCreate(payload, runtime); this.applySceneStatus(runtime, 'todoUpdate', '正在调整待办');
this.applySceneStatus(runtime, 'todoUpdate', '正在调整进度'); await this.ensureTodoWindowVisible();
this.shiftTodoCard('backlog', 'doing'); const targetText = payload?.arguments?.title || payload?.arguments?.task || null;
await sleep(400); const completed =
payload?.arguments?.completed ?? payload?.arguments?.done ?? payload?.arguments?.checked ?? true;
await this.toggleTodoItem(targetText, !!completed);
await sleep(260);
}; };
this.sceneHandlers.todoFinish = async (_payload, runtime) => { this.sceneHandlers.todoFinish = async (_payload, runtime) => {
this.applySceneStatus(runtime, 'todoFinish', '正在完成任务'); this.applySceneStatus(runtime, 'todoFinish', '正在完成任务');
this.shiftTodoCard('doing', 'done'); const redDot = this.elements.todoWindow.querySelector('.traffic-dot.red') as HTMLElement | null;
await sleep(400); if (redDot) {
await this.movePointerToElement(redDot, { duration: 420 });
await this.click();
}
this.closeWindow(this.elements.todoWindow);
await sleep(320);
}; };
this.sceneHandlers.todoFinishConfirm = this.sceneHandlers.todoFinish; this.sceneHandlers.todoFinishConfirm = this.sceneHandlers.todoFinish;
this.sceneHandlers.todoDelete = async (payload, runtime) => { this.sceneHandlers.todoDelete = async (payload, runtime) => {
this.applySceneStatus(runtime, 'todoDelete', '正在移除任务'); this.applySceneStatus(runtime, 'todoDelete', '正在移除待办');
const title = payload?.arguments?.title; const targetText = payload?.arguments?.title || payload?.arguments?.task || null;
this.removeTodoCard(title); const card = this.findTodoItemByText(targetText);
await sleep(300); if (card) {
await this.scrollTodoItemIntoView(card);
card.classList.add('removing');
await this.movePointerToElement(card, { duration: 420 });
await this.click({ right: true });
setTimeout(() => card.remove(), 260);
}
await sleep(280);
}; };
this.sceneHandlers.terminalSession = async (payload, runtime) => { this.sceneHandlers.terminalSession = async (payload, runtime) => {
@ -4989,51 +5009,116 @@ export class MonitorDirector implements MonitorDriver {
this.updateMemoryMeta(); this.updateMemoryMeta();
} }
private pushTodoCard(column: 'backlog' | 'doing' | 'done', title: string) { private getTodoItems(): HTMLElement[] {
const map: Record<'backlog' | 'doing' | 'done', HTMLElement> = { if (!this.elements.todoList) return [];
backlog: this.elements.todoBacklog, return Array.from(this.elements.todoList.children) as HTMLElement[];
doing: this.elements.todoDoing,
done: this.elements.todoDone
};
const card = document.createElement('div');
card.className = 'todo-card';
card.textContent = title;
map[column].appendChild(card);
} }
private shiftTodoCard(source: 'backlog' | 'doing', target: 'doing' | 'done') { private findTodoItemByText(text?: string | null): HTMLElement | null {
const map: Record<'backlog' | 'doing' | 'done', HTMLElement> = { if (!text) return this.getTodoItems()[0] || null;
backlog: this.elements.todoBacklog, const target = (text || '').trim();
doing: this.elements.todoDoing, return this.getTodoItems().find(item => item.querySelector('.todo-text')?.textContent?.trim() === target) || null;
done: this.elements.todoDone
};
const sourceList = map[source];
if (!sourceList.firstElementChild) {
return;
} }
const card = sourceList.firstElementChild as HTMLElement;
sourceList.removeChild(card); private normalizeTodoTasks(raw: any): Array<{ text: string; done?: boolean }> {
card.classList.add('moving'); if (Array.isArray(raw)) {
requestAnimationFrame(() => { return raw
card.classList.remove('moving'); .map(item => {
map[target].appendChild(card); if (typeof item === 'string') return { text: item, done: false };
if (target === 'done') { if (item && typeof item === 'object') {
card.classList.add('done'); const text = String(item.title || item.task || item.text || '').trim();
const done = typeof item.completed === 'boolean' ? item.completed : !!item.done;
if (!text) return null;
return { text, done };
} }
return null;
})
.filter(Boolean) as Array<{ text: string; done?: boolean }>;
}
if (typeof raw === 'string') {
return [{ text: raw, done: false }];
}
return [];
}
private createTodoItem(text: string, done = false) {
const item = document.createElement('div');
item.className = 'todo-item fly-in';
const body = document.createElement('div');
body.className = 'todo-text';
body.textContent = text;
const check = document.createElement('div');
check.className = 'todo-check';
if (done) {
check.classList.add('checked');
item.classList.add('done');
}
item.appendChild(body);
item.appendChild(check);
return item;
}
private renderTodoItems(items: Array<{ text: string; done?: boolean }>) {
if (!this.elements.todoList) return;
this.elements.todoList.innerHTML = '';
items.forEach(task => {
const card = this.createTodoItem(task.text, !!task.done);
card.classList.add('visible');
this.elements.todoList.appendChild(card);
}); });
} }
private removeTodoCard(title?: string) { private async ensureTodoWindowVisible() {
if (!title) { if (this.isWindowVisible(this.elements.todoWindow)) {
this.showWindow(this.elements.todoWindow);
return; return;
} }
const selector = `.todo-card`; await this.movePointerToApp('todo');
const cards = this.elements.todoWindow.querySelectorAll<HTMLElement>(selector); await this.click({ count: 2 });
cards.forEach(card => { this.showWindow(this.elements.todoWindow);
if (card.textContent?.trim() === title.trim()) {
card.classList.add('removing');
setTimeout(() => card.remove(), 300);
} }
});
private async typeTodoSummary(text: string) {
if (!this.elements.todoSummary) return;
await this.movePointerToElement(this.elements.todoSummary, { duration: 420 });
await this.click();
this.elements.todoSummary.textContent = '';
const chars = Array.from(text || '暂无摘要');
for (const ch of chars) {
this.elements.todoSummary.textContent = (this.elements.todoSummary.textContent || '') + ch;
await sleep(20);
}
}
private async animateTodoAppend(task: { text: string; done?: boolean }) {
if (!this.elements.todoList) return;
const card = this.createTodoItem(task.text, !!task.done);
this.elements.todoList.appendChild(card);
requestAnimationFrame(() => card.classList.add('visible'));
await this.scrollTodoItemIntoView(card);
await sleep(180);
}
private async scrollTodoItemIntoView(card: HTMLElement | null) {
if (!card || !this.elements.todoList) return;
const body = this.elements.todoList;
const targetTop = card.offsetTop - body.clientHeight * 0.2;
const clamped = Math.max(0, targetTop);
body.scrollTo({ top: clamped, behavior: 'smooth' });
await this.waitForScrollSettled(body, clamped);
}
private async toggleTodoItem(text?: string | null, done?: boolean) {
const card = this.findTodoItemByText(text);
if (!card) return;
await this.scrollTodoItemIntoView(card);
const check = card.querySelector('.todo-check') as HTMLElement | null;
if (!check) return;
await this.movePointerToElement(check, { duration: 420 });
await this.click();
const targetState = typeof done === 'boolean' ? done : !check.classList.contains('checked');
check.classList.toggle('checked', targetState);
card.classList.toggle('done', targetState);
await sleep(200);
} }
} }

View File

@ -1135,32 +1135,114 @@
transition: transform 0.24s ease, opacity 0.22s ease; transition: transform 0.24s ease, opacity 0.22s ease;
} }
/* memory-tag removed to simplify UI */ .virtual-monitor-surface .memory-footer.hidden { display: none; }
.virtual-monitor-surface .todo-columns { /* Todo Window */
display: grid; .virtual-monitor-surface .todo-window {
grid-template-columns: repeat(3, 1fr); width: 520px;
gap: 12px; height: 360px;
} top: 180px;
left: 520px;
.virtual-monitor-surface .todo-column {
background: rgba(255, 255, 255, 0.65);
border-radius: 12px;
padding: 10px;
}
.virtual-monitor-surface .todo-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px;
} }
.virtual-monitor-surface .todo-card { .virtual-monitor-surface .todo-body {
background: #fff; flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
}
.virtual-monitor-surface .todo-summary {
max-height: 120px;
min-height: 64px;
padding: 10px 12px;
border-radius: 10px; border-radius: 10px;
padding: 8px 10px; background: rgba(15, 23, 42, 0.04);
border: 1px solid rgba(15, 23, 42, 0.08);
overflow-y: auto;
line-height: 1.5;
font-size: 13px; font-size: 13px;
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12); }
.virtual-monitor-surface .todo-progress {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
padding-right: 2px;
}
.virtual-monitor-surface .todo-summary,
.virtual-monitor-surface .todo-progress {
scrollbar-width: none;
-ms-overflow-style: none;
}
.virtual-monitor-surface .todo-summary::-webkit-scrollbar,
.virtual-monitor-surface .todo-progress::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
.virtual-monitor-surface .todo-item {
display: grid;
grid-template-columns: 1fr 32px;
gap: 10px;
align-items: center;
padding: 12px 14px;
border-radius: 12px;
background: #fff;
border: 1px solid rgba(15, 23, 42, 0.08);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12);
opacity: 0;
transform: translateX(-28px);
transition: transform 0.32s ease, opacity 0.28s ease, box-shadow 0.22s ease;
}
.virtual-monitor-surface .todo-item.visible {
opacity: 1;
transform: translateX(0);
}
.virtual-monitor-surface .todo-item.done {
border-color: rgba(72, 187, 120, 0.55);
box-shadow: 0 10px 24px rgba(72, 187, 120, 0.18);
}
.virtual-monitor-surface .todo-text {
line-height: 1.5;
word-break: break-word;
white-space: pre-wrap;
font-size: 13px;
}
.virtual-monitor-surface .todo-check {
width: 20px;
height: 20px;
border-radius: 6px;
border: 2px solid rgba(15, 23, 42, 0.16);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
position: relative;
}
.virtual-monitor-surface .todo-check.checked {
background: linear-gradient(135deg, #4caf50, #60c78f);
border-color: transparent;
}
.virtual-monitor-surface .todo-check.checked::after {
content: '';
color: #fff;
font-size: 12px;
line-height: 1;
} }
.virtual-monitor-surface .todo-card.done { .virtual-monitor-surface .todo-card.done {