agent-Specialization/static/src/components/chat/quickdock/FileWindow.vue
JOJO 171833e4d1 feat(quickdock): 新增对话区右侧快捷窗口(待办/子智能体/后台指令/文件记录)
- 四窗口固定顺序占位布局,空时收起到 0 宽;动画区分「运行中新出现」与「切换/加载」
- 文件记录:edit/write 埋点写入对话 metadata.edited_files,预览走既有 /api/file/content
- 详情面板:lastDetail/lastStatus 快照、首次填充静态+瞬间到底、终态四色分类
- 数据通道全部走 REST 轮询(带 conversation_id 对话级隔离),含 demo 与设计文档
2026-07-22 00:52:36 +08:00

178 lines
5.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<section v-if="rows.length" class="qd-window" :class="{ 'qd-window-enter': windowEntering }">
<header class="qd-window__header">
<svg class="qd-window__icon" viewBox="0 0 16 16" fill="none">
<path
d="M4 2.5h5.5L12 5v8.5H4V2.5z"
stroke="currentColor"
stroke-width="1.3"
stroke-linejoin="round"
/>
<path d="M9.5 2.5V5H12" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round" />
</svg>
<span class="qd-window__title">文件</span>
<span class="qd-window__counter">{{ rows.length }}</span>
</header>
<ul ref="listRef" class="qd-list">
<li
v-for="row in rows"
:key="row.path"
class="qd-file-item"
:class="{ 'is-active': previewPath === row.path, 'qd-row-enter': row.entering }"
:style="row.pendingEnter ? { opacity: '0' } : undefined"
:title="row.path"
@click="openRow(row)"
>
<button class="qd-row-menu-btn" title="更多" @click.stop="openMenu($event, row)">
<svg viewBox="0 0 16 16">
<circle cx="3.5" cy="8" r="1.3" fill="currentColor" />
<circle cx="8" cy="8" r="1.3" fill="currentColor" />
<circle cx="12.5" cy="8" r="1.3" fill="currentColor" />
</svg>
</button>
<span class="qd-file-name">{{ basename(row.path) }}</span>
</li>
</ul>
</section>
</template>
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useQuickDockStore } from '@/stores/quickDock';
/**
* 文件记录窗口
* 本次对话中编辑/创建过且仍存在的文件同一 path 只出现一次
* 行结构[] 文件名点击行 右侧预览侧边栏 菜单下载/打开/复制路径
*/
interface Row {
path: string;
entering: boolean;
pendingEnter: boolean;
}
const quickDock = useQuickDockStore();
const { editedFiles, previewPath } = storeToRefs(quickDock);
const rows = ref<Row[]>([]);
const windowEntering = ref(false);
const listRef = ref<HTMLElement | null>(null);
let windowEnterShown = false;
function basename(p: string): string {
return p.split('/').pop() || p;
}
function openRow(row: Row) {
quickDock.closeMenu();
quickDock.openPreview(row.path);
}
function openMenu(e: MouseEvent, row: Row) {
const btn = e.currentTarget as HTMLElement;
const rect = btn.getBoundingClientRect();
quickDock.openMenu({
type: 'file',
key: row.path,
left: rect.left, // 与按钮左对齐、向下展开
top: rect.bottom + 6,
alignRight: false
});
}
function scrollElIntoView(el: HTMLElement): Promise<void> {
return new Promise((resolve) => {
const listEl = listRef.value;
if (!listEl) {
resolve();
return;
}
const listRect = listEl.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const fullyVisible = elRect.top >= listRect.top && elRect.bottom <= listRect.bottom;
if (fullyVisible) {
resolve();
return;
}
let done = false;
let timer: ReturnType<typeof setTimeout> | undefined;
const finish = () => {
if (done) {
return;
}
done = true;
if (timer) {
clearTimeout(timer);
}
listEl.removeEventListener('scroll', onScroll);
resolve();
};
const onScroll = () => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(finish, 90);
};
listEl.addEventListener('scroll', onScroll);
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
timer = setTimeout(finish, 800);
});
}
async function playEnter(row: Row) {
await nextTick();
const listEl = listRef.value;
const index = rows.value.indexOf(row);
const el = listEl?.children[index] as HTMLElement | undefined;
if (!el) {
row.pendingEnter = false;
row.entering = true;
setTimeout(() => {
row.entering = false;
}, 380);
return;
}
await scrollElIntoView(el);
row.pendingEnter = false;
row.entering = true;
setTimeout(() => {
row.entering = false;
}, 380);
}
watch(
editedFiles,
(list) => {
// live=true 来自任务期实时事件播动画false 来自加载/bootstrap静态呈现
const live = quickDock.editedFilesLive;
const byPath = new Map(list.map((item) => [item.path, item]));
// 移除已消失的行(文件被删除 / 对话切换清空)
rows.value = rows.value.filter((row) => byPath.has(row.path));
// 新增行:实时事件隐身插入 → 滚动露出 → 播进入动画;加载来源直接静态插入
const existing = new Set(rows.value.map((row) => row.path));
const added = list.filter((item) => !existing.has(item.path));
if (added.length) {
if (live && !windowEnterShown && !rows.value.length) {
windowEnterShown = true;
windowEntering.value = true;
setTimeout(() => {
windowEntering.value = false;
}, 320);
}
added.forEach((item) => {
const row: Row = { path: item.path, entering: false, pendingEnter: live };
rows.value.push(row);
if (live) {
void playEnter(row);
}
});
}
},
{ immediate: true }
);
</script>