feat: redesign todo window animation and layout
This commit is contained in:
parent
9183e0caf0
commit
8b250c5c6b
@ -138,6 +138,11 @@
|
||||
<div class="memory-body">
|
||||
<div class="memory-list" ref="memoryList"></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 class="window todo-window" ref="todoWindow">
|
||||
@ -145,21 +150,11 @@
|
||||
<span class="traffic-dot red"></span>
|
||||
<span class="traffic-dot yellow"></span>
|
||||
<span class="traffic-dot green"></span>
|
||||
<span>任务看板</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>
|
||||
<span class="window-title">待办事项</span>
|
||||
</div>
|
||||
<div class="todo-body">
|
||||
<div class="todo-summary" ref="todoSummary"></div>
|
||||
<div class="todo-progress" ref="todoList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -256,9 +251,8 @@ const memoryStatus = ref<HTMLElement | null>(null);
|
||||
const memoryCount = ref<HTMLElement | null>(null);
|
||||
const memoryTime = ref<HTMLElement | null>(null);
|
||||
const todoWindow = ref<HTMLElement | null>(null);
|
||||
const todoBacklog = ref<HTMLElement | null>(null);
|
||||
const todoDoing = ref<HTMLElement | null>(null);
|
||||
const todoDone = ref<HTMLElement | null>(null);
|
||||
const todoSummary = ref<HTMLElement | null>(null);
|
||||
const todoList = ref<HTMLElement | null>(null);
|
||||
const desktopMenu = ref<HTMLElement | null>(null);
|
||||
const folderMenu = ref<HTMLElement | null>(null);
|
||||
const fileMenu = ref<HTMLElement | null>(null);
|
||||
@ -338,9 +332,8 @@ const mountDirector = async () => {
|
||||
memoryCount: memoryCount.value!,
|
||||
memoryTime: memoryTime.value!,
|
||||
todoWindow: todoWindow.value!,
|
||||
todoBacklog: todoBacklog.value!,
|
||||
todoDoing: todoDoing.value!,
|
||||
todoDone: todoDone.value!,
|
||||
todoSummary: todoSummary.value!,
|
||||
todoList: todoList.value!,
|
||||
desktopMenu: desktopMenu.value!,
|
||||
folderMenu: folderMenu.value!,
|
||||
fileMenu: fileMenu.value!,
|
||||
|
||||
@ -63,9 +63,8 @@ export interface MonitorElements {
|
||||
memoryCount: HTMLElement;
|
||||
memoryTime: HTMLElement;
|
||||
todoWindow: HTMLElement;
|
||||
todoBacklog: HTMLElement;
|
||||
todoDoing: HTMLElement;
|
||||
todoDone: HTMLElement;
|
||||
todoSummary: HTMLElement;
|
||||
todoList: HTMLElement;
|
||||
desktopMenu: HTMLElement;
|
||||
folderMenu: HTMLElement;
|
||||
fileMenu: HTMLElement;
|
||||
@ -140,7 +139,7 @@ const DESKTOP_APPS: Array<{ id: string; label: string; assetKey: string }> = [
|
||||
{ id: 'command', label: '命令行', assetKey: 'command' },
|
||||
{ id: 'python', label: 'Python', assetKey: 'python' },
|
||||
{ id: 'memory', label: '记忆', assetKey: 'memory' },
|
||||
{ id: 'todo', label: '看板', assetKey: 'todo' },
|
||||
{ id: 'todo', label: '待办事项', assetKey: 'todo' },
|
||||
{ id: 'subagent', label: '子代理', assetKey: 'subagent' }
|
||||
];
|
||||
|
||||
@ -344,9 +343,8 @@ export class MonitorDirector implements MonitorDriver {
|
||||
this.terminalLastFocusedAt = 0;
|
||||
this.elements.readerLines.innerHTML = '';
|
||||
this.elements.readerOcr.innerHTML = '';
|
||||
this.elements.todoBacklog.innerHTML = '';
|
||||
this.elements.todoDoing.innerHTML = '';
|
||||
this.elements.todoDone.innerHTML = '';
|
||||
this.elements.todoSummary.textContent = '';
|
||||
this.elements.todoList.innerHTML = '';
|
||||
this.folderIcons.clear();
|
||||
this.fileIcons.clear();
|
||||
}
|
||||
@ -2770,34 +2768,56 @@ export class MonitorDirector implements MonitorDriver {
|
||||
|
||||
this.sceneHandlers.todoCreate = async (payload, runtime) => {
|
||||
this.applySceneStatus(runtime, 'todoCreate', '正在更新待办');
|
||||
await this.movePointerToApp('todo');
|
||||
await this.click({ count: 2 });
|
||||
this.showWindow(this.elements.todoWindow);
|
||||
const title = payload?.arguments?.title || payload?.arguments?.task || '新的待办';
|
||||
this.pushTodoCard('backlog', title);
|
||||
await sleep(400);
|
||||
const summary =
|
||||
payload?.arguments?.summary ||
|
||||
payload?.arguments?.overview ||
|
||||
payload?.arguments?.title ||
|
||||
payload?.arguments?.task ||
|
||||
'待办摘要';
|
||||
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) => {
|
||||
await this.sceneHandlers.todoCreate(payload, runtime);
|
||||
this.applySceneStatus(runtime, 'todoUpdate', '正在调整进度');
|
||||
this.shiftTodoCard('backlog', 'doing');
|
||||
await sleep(400);
|
||||
this.applySceneStatus(runtime, 'todoUpdate', '正在调整待办');
|
||||
await this.ensureTodoWindowVisible();
|
||||
const targetText = payload?.arguments?.title || payload?.arguments?.task || null;
|
||||
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.applySceneStatus(runtime, 'todoFinish', '正在完成任务');
|
||||
this.shiftTodoCard('doing', 'done');
|
||||
await sleep(400);
|
||||
const redDot = this.elements.todoWindow.querySelector('.traffic-dot.red') as HTMLElement | null;
|
||||
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.todoDelete = async (payload, runtime) => {
|
||||
this.applySceneStatus(runtime, 'todoDelete', '正在移除任务');
|
||||
const title = payload?.arguments?.title;
|
||||
this.removeTodoCard(title);
|
||||
await sleep(300);
|
||||
this.applySceneStatus(runtime, 'todoDelete', '正在移除待办');
|
||||
const targetText = payload?.arguments?.title || payload?.arguments?.task || null;
|
||||
const card = this.findTodoItemByText(targetText);
|
||||
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) => {
|
||||
@ -4989,51 +5009,116 @@ export class MonitorDirector implements MonitorDriver {
|
||||
this.updateMemoryMeta();
|
||||
}
|
||||
|
||||
private pushTodoCard(column: 'backlog' | 'doing' | 'done', title: string) {
|
||||
const map: Record<'backlog' | 'doing' | 'done', HTMLElement> = {
|
||||
backlog: this.elements.todoBacklog,
|
||||
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 getTodoItems(): HTMLElement[] {
|
||||
if (!this.elements.todoList) return [];
|
||||
return Array.from(this.elements.todoList.children) as HTMLElement[];
|
||||
}
|
||||
|
||||
private shiftTodoCard(source: 'backlog' | 'doing', target: 'doing' | 'done') {
|
||||
const map: Record<'backlog' | 'doing' | 'done', HTMLElement> = {
|
||||
backlog: this.elements.todoBacklog,
|
||||
doing: this.elements.todoDoing,
|
||||
done: this.elements.todoDone
|
||||
};
|
||||
const sourceList = map[source];
|
||||
if (!sourceList.firstElementChild) {
|
||||
return;
|
||||
private findTodoItemByText(text?: string | null): HTMLElement | null {
|
||||
if (!text) return this.getTodoItems()[0] || null;
|
||||
const target = (text || '').trim();
|
||||
return this.getTodoItems().find(item => item.querySelector('.todo-text')?.textContent?.trim() === target) || null;
|
||||
}
|
||||
const card = sourceList.firstElementChild as HTMLElement;
|
||||
sourceList.removeChild(card);
|
||||
card.classList.add('moving');
|
||||
requestAnimationFrame(() => {
|
||||
card.classList.remove('moving');
|
||||
map[target].appendChild(card);
|
||||
if (target === 'done') {
|
||||
card.classList.add('done');
|
||||
|
||||
private normalizeTodoTasks(raw: any): Array<{ text: string; done?: boolean }> {
|
||||
if (Array.isArray(raw)) {
|
||||
return raw
|
||||
.map(item => {
|
||||
if (typeof item === 'string') return { text: item, done: false };
|
||||
if (item && typeof item === 'object') {
|
||||
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) {
|
||||
if (!title) {
|
||||
private async ensureTodoWindowVisible() {
|
||||
if (this.isWindowVisible(this.elements.todoWindow)) {
|
||||
this.showWindow(this.elements.todoWindow);
|
||||
return;
|
||||
}
|
||||
const selector = `.todo-card`;
|
||||
const cards = this.elements.todoWindow.querySelectorAll<HTMLElement>(selector);
|
||||
cards.forEach(card => {
|
||||
if (card.textContent?.trim() === title.trim()) {
|
||||
card.classList.add('removing');
|
||||
setTimeout(() => card.remove(), 300);
|
||||
await this.movePointerToApp('todo');
|
||||
await this.click({ count: 2 });
|
||||
this.showWindow(this.elements.todoWindow);
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1135,32 +1135,114 @@
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .todo-column {
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .todo-list {
|
||||
/* Todo Window */
|
||||
.virtual-monitor-surface .todo-window {
|
||||
width: 520px;
|
||||
height: 360px;
|
||||
top: 180px;
|
||||
left: 520px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .todo-card {
|
||||
background: #fff;
|
||||
.virtual-monitor-surface .todo-body {
|
||||
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;
|
||||
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;
|
||||
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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user