fix(sidebar): 深色模式类型切换选中态配色,双类型常驻缓存修复切换动画

This commit is contained in:
JOJO 2026-08-12 02:20:03 +08:00
parent ee03b34c41
commit a33c3fa6af
9 changed files with 315 additions and 147 deletions

View File

@ -196,10 +196,13 @@ export const actionMethods = {
[newConversationId]: 'create' [newConversationId]: 'create'
}; };
this.conversationListAnimationMode = 'create'; this.conversationListAnimationMode = 'create';
this.conversations = [ /* 原地 splice 保持数组引用conversations 与双类型缓存中当前类型列表同一引用 */
this.conversations.splice(
0,
this.conversations.length,
placeholder, placeholder,
...this.conversations.filter((conv) => conv && conv.id !== newConversationId) ...this.conversations.filter((conv) => conv && conv.id !== newConversationId)
]; );
// 分组视图下同步在当前工作区分组顶部插入占位 // 分组视图下同步在当前工作区分组顶部插入占位
try { try {
@ -212,10 +215,12 @@ export const actionMethods = {
(g: any) => g.workspaceId === currentWorkspaceId (g: any) => g.workspaceId === currentWorkspaceId
); );
if (group) { if (group) {
group.conversations = [ group.conversations.splice(
0,
group.conversations.length,
placeholder, placeholder,
...group.conversations.filter((conv: any) => conv.id !== newConversationId) ...group.conversations.filter((conv: any) => conv.id !== newConversationId)
]; );
group.expanded = true; group.expanded = true;
group.visibleOffset = 0; group.visibleOffset = 0;
} }
@ -336,10 +341,12 @@ export const actionMethods = {
conversationStore.ensureWorkspaceGroup(workspaceId); conversationStore.ensureWorkspaceGroup(workspaceId);
const group = conversationStore.workspaceGroups.find((g: any) => g.workspaceId === workspaceId); const group = conversationStore.workspaceGroups.find((g: any) => g.workspaceId === workspaceId);
if (group) { if (group) {
group.conversations = [ group.conversations.splice(
0,
group.conversations.length,
placeholder, placeholder,
...group.conversations.filter((conv: any) => conv.id !== newConversationId) ...group.conversations.filter((conv: any) => conv.id !== newConversationId)
]; );
group.expanded = true; group.expanded = true;
group.visibleOffset = 0; group.visibleOffset = 0;
} }
@ -432,7 +439,11 @@ export const actionMethods = {
const { useConversationStore } = await import('../../../stores/conversation'); const { useConversationStore } = await import('../../../stores/conversation');
const conversationStore = useConversationStore(); const conversationStore = useConversationStore();
conversationStore.workspaceGroups.forEach((group: any) => { conversationStore.workspaceGroups.forEach((group: any) => {
group.conversations = group.conversations.filter((conv: any) => conv.id !== conversationId); group.conversations.splice(
0,
group.conversations.length,
...group.conversations.filter((conv: any) => conv.id !== conversationId)
);
const maxVisibleOffset = Math.max(0, group.conversations.length - group.visibleLimit); const maxVisibleOffset = Math.max(0, group.conversations.length - group.visibleLimit);
if (group.visibleOffset > maxVisibleOffset) { if (group.visibleOffset > maxVisibleOffset) {
group.visibleOffset = maxVisibleOffset; group.visibleOffset = maxVisibleOffset;
@ -443,8 +454,10 @@ export const actionMethods = {
} }
await waitForAnimation(430); await waitForAnimation(430);
this.conversations = this.conversations.filter( this.conversations.splice(
(conversation) => conversation.id !== conversationId 0,
this.conversations.length,
...this.conversations.filter((conversation) => conversation.id !== conversationId)
); );
this.searchResults = this.searchResults.filter( this.searchResults = this.searchResults.filter(
(conversation) => conversation.id !== conversationId (conversation) => conversation.id !== conversationId
@ -532,13 +545,15 @@ export const actionMethods = {
(conv) => conv && conv.id === conversationId (conv) => conv && conv.id === conversationId
); );
if (insertIndex >= 0) { if (insertIndex >= 0) {
this.conversations = [ this.conversations.splice(
0,
this.conversations.length,
...withoutDuplicate.slice(0, insertIndex + 1), ...withoutDuplicate.slice(0, insertIndex + 1),
duplicatePlaceholder, duplicatePlaceholder,
...withoutDuplicate.slice(insertIndex + 1) ...withoutDuplicate.slice(insertIndex + 1)
]; );
} else { } else {
this.conversations = [duplicatePlaceholder, ...withoutDuplicate]; this.conversations.splice(0, this.conversations.length, duplicatePlaceholder, ...withoutDuplicate);
} }
window.setTimeout(() => { window.setTimeout(() => {
@ -560,13 +575,15 @@ export const actionMethods = {
const sourceGroupIndex = group.conversations.findIndex((conv: any) => conv.id === conversationId); const sourceGroupIndex = group.conversations.findIndex((conv: any) => conv.id === conversationId);
const withoutDuplicate = group.conversations.filter((conv: any) => conv.id !== newId); const withoutDuplicate = group.conversations.filter((conv: any) => conv.id !== newId);
if (sourceGroupIndex >= 0) { if (sourceGroupIndex >= 0) {
group.conversations = [ group.conversations.splice(
0,
group.conversations.length,
...withoutDuplicate.slice(0, sourceGroupIndex + 1), ...withoutDuplicate.slice(0, sourceGroupIndex + 1),
duplicatePlaceholder, duplicatePlaceholder,
...withoutDuplicate.slice(sourceGroupIndex + 1) ...withoutDuplicate.slice(sourceGroupIndex + 1)
]; );
} else { } else {
group.conversations = [duplicatePlaceholder, ...withoutDuplicate]; group.conversations.splice(0, group.conversations.length, duplicatePlaceholder, ...withoutDuplicate);
} }
group.expanded = true; group.expanded = true;
} }

View File

@ -12,37 +12,49 @@ export const loadMethods = {
* ConversationSidebar conversation-type-change * ConversationSidebar conversation-type-change
* store sidebarConversationType * store sidebarConversationType
*/ */
/**
* /
* store setSidebarConversationType
*
*
*/
async handleSidebarConversationTypeChange(_type?: 'normal' | 'multi_agent') { async handleSidebarConversationTypeChange(_type?: 'normal' | 'multi_agent') {
this.conversationsOffset = 0; const { useConversationStore } = await import('../../../stores/conversation');
this.hasMoreConversations = false; const conversationStore = useConversationStore();
await this.loadConversationsList(); const listType = conversationStore.sidebarConversationType;
if (!conversationStore.conversationsCache[listType]?.loaded) {
this.conversationsOffset = 0;
this.hasMoreConversations = false;
await this.loadConversationsList();
}
try { try {
const { useConversationStore } = await import('../../../stores/conversation');
const conversationStore = useConversationStore();
const groups = Array.isArray(conversationStore.workspaceGroups) const groups = Array.isArray(conversationStore.workspaceGroups)
? conversationStore.workspaceGroups ? conversationStore.workspaceGroups
: []; : [];
for (const group of groups) { for (const group of groups) {
if (group && group.workspaceId) { if (group && group.workspaceId && !group.pagingByType?.[listType]?.loaded) {
await this.loadWorkspaceConversations(group.workspaceId, { reset: true }); await conversationStore.loadWorkspaceConversations(group.workspaceId);
} }
} }
} catch (error) { } catch (error) {
console.error('刷新工作区分组对话失败:', error); console.error('补载工作区分组对话失败:', error);
} }
}, },
async loadConversationsList() { async loadConversationsList() {
const queryOffset = this.conversationsOffset; const queryOffset = this.conversationsOffset;
const queryLimit = this.conversationsLimit; const queryLimit = this.conversationsLimit;
const { useConversationStore } = await import('../../../stores/conversation');
const conversationStore = useConversationStore();
/* 锁定发起时类型:响应期间用户可能已切换过滤器,数据始终写入对应类型缓存 */
const listType = conversationStore.sidebarConversationType;
const refreshToken = const refreshToken =
queryOffset === 0 ? ++this.conversationListRefreshToken : this.conversationListRefreshToken; queryOffset === 0 ? ++this.conversationListRefreshToken : this.conversationListRefreshToken;
const requestSeq = ++this.conversationListRequestSeq; const requestSeq = ++this.conversationListRequestSeq;
this.conversationsLoading = true; this.conversationsLoading = true;
try { try {
// 列表过滤由侧边栏类型过滤器(普通/多智能体)决定 // 列表过滤由请求发起时的侧边栏类型过滤器(普通/多智能体)决定
const { useConversationStore } = await import('../../../stores/conversation'); const maParam = listType === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0';
const maParam = useConversationStore().sidebarConversationType === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0';
const response = await fetch(`/api/conversations?limit=${queryLimit}&offset=${queryOffset}${maParam}`); const response = await fetch(`/api/conversations?limit=${queryLimit}&offset=${queryOffset}${maParam}`);
const data = await response.json(); const data = await response.json();
@ -57,32 +69,48 @@ export const loadMethods = {
return; return;
} }
/* 原地写入该类型缓存(当前显示类型的缓存与 conversations 同一引用,原地修改即同步显示) */
const cache = conversationStore.conversationsCache[listType];
const items = data.data.conversations;
if (queryOffset === 0) { if (queryOffset === 0) {
this.conversations = data.data.conversations; cache.list.splice(0, cache.list.length, ...items);
} else { } else {
this.conversations.push(...data.data.conversations); cache.list.push(...items);
} }
if (this.currentConversationId) { cache.offset = queryOffset;
this.promoteConversationToTop(this.currentConversationId); cache.hasMore = data.data.has_more;
} cache.loaded = true;
this.hasMoreConversations = data.data.has_more;
debugLog(`已加载 ${this.conversations.length} 个对话`);
if ( /* 仅仍是当前显示类型时同步扁平字段与首对话自动加载 */
this.conversationsOffset === 0 && if (listType === conversationStore.sidebarConversationType) {
!this.currentConversationId && this.conversations = cache.list;
this.conversations.length > 0 && this.hasMoreConversations = cache.hasMore;
!this.isExplicitNewConversationRoute() if (this.currentConversationId) {
) { this.promoteConversationToTop(this.currentConversationId);
// 只有在初始化完成后,才自动加载第一个对话 }
// 避免与 bootstrapRoute 冲突 if (
if (this.initialRouteResolved) { queryOffset === 0 &&
const latestConversation = this.conversations[0]; !this.currentConversationId &&
if (latestConversation && latestConversation.id) { cache.list.length > 0 &&
await this.loadConversation(latestConversation.id); !this.isExplicitNewConversationRoute()
) {
// 只有在初始化完成后,才自动加载第一个对话
// 避免与 bootstrapRoute 冲突
if (this.initialRouteResolved) {
const latestConversation = cache.list[0];
if (latestConversation && latestConversation.id) {
await this.loadConversation(latestConversation.id);
}
} }
} }
} }
debugLog(`已加载 ${cache.list.length} 个对话`);
/* 首页加载成功后后台补载另一类型,保证切换过滤器时零等待 */
const otherType = listType === 'multi_agent' ? 'normal' : 'multi_agent';
if (queryOffset === 0 && !conversationStore.conversationsCache[otherType].loaded) {
this.loadConversationTypeCache(otherType).catch(() => {});
}
} else { } else {
console.error('加载对话列表失败:', data.error); console.error('加载对话列表失败:', data.error);
} }
@ -94,6 +122,32 @@ export const loadMethods = {
} }
} }
}, },
/** 后台补载指定类型的首页列表缓存已加载则跳过refreshToken 快照防 reset/刷新后旧响应污染 */
async loadConversationTypeCache(type: 'normal' | 'multi_agent') {
const { useConversationStore } = await import('../../../stores/conversation');
const conversationStore = useConversationStore();
const cache = conversationStore.conversationsCache[type];
if (!cache || cache.loaded) return;
const tokenAtStart = this.conversationListRefreshToken;
const maParam = type === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0';
try {
const response = await fetch(
`/api/conversations?limit=${this.conversationsLimit}&offset=0${maParam}`
);
const data = await response.json();
if (!data?.success) return;
/* reset/刷新后旧响应丢弃;并发补载去重 */
if (tokenAtStart !== this.conversationListRefreshToken || cache.loaded) return;
const items = data.data?.conversations || [];
cache.list.push(...items);
cache.offset = 0;
cache.hasMore = !!data.data?.has_more;
cache.loaded = true;
} catch (error) {
console.error('补载对话列表缓存异常:', error);
}
},
async loadMoreConversations() { async loadMoreConversations() {
if (this.loadingMoreConversations || !this.hasMoreConversations) return; if (this.loadingMoreConversations || !this.hasMoreConversations) return;
@ -254,70 +308,9 @@ export const loadMethods = {
type: 'error' type: 'error'
}); });
} }
},
async loadWorkspaceConversations(workspaceId: string, { reset = false } = {}) {
if (!workspaceId) return;
const { useConversationStore } = await import('../../../stores/conversation');
const conversationStore = useConversationStore();
let group = conversationStore.workspaceGroups.find((g: any) => g.workspaceId === workspaceId);
if (!group) {
group = {
workspaceId,
conversations: [],
loading: true,
hasMore: false,
loadingMore: false,
offset: 0,
limit: 20,
expanded: true
};
conversationStore.workspaceGroups.push(group);
}
if (reset) {
group.conversations = [];
group.offset = 0;
group.hasMore = false;
}
if (group.loading || group.loadingMore) return;
group.loading = true;
try {
const { useConversationStore: useConvStore } = await import('../../../stores/conversation');
const maParam = useConvStore().sidebarConversationType === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0';
const response = await fetch(`/api/conversations?workspace_id=${encodeURIComponent(workspaceId)}&limit=${group.limit}&offset=${group.offset}${maParam}`);
const data = await response.json();
if (data.success) {
const items = (data.data?.conversations || []).map((conv: any) => ({
id: conv.id,
title: conv.title,
updated_at: conv.updated_at,
total_messages: conv.total_messages,
total_tools: conv.total_tools
}));
if (group.offset === 0) {
group.conversations = items;
} else {
group.conversations.push(...items);
}
group.hasMore = !!data.data?.has_more;
} else {
console.error('加载工作区对话失败:', data.error);
}
} catch (error) {
console.error('加载工作区对话异常:', error);
} finally {
group.loading = false;
}
},
async loadMoreWorkspaceConversations(workspaceId: string) {
const { useConversationStore } = await import('../../../stores/conversation');
const conversationStore = useConversationStore();
const group = conversationStore.workspaceGroups.find((g: any) => g.workspaceId === workspaceId);
if (!group || group.loadingMore || !group.hasMore) return;
group.loadingMore = true;
group.offset += group.limit;
await this.loadWorkspaceConversations(workspaceId);
group.loadingMore = false;
} }
}; };
/* store stores/conversation.ts loadWorkspaceConversations/
loadMoreWorkspaceConversations/loadWorkspaceConversationTypeCache
*/

View File

@ -302,7 +302,12 @@ export const sendMethods = {
total_messages: 0, total_messages: 0,
total_tools: 0 total_tools: 0
}; };
this.conversations = [newPlaceholder, ...this.conversations.filter((conv) => conv && conv.id !== targetConversationId)]; this.conversations.splice(
0,
this.conversations.length,
newPlaceholder,
...this.conversations.filter((conv) => conv && conv.id !== targetConversationId)
);
// 分组视图下同步到当前工作区 // 分组视图下同步到当前工作区
try { try {
@ -315,7 +320,12 @@ export const sendMethods = {
(g: any) => g.workspaceId === currentWorkspaceId (g: any) => g.workspaceId === currentWorkspaceId
); );
if (group) { if (group) {
group.conversations = [newPlaceholder, ...group.conversations.filter((conv: any) => conv.id !== targetConversationId)]; group.conversations.splice(
0,
group.conversations.length,
newPlaceholder,
...group.conversations.filter((conv: any) => conv.id !== targetConversationId)
);
group.expanded = true; group.expanded = true;
group.visibleOffset = 0; group.visibleOffset = 0;
group.visibleLimit = 5; group.visibleLimit = 5;

View File

@ -38,19 +38,15 @@ export const titleMethods = {
} }
if (Array.isArray(this.conversations)) { if (Array.isArray(this.conversations)) {
let listChanged = false; /* 原地替换保持数组引用conversations 与双类型缓存中当前类型列表同一引用 */
const nextConversations = this.conversations.map((conv: any) => { const convIndex = this.conversations.findIndex(
if (!conv || conv.id !== normalizedConversationId || conv.title === normalizedTitle) { (conv: any) => conv && conv.id === normalizedConversationId && conv.title !== normalizedTitle
return conv; );
} if (convIndex >= 0) {
listChanged = true; this.conversations.splice(convIndex, 1, {
return { ...this.conversations[convIndex],
...conv,
title: normalizedTitle title: normalizedTitle
}; });
});
if (listChanged) {
this.conversations = nextConversations;
changed = true; changed = true;
} }
} }

View File

@ -78,7 +78,9 @@ export const hostWorkspaceMethods = {
const previousSearchActive = this.searchActive; const previousSearchActive = this.searchActive;
this.searchActive = false; this.searchActive = false;
this.searchResults = []; this.searchResults = [];
this.conversations = []; /* 主列表是当前工作区作用域切换前使双类型缓存整体失效conversations 重指空缓存) */
const { useConversationStore } = await import('../../../stores/conversation');
useConversationStore().resetConversationsTypeCache();
this.conversationsOffset = 0; this.conversationsOffset = 0;
this.hasMoreConversations = false; this.hasMoreConversations = false;
this.conversationsLoading = true; this.conversationsLoading = true;
@ -153,7 +155,12 @@ export const hostWorkspaceMethods = {
this.refreshProjectGitSummary?.(); this.refreshProjectGitSummary?.();
this.fetchTerminalCount(); this.fetchTerminalCount();
} catch (error) { } catch (error) {
this.conversations = previousConversationList; /* 切换失败仍在旧工作区:恢复旧列表并同步回当前类型缓存,保持引用一致 */
const conversationStore = useConversationStore();
const restoredCache = conversationStore.conversationsCache[conversationStore.sidebarConversationType];
restoredCache.list = previousConversationList;
restoredCache.loaded = true;
this.conversations = restoredCache.list;
this.searchActive = previousSearchActive; this.searchActive = previousSearchActive;
this.conversationsLoading = false; this.conversationsLoading = false;
const message = error instanceof Error ? error.message : String(error || '切换失败'); const message = error instanceof Error ? error.message : String(error || '切换失败');
@ -322,7 +329,9 @@ export const hostWorkspaceMethods = {
this.currentConversationTitle = '新对话'; this.currentConversationTitle = '新对话';
this.searchActive = false; this.searchActive = false;
this.searchResults = []; this.searchResults = [];
this.conversations = []; /* 当前工作区已删除:双类型缓存整体失效后重新加载 */
const { useConversationStore } = await import('../../../stores/conversation');
useConversationStore().resetConversationsTypeCache();
this.conversationsOffset = 0; this.conversationsOffset = 0;
this.hasMoreConversations = false; this.hasMoreConversations = false;
this.conversationsLoading = true; this.conversationsLoading = true;

View File

@ -221,10 +221,12 @@ export const versioningMethods = {
await this.loadConversationsList(); await this.loadConversationsList();
// copy 模式下给侧边栏一个即时占位,随后列表刷新会补齐真实数据 // copy 模式下给侧边栏一个即时占位,随后列表刷新会补齐真实数据
if (restoreMode === 'copy' && !this.conversations.some((c) => c && c.id === targetConversationId)) { if (restoreMode === 'copy' && !this.conversations.some((c) => c && c.id === targetConversationId)) {
this.conversations = [ this.conversations.splice(
0,
this.conversations.length,
{ id: targetConversationId, title: '版本回溯对话', updated_at: new Date().toISOString(), total_messages: 0, total_tools: 0 }, { id: targetConversationId, title: '版本回溯对话', updated_at: new Date().toISOString(), total_messages: 0, total_tools: 0 },
...this.conversations.filter((c) => c && c.id !== targetConversationId) ...this.conversations.filter((c) => c && c.id !== targetConversationId)
]; );
} }
this.uiPushToast({ this.uiPushToast({
title: '版本管理', title: '版本管理',

View File

@ -768,6 +768,9 @@ const slideTransitionName = computed(() =>
/** 列表容器:切换类型时重置滚动位置,保证推挤动画从顶部开始 */ /** 列表容器:切换类型时重置滚动位置,保证推挤动画从顶部开始 */
const conversationListEl = ref<HTMLElement | null>(null); const conversationListEl = ref<HTMLElement | null>(null);
/* store setSidebarConversationType
同步交换 conversations 引用与分页状态无请求无加载态pane key 与数据同 tick 变化
新面板初始挂载即新数据transition-group 初始渲染不播动画只保留整板左右平移 */
const setSidebarType = (type: 'normal' | 'multi_agent') => { const setSidebarType = (type: 'normal' | 'multi_agent') => {
if (conversationStore.sidebarConversationType === type) return; if (conversationStore.sidebarConversationType === type) return;
if (conversationListEl.value) conversationListEl.value.scrollTop = 0; if (conversationListEl.value) conversationListEl.value.scrollTop = 0;

View File

@ -30,6 +30,7 @@ export interface ConversationSummary {
export interface WorkspaceConversationGroup { export interface WorkspaceConversationGroup {
workspaceId: string; workspaceId: string;
/** 当前过滤器类型对应的列表:与 conversationsByType[sidebarConversationType] 同一数组引用 */
conversations: ConversationSummary[]; conversations: ConversationSummary[];
loading: boolean; loading: boolean;
hasMore: boolean; hasMore: boolean;
@ -40,8 +41,27 @@ export interface WorkspaceConversationGroup {
bufferLimit: number; bufferLimit: number;
fetchLimit: number; fetchLimit: number;
expanded: boolean; expanded: boolean;
/** 双类型常驻缓存:切换过滤器时 conversations 只换引用,不重请求 */
conversationsByType: { normal: ConversationSummary[]; multi_agent: ConversationSummary[] };
/** 各类型分页状态offset/hasMore与是否已加载过切换时与扁平字段交换 */
pagingByType: {
normal: { offset: number; hasMore: boolean; loaded: boolean };
multi_agent: { offset: number; hasMore: boolean; loaded: boolean };
};
} }
/** 新建空分组时的双类型缓存初始化 */
export const createEmptyGroupTypeCache = () => ({
conversationsByType: { normal: [], multi_agent: [] } as {
normal: ConversationSummary[];
multi_agent: ConversationSummary[];
},
pagingByType: {
normal: { offset: 0, hasMore: false, loaded: false },
multi_agent: { offset: 0, hasMore: false, loaded: false }
}
});
/** 跨工作区搜索结果分组(后端 /api/conversations/search?all_workspaces=1 返回) */ /** 跨工作区搜索结果分组(后端 /api/conversations/search?all_workspaces=1 返回) */
export interface WorkspaceSearchGroup { export interface WorkspaceSearchGroup {
workspace_id: string; workspace_id: string;
@ -78,6 +98,14 @@ interface ConversationState {
multiAgentMode: boolean; multiAgentMode: boolean;
/** 侧边栏对话类型过滤器:'normal' 普通对话 | 'multi_agent' 多智能体对话localStorage 持久化) */ /** 侧边栏对话类型过滤器:'normal' 普通对话 | 'multi_agent' 多智能体对话localStorage 持久化) */
sidebarConversationType: 'normal' | 'multi_agent'; sidebarConversationType: 'normal' | 'multi_agent';
/** conversations conversationsCache[sidebarConversationType].list
push/splice/unshift conversations
loaded */
conversationsCache: {
normal: { list: ConversationSummary[]; offset: number; hasMore: boolean; loaded: boolean };
multi_agent: { list: ConversationSummary[]; offset: number; hasMore: boolean; loaded: boolean };
};
} }
export const useConversationStore = defineStore('conversation', { export const useConversationStore = defineStore('conversation', {
@ -106,22 +134,50 @@ export const useConversationStore = defineStore('conversation', {
acknowledgedCompletedTaskIds: [], acknowledgedCompletedTaskIds: [],
workspaceGroups: [], workspaceGroups: [],
multiAgentMode: false, multiAgentMode: false,
sidebarConversationType: loadSidebarConversationType() sidebarConversationType: loadSidebarConversationType(),
conversationsCache: {
normal: { list: [], offset: 0, hasMore: false, loaded: false },
multi_agent: { list: [], offset: 0, hasMore: false, loaded: false }
}
}), }),
actions: { actions: {
/** 当前对话 id 同步(由 app watcher 在 this.currentConversationId 变化时写入) */ /** 当前对话 id 同步(由 app watcher 在 this.currentConversationId 变化时写入) */
setCurrentConversationId(id: string | null) { setCurrentConversationId(id: string | null) {
this.currentConversationId = id; this.currentConversationId = id;
}, },
/** 侧边栏对话类型过滤器切换(左右切换控件写入,驱动列表请求的服务端过滤) */ /**
*
* conversations
* pane
*/
setSidebarConversationType(type: 'normal' | 'multi_agent') { setSidebarConversationType(type: 'normal' | 'multi_agent') {
const normalized = type === 'multi_agent' ? 'multi_agent' : 'normal'; const normalized = type === 'multi_agent' ? 'multi_agent' : 'normal';
if (this.sidebarConversationType === normalized) return; if (this.sidebarConversationType === normalized) return;
const oldType = this.sidebarConversationType;
const curCache = this.conversationsCache[oldType];
curCache.offset = this.conversationsOffset;
curCache.hasMore = this.hasMoreConversations;
this.sidebarConversationType = normalized; this.sidebarConversationType = normalized;
persistSidebarConversationType(normalized); persistSidebarConversationType(normalized);
const nextCache = this.conversationsCache[normalized];
this.conversations = nextCache.list;
this.conversationsOffset = nextCache.offset;
this.hasMoreConversations = nextCache.hasMore;
this.loadingMoreConversations = false;
for (const group of this.workspaceGroups) {
if (!group || !group.conversationsByType) continue;
const curPaging = group.pagingByType[oldType];
curPaging.offset = group.offset;
curPaging.hasMore = group.hasMore;
group.conversations = group.conversationsByType[normalized];
group.offset = group.pagingByType[normalized].offset;
group.hasMore = group.pagingByType[normalized].hasMore;
group.loadingMore = false;
}
}, },
resetConversations() { resetConversations() {
this.conversations = []; /* 重置双类型缓存并对齐 conversations 引用(核心不变量) */
this.resetConversationsCacheOnly();
this.searchResults = []; this.searchResults = [];
this.searchGroups = []; this.searchGroups = [];
this.conversationInsertAnimations = {}; this.conversationInsertAnimations = {};
@ -138,6 +194,19 @@ export const useConversationStore = defineStore('conversation', {
this.runningWorkspaceTasks = []; this.runningWorkspaceTasks = [];
this.acknowledgedCompletedTaskIds = []; this.acknowledgedCompletedTaskIds = [];
}, },
/** /使
conversations */
resetConversationsTypeCache() {
this.resetConversationsCacheOnly();
},
/** 内部共用:仅重置双类型缓存并对齐 conversations 引用 */
resetConversationsCacheOnly() {
this.conversationsCache = {
normal: { list: [], offset: 0, hasMore: false, loaded: false },
multi_agent: { list: [], offset: 0, hasMore: false, loaded: false }
};
this.conversations = this.conversationsCache[this.sidebarConversationType].list;
},
cancelSearchTimer() { cancelSearchTimer() {
if (this.searchTimer) { if (this.searchTimer) {
clearTimeout(this.searchTimer); clearTimeout(this.searchTimer);
@ -156,7 +225,8 @@ export const useConversationStore = defineStore('conversation', {
setWorkspaceGroupConversations(workspaceId: string, conversations: ConversationSummary[]) { setWorkspaceGroupConversations(workspaceId: string, conversations: ConversationSummary[]) {
const group = this.workspaceGroups.find((g) => g.workspaceId === workspaceId); const group = this.workspaceGroups.find((g) => g.workspaceId === workspaceId);
if (group) { if (group) {
group.conversations = conversations; /* 原地替换保持数组引用group.conversations 与 conversationsByType[当前类型] 是同一引用 */
group.conversations.splice(0, group.conversations.length, ...conversations);
} }
}, },
appendWorkspaceGroupConversations(workspaceId: string, conversations: ConversationSummary[]) { appendWorkspaceGroupConversations(workspaceId: string, conversations: ConversationSummary[]) {
@ -199,9 +269,11 @@ export const useConversationStore = defineStore('conversation', {
if (!workspaceId) return; if (!workspaceId) return;
const exists = this.workspaceGroups.some((g) => g.workspaceId === workspaceId); const exists = this.workspaceGroups.some((g) => g.workspaceId === workspaceId);
if (!exists) { if (!exists) {
const typeCache = createEmptyGroupTypeCache();
this.workspaceGroups.push({ this.workspaceGroups.push({
workspaceId, workspaceId,
conversations: [], conversations: typeCache.conversationsByType[this.sidebarConversationType],
...typeCache,
loading: false, loading: false,
hasMore: false, hasMore: false,
loadingMore: false, loadingMore: false,
@ -225,18 +297,27 @@ export const useConversationStore = defineStore('conversation', {
index = this.workspaceGroups.length - 1; index = this.workspaceGroups.length - 1;
} }
const group = this.workspaceGroups[index]; const group = this.workspaceGroups[index];
/* 锁定发起时类型:响应期间用户可能已切换过滤器,数据始终写入对应类型缓存 */
const listType = this.sidebarConversationType;
const listCache = group.conversationsByType[listType];
const paging = group.pagingByType[listType];
if (reset) { if (reset) {
group.conversations = []; listCache.length = 0;
group.offset = 0; paging.offset = 0;
paging.hasMore = false;
paging.loaded = false;
group.visibleOffset = 0; group.visibleOffset = 0;
group.hasMore = false; if (listType === this.sidebarConversationType) {
group.offset = 0;
group.hasMore = false;
}
} }
if (group.loading || group.loadingMore) return; if (group.loading || group.loadingMore) return;
const fetchOffset = refresh ? 0 : group.offset; const fetchOffset = refresh ? 0 : paging.offset;
group.loading = true; group.loading = true;
try { try {
// 列表过滤由侧边栏类型过滤器(普通/多智能体)决定 // 列表过滤由请求发起时的侧边栏类型过滤器(普通/多智能体)决定
const maParam = this.sidebarConversationType === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0'; const maParam = listType === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0';
const response = await fetch( const response = await fetch(
`/api/conversations?workspace_id=${encodeURIComponent(workspaceId)}&limit=${group.fetchLimit}&offset=${fetchOffset}${maParam}` `/api/conversations?workspace_id=${encodeURIComponent(workspaceId)}&limit=${group.fetchLimit}&offset=${fetchOffset}${maParam}`
); );
@ -249,17 +330,29 @@ export const useConversationStore = defineStore('conversation', {
total_messages: conv.total_messages, total_messages: conv.total_messages,
total_tools: conv.total_tools total_tools: conv.total_tools
})); }));
/* 原地写入缓存数组,保持 group.conversations 引用一致 */
if (refresh) { if (refresh) {
const tail = group.conversations.slice(items.length); const tail = listCache.slice(items.length);
group.conversations = [...items, ...tail]; listCache.splice(0, listCache.length, ...items, ...tail);
} else if (fetchOffset === 0) { } else if (fetchOffset === 0) {
group.conversations = items; listCache.splice(0, listCache.length, ...items);
} else { } else {
group.conversations.push(...items); listCache.push(...items);
} }
group.hasMore = !!data.data?.has_more; paging.hasMore = !!data.data?.has_more;
paging.loaded = true;
if (!refresh) { if (!refresh) {
group.offset = fetchOffset + items.length; paging.offset = fetchOffset + items.length;
}
/* 仍是当前显示类型时同步扁平字段group.conversations 引用已是该缓存) */
if (listType === this.sidebarConversationType) {
group.hasMore = paging.hasMore;
group.offset = paging.offset;
}
/* 首页加载成功后后台补载另一类型,保证切换过滤器时零等待 */
const otherType = listType === 'multi_agent' ? 'normal' : 'multi_agent';
if (fetchOffset === 0 && !group.pagingByType[otherType].loaded) {
this.loadWorkspaceConversationTypeCache(workspaceId, otherType).catch(() => {});
} }
} else { } else {
console.error('加载工作区对话失败:', data.error); console.error('加载工作区对话失败:', data.error);
@ -270,6 +363,36 @@ export const useConversationStore = defineStore('conversation', {
group.loading = false; group.loading = false;
} }
}, },
/** 后台补载某工作区指定类型的首页缓存:已加载则跳过,不干扰当前显示 */
async loadWorkspaceConversationTypeCache(
workspaceId: string,
type: 'normal' | 'multi_agent'
) {
const group = this.workspaceGroups.find((g) => g.workspaceId === workspaceId);
if (!group || group.pagingByType[type].loaded) return;
const maParam = type === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0';
try {
const response = await fetch(
`/api/conversations?workspace_id=${encodeURIComponent(workspaceId)}&limit=${group.fetchLimit}&offset=0${maParam}`
);
const data = await response.json();
/* 响应时重新校验:可能已被 reset/补载并发填充 */
if (!data?.success || group.pagingByType[type].loaded) return;
const items = (data.data?.conversations || []).map((conv: any) => ({
id: conv.id,
title: conv.title,
updated_at: conv.updated_at,
total_messages: conv.total_messages,
total_tools: conv.total_tools
}));
group.conversationsByType[type].push(...items);
group.pagingByType[type].offset = items.length;
group.pagingByType[type].hasMore = !!data.data?.has_more;
group.pagingByType[type].loaded = true;
} catch (error) {
console.error('补载工作区对话缓存异常:', error);
}
},
async loadMoreWorkspaceConversations(workspaceId: string) { async loadMoreWorkspaceConversations(workspaceId: string) {
const index = this.workspaceGroups.findIndex((g) => g.workspaceId === workspaceId); const index = this.workspaceGroups.findIndex((g) => g.workspaceId === workspaceId);
if (index === -1) return; if (index === -1) return;
@ -281,9 +404,10 @@ export const useConversationStore = defineStore('conversation', {
group.loadingMore = true; group.loadingMore = true;
// 先尝试从后端再加载 20 条作为新的缓冲 // 先尝试从后端再加载 20 条作为新的缓冲
if (group.hasMore) { if (group.hasMore) {
const listType = this.sidebarConversationType;
try { try {
const fetchOffset = group.conversations.length; const fetchOffset = group.conversations.length;
const maParam = this.sidebarConversationType === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0'; const maParam = listType === 'multi_agent' ? '&multi_agent_mode=1' : '&multi_agent_mode=0';
const response = await fetch( const response = await fetch(
`/api/conversations?workspace_id=${encodeURIComponent(workspaceId)}&limit=${group.bufferLimit}&offset=${fetchOffset}${maParam}` `/api/conversations?workspace_id=${encodeURIComponent(workspaceId)}&limit=${group.bufferLimit}&offset=${fetchOffset}${maParam}`
); );
@ -298,6 +422,12 @@ export const useConversationStore = defineStore('conversation', {
})); }));
group.conversations.push(...items); group.conversations.push(...items);
group.hasMore = !!data.data?.has_more; group.hasMore = !!data.data?.has_more;
/* 同步该类型分页缓存group.conversations 与该缓存同一引用,原地 push 已同步) */
const paging = group.pagingByType?.[listType];
if (paging) {
paging.offset = group.conversations.length;
paging.hasMore = group.hasMore;
}
} }
} catch (error) { } catch (error) {
console.error('加载更多工作区对话异常:', error); console.error('加载更多工作区对话异常:', error);

View File

@ -1164,6 +1164,14 @@ body[data-theme='dark'] .conversation-sidebar .load-more-btn:hover:not(:disabled
font-weight: 600; font-weight: 600;
} }
/* 深色模式surface-soft 是近黑 #0f0f0f比轨道#141414更暗
选中态变成更黑的坑层级方向反了且违反深色禁近黑规则
改为白色微量 tint 提亮一档#3a3a3a与深色可见灰阶先例一致 */
:root[data-theme='dark'] .conversation-type-option.active,
body[data-theme='dark'] .conversation-type-option.active {
background: color-mix(in srgb, var(--text-primary) 16%, transparent);
}
/* 列表区域整体左右滑动切换动画推挤式新面板从侧边滑入把旧面板顶出去 /* 列表区域整体左右滑动切换动画推挤式新面板从侧边滑入把旧面板顶出去
out-in 空白期两面板同速同幅滑动边缘始终相接 */ out-in 空白期两面板同速同幅滑动边缘始终相接 */
.conversation-list-pane { .conversation-list-pane {