ConversationList.vue 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
  1. <script setup lang="ts">
  2. import { computed, ref } from 'vue'
  3. import ResourceSidebar from '@/components/app/ResourceSidebar.vue'
  4. import { workspaceConversationItem } from './viewModels'
  5. import type { WorkspaceAgent, WorkspaceConversation } from '@shared/workspace'
  6. const props = withDefaults(defineProps<{ conversations: WorkspaceConversation[]; agents?: WorkspaceAgent[]; selected?: string }>(), { agents: () => [] })
  7. const emit = defineEmits<{ select: [id: string]; pin: [id: string]; archive: [id: string]; delete: [id: string] }>()
  8. const menuId = ref('')
  9. const menuPosition = ref({ left: '8px', top: '8px' })
  10. const menuConversation = computed(() => props.conversations.find(c => c.id === menuId.value))
  11. const rows = computed(() => props.conversations.filter(c => !c.archived).map(c => ({
  12. ...workspaceConversationItem(c, props.agents),
  13. section: '',
  14. })))
  15. function openMenu(id: string, event: MouseEvent) {
  16. menuId.value = id
  17. menuPosition.value = { left: `${Math.max(8, Math.min(event.clientX, window.innerWidth - 170))}px`, top: `${Math.max(8, Math.min(event.clientY, window.innerHeight - 180))}px` }
  18. }
  19. function action(kind: 'pin' | 'archive' | 'delete') {
  20. if (kind === 'pin') emit('pin', menuId.value)
  21. else if (kind === 'archive') emit('archive', menuId.value)
  22. else emit('delete', menuId.value)
  23. menuId.value = ''
  24. }
  25. function viewConversation(){emit('select',menuId.value);menuId.value=''}
  26. </script>
  27. <template>
  28. <div class="conversation-list">
  29. <ResourceSidebar :items="rows" :active-id="selected" :searchable="false" :avatar-size="44" external-search
  30. empty-title="还没有聊天" empty-description="创建机器人,或选择成员新建群聊。"
  31. @select="emit('select', $event)" @more="openMenu" @context-menu="openMenu" />
  32. <Teleport to="body">
  33. <div v-if="menuConversation" class="conversation-menu-dismiss" @pointerdown.self="menuId = ''" @keydown.esc="menuId = ''">
  34. <section class="conversation-actions" :style="menuPosition" role="menu" aria-label="聊天操作">
  35. <button role="menuitem" @click="viewConversation">{{ menuConversation.kind === 'direct' ? '查看机器人' : '查看群聊' }}</button>
  36. <button role="menuitem" @click="action('pin')">{{ menuConversation.pinned ? '取消置顶' : '置顶聊天' }}</button>
  37. <button role="menuitem" @click="action('archive')">{{ menuConversation.archived ? '恢复聊天' : '归档聊天' }}</button>
  38. <button class="danger" role="menuitem" @click="action('delete')">删除聊天</button>
  39. </section>
  40. </div>
  41. </Teleport>
  42. </div>
  43. </template>
  44. <style scoped>
  45. .conversation-list{display:flex;flex:1;flex-direction:column;min-height:0}
  46. .conversation-list :deep(.sidebar-list){padding:0 12px 18px;scrollbar-gutter:auto}
  47. .conversation-list :deep(.sidebar-item){min-height:76px;padding:14px 8px;gap:16px;border-radius:12px}
  48. .conversation-list :deep(.sidebar-item__icon){width:44px;height:44px;flex-basis:44px}
  49. .conversation-list :deep(.sidebar-item__row strong){font-size:15px;font-weight:600;line-height:22px}
  50. .conversation-list :deep(.sidebar-item__row--secondary){font-size:13px;line-height:19px;margin-top:4px}
  51. .conversation-list :deep(.sidebar-item__row small){font-size:10px;color:var(--text-muted)}
  52. .conversation-menu-dismiss{position:fixed;inset:0;z-index:200}.conversation-actions{position:absolute;display:grid;min-width:155px;padding:5px;border:1px solid var(--line);border-radius:10px;background:var(--surface-raised);box-shadow:var(--shadow-float)}.conversation-actions button{padding:9px 12px;border:0;border-radius:7px;background:transparent;color:var(--text-primary);text-align:left;cursor:pointer;font-size:12px}.conversation-actions button:hover{background:var(--surface-hover)}
  53. .conversation-actions button.danger{color:var(--danger)}
  54. </style>