Files
PlantAI-dev/apps/web/vue3copy0416/conversation-sidebar.js
2026-08-03 18:42:47 +08:00

374 lines
15 KiB
JavaScript
Executable File

/**
* Conversation Sidebar Component
* 类似ChatGPT的会话侧边栏组件
*/
const ConversationSidebar = {
template: `
<div class="conversation-sidebar" :class="{ collapsed: isCollapsed }">
<!-- 头部:New Chat按钮 -->
<div class="sidebar-header">
<button class="new-chat-btn" @click="createNewConversation">
<el-icon><plus /></el-icon>
<span v-if="!isCollapsed">New Chat</span>
</button>
<button class="toggle-btn" @click="toggleSidebar">
<el-icon><arrow-left v-if="!isCollapsed" /><arrow-right v-else /></el-icon>
</button>
</div>
<!-- 搜索框 -->
<div class="search-box" v-if="!isCollapsed">
<el-input
v-model="searchQuery"
placeholder="搜索会话..."
prefix-icon="search"
clearable
size="small"
/>
</div>
<!-- 会话列表 -->
<div class="conversation-list" v-if="!isCollapsed">
<!-- 今日会话 -->
<div class="list-section" v-if="todayConversations.length > 0">
<div class="section-title">今天</div>
<div
v-for="conv in todayConversations"
:key="conv.conversation_id"
class="conversation-item"
:class="{ active: currentConversationId === conv.conversation_id }"
@click="selectConversation(conv)"
>
<div class="item-icon">
<el-icon><chat-dot-round /></el-icon>
</div>
<div class="item-content">
<div class="item-title">{{ conv.title }}</div>
<div class="item-meta">
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
<span class="message-count">{{ conv.message_count }}条消息</span>
</div>
<div class="item-preview">{{ conv.last_message }}</div>
</div>
<div class="item-actions" @click.stop>
<el-dropdown trigger="click">
<el-icon><more /></el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="renameConversation(conv)">
<el-icon><edit /></el-icon>重命名
</el-dropdown-item>
<el-dropdown-item @click="archiveConversation(conv)">
<el-icon><folder /></el-icon>归档
</el-dropdown-item>
<el-dropdown-item divided @click="deleteConversation(conv)" type="danger">
<el-icon><delete /></el-icon>删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<!-- 昨日会话 -->
<div class="list-section" v-if="yesterdayConversations.length > 0">
<div class="section-title">昨天</div>
<div
v-for="conv in yesterdayConversations"
:key="conv.conversation_id"
class="conversation-item"
:class="{ active: currentConversationId === conv.conversation_id }"
@click="selectConversation(conv)"
>
<div class="item-icon">
<el-icon><chat-dot-round /></el-icon>
</div>
<div class="item-content">
<div class="item-title">{{ conv.title }}</div>
<div class="item-meta">
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
<span class="message-count">{{ conv.message_count }}条</span>
</div>
</div>
</div>
</div>
<!-- 更早会话 -->
<div class="list-section" v-if="olderConversations.length > 0">
<div class="section-title">过去7天</div>
<div
v-for="conv in olderConversations"
:key="conv.conversation_id"
class="conversation-item"
:class="{ active: currentConversationId === conv.conversation_id }"
@click="selectConversation(conv)"
>
<div class="item-icon">
<el-icon><chat-dot-round /></el-icon>
</div>
<div class="item-content">
<div class="item-title">{{ conv.title }}</div>
<div class="item-meta">
<span class="gene-family" v-if="conv.gene_family">{{ conv.gene_family }}</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="filteredConversations.length === 0" class="empty-state">
<el-icon><chat-line-round /></el-icon>
<p>暂无会话</p>
<p class="hint">点击上方"New Chat"开始新的基因家族分析</p>
</div>
</div>
<!-- 底部:用户信息 -->
<div class="sidebar-footer" v-if="!isCollapsed">
<div class="user-info">
<el-avatar :size="32" :icon="UserFilled" />
<span class="username">{{ userId }}</span>
</div>
</div>
</div>
`,
props: {
userId: {
type: String,
default: 'xia-lab'
},
apiBaseUrl: {
type: String,
default: 'http://localhost:8000/api'
}
},
data() {
return {
isCollapsed: false,
searchQuery: '',
conversations: [],
currentConversationId: null,
loading: false,
// 图标
Plus: ElementPlusIconsVue.Plus,
ArrowLeft: ElementPlusIconsVue.ArrowLeft,
ArrowRight: ElementPlusIconsVue.ArrowRight,
ChatDotRound: ElementPlusIconsVue.ChatDotRound,
ChatLineRound: ElementPlusIconsVue.ChatLineRound,
More: ElementPlusIconsVue.More,
Edit: ElementPlusIconsVue.Edit,
Folder: ElementPlusIconsVue.Folder,
Delete: ElementPlusIconsVue.Delete,
UserFilled: ElementPlusIconsVue.UserFilled,
}
},
computed: {
filteredConversations() {
if (!this.searchQuery) return this.conversations;
const query = this.searchQuery.toLowerCase();
return this.conversations.filter(conv =>
conv.title.toLowerCase().includes(query) ||
(conv.gene_family && conv.gene_family.toLowerCase().includes(query)) ||
conv.last_message.toLowerCase().includes(query)
);
},
todayConversations() {
const today = new Date().toDateString();
return this.filteredConversations.filter(conv => {
const convDate = new Date(conv.updated_at).toDateString();
return convDate === today;
});
},
yesterdayConversations() {
const yesterday = new Date(Date.now() - 86400000).toDateString();
return this.filteredConversations.filter(conv => {
const convDate = new Date(conv.updated_at).toDateString();
return convDate === yesterday;
});
},
olderConversations() {
const yesterday = new Date(Date.now() - 86400000).toDateString();
return this.filteredConversations.filter(conv => {
const convDate = new Date(conv.updated_at).toDateString();
return convDate < yesterday;
});
}
},
mounted() {
this.loadConversations();
// 定时刷新
this.refreshInterval = setInterval(() => {
this.loadConversations();
}, 30000); // 30秒刷新一次
},
beforeUnmount() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
},
methods: {
async loadConversations() {
try {
this.loading = true;
const response = await fetch(`${this.apiBaseUrl}/conversations/list/${this.userId}`);
if (response.ok) {
const data = await response.json();
this.conversations = data.conversations || [];
}
} catch (error) {
console.error('Failed to load conversations:', error);
// 使用模拟数据
this.conversations = this.getMockConversations();
} finally {
this.loading = false;
}
},
getMockConversations() {
// 模拟数据,用于测试
return [
{
conversation_id: 'conv_001',
title: 'Ft.SPL基因家族分析',
gene_family: 'SPL',
species: 'Ft',
message_count: 12,
last_message: '系统发育树已生成完成',
updated_at: new Date().toISOString()
},
{
conversation_id: 'conv_002',
title: 'Os.TCP进化分析',
gene_family: 'TCP',
species: 'Os',
message_count: 5,
last_message: '正在构建进化树...',
updated_at: new Date(Date.now() - 3600000).toISOString()
},
{
conversation_id: 'conv_003',
title: 'HMMER参数配置咨询',
message_count: 8,
last_message: 'E-value建议设置为1e-5',
updated_at: new Date(Date.now() - 86400000).toISOString()
}
];
},
toggleSidebar() {
this.isCollapsed = !this.isCollapsed;
this.$emit('toggle', this.isCollapsed);
},
async createNewConversation() {
try {
const response = await fetch(`${this.apiBaseUrl}/conversations/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: this.userId,
title: '新会话 ' + new Date().toLocaleTimeString()
})
});
if (response.ok) {
const data = await response.json();
this.currentConversationId = data.conversation_id;
this.$emit('new-conversation', data);
await this.loadConversations();
}
} catch (error) {
console.error('Failed to create conversation:', error);
// 创建本地会话
const newConv = {
conversation_id: 'conv_' + Date.now(),
title: '新会话',
message_count: 0,
last_message: '',
updated_at: new Date().toISOString()
};
this.conversations.unshift(newConv);
this.currentConversationId = newConv.conversation_id;
this.$emit('new-conversation', newConv);
}
},
selectConversation(conv) {
this.currentConversationId = conv.conversation_id;
this.$emit('select-conversation', conv);
},
async renameConversation(conv) {
const newTitle = prompt('请输入新标题:', conv.title);
if (newTitle && newTitle !== conv.title) {
try {
await fetch(`${this.apiBaseUrl}/conversations/update-title`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversation_id: conv.conversation_id,
title: newTitle
})
});
conv.title = newTitle;
} catch (error) {
console.error('Failed to rename:', error);
conv.title = newTitle; // 本地更新
}
}
},
async archiveConversation(conv) {
try {
await fetch(`${this.apiBaseUrl}/conversations/archive`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversation_id: conv.conversation_id
})
});
await this.loadConversations();
} catch (error) {
console.error('Failed to archive:', error);
}
},
async deleteConversation(conv) {
if (confirm(`确定要删除会话 "${conv.title}" 吗?`)) {
try {
await fetch(`${this.apiBaseUrl}/conversations/delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversation_id: conv.conversation_id
})
});
await this.loadConversations();
if (this.currentConversationId === conv.conversation_id) {
this.currentConversationId = null;
this.$emit('select-conversation', null);
}
} catch (error) {
console.error('Failed to delete:', error);
}
}
}
}
};
// 导出组件
if (typeof module !== 'undefined' && module.exports) {
module.exports = { ConversationSidebar };
}