205 lines
7.6 KiB
Vue
205 lines
7.6 KiB
Vue
<template>
|
||
<aside class="conversation-sidebar" :class="{ collapsed }">
|
||
<div class="conversation-header" :class="{ 'collapsed-layout': collapsed }">
|
||
<template v-if="collapsed">
|
||
<div class="collapsed-header-buttons">
|
||
<button
|
||
type="button"
|
||
class="collapsed-control-btn conversation-menu-btn"
|
||
title="展开对话记录"
|
||
@click="$emit('toggle')"
|
||
>
|
||
<span class="sr-only">展开对话记录</span>
|
||
<span class="chat-icon" aria-hidden="true">
|
||
<slot name="collapsed-chat-icon">
|
||
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path
|
||
d="M5 6.5c0-1.38 1.12-2.5 2.5-2.5h13c1.38 0 2.5 1.12 2.5 2.5v8.5c0 1.38-1.12 2.5-2.5 2.5h-5.6l-3.4 3.2.6-3.2H7.5c-1.38 0-2.5-1.12-2.5-2.5V6.5z"
|
||
stroke="currentColor"
|
||
stroke-width="1.7"
|
||
stroke-linejoin="round"
|
||
/>
|
||
<path d="M9 9.5h10" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
|
||
<path d="M9 13h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
|
||
</svg>
|
||
</slot>
|
||
</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="collapsed-control-btn quick-plus-btn"
|
||
title="快捷新建对话"
|
||
@click="$emit('create')"
|
||
>
|
||
<span class="sr-only">新建对话</span>
|
||
<span class="pencil-icon" aria-hidden="true">
|
||
<slot name="collapsed-create-icon">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
width="24"
|
||
height="24"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.8"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<path
|
||
d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"
|
||
fill="none"
|
||
/>
|
||
<path d="m15 5 4 4" fill="none" />
|
||
</svg>
|
||
</slot>
|
||
</span>
|
||
</button>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<button class="new-conversation-btn" @click="$emit('create')">
|
||
<span class="btn-icon">+</span>
|
||
<span class="btn-text">新建对话</span>
|
||
</button>
|
||
<button v-if="showCollapseButton" class="toggle-sidebar-btn" @click="$emit('toggle')">
|
||
<template v-if="collapseButtonVariant === 'toggle'">
|
||
<span v-if="collapsed" class="icon icon-md" :style="iconStyle('menu')" aria-hidden="true"></span>
|
||
<span v-else class="toggle-arrow" aria-hidden="true">←</span>
|
||
</template>
|
||
<template v-else>
|
||
<span class="toggle-close" aria-hidden="true">×</span>
|
||
</template>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
|
||
<template v-if="!collapsed">
|
||
<div class="conversation-search">
|
||
<input
|
||
class="search-input"
|
||
:value="searchQuery"
|
||
placeholder="搜索对话..."
|
||
@input="$emit('search', ($event.target as HTMLInputElement).value)"
|
||
/>
|
||
</div>
|
||
|
||
<div class="conversation-list">
|
||
<div v-if="loading" class="loading-conversations">正在加载...</div>
|
||
<div v-else-if="!conversations.length" class="no-conversations">暂无对话记录</div>
|
||
<div v-else>
|
||
<div
|
||
v-for="conv in conversations"
|
||
:key="conv.id"
|
||
class="conversation-item"
|
||
:class="{ active: conv.id === currentConversationId }"
|
||
@click="$emit('select', conv.id)"
|
||
>
|
||
<div class="conversation-title">{{ conv.title }}</div>
|
||
<div class="conversation-meta">
|
||
<span class="conversation-time">{{ formatTime(conv.updated_at) }}</span>
|
||
<span class="conversation-counts">
|
||
{{ (conv.total_messages || 0) }}条消息
|
||
<span v-if="(conv.total_tools || 0) > 0"> · {{ conv.total_tools }}工具</span>
|
||
</span>
|
||
</div>
|
||
<div class="conversation-actions" @click.stop>
|
||
<button
|
||
type="button"
|
||
class="conversation-action-btn delete-btn"
|
||
title="删除对话"
|
||
@click="$emit('delete', conv.id)"
|
||
>
|
||
×
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="conversation-action-btn copy-btn"
|
||
title="复制对话"
|
||
@click="$emit('duplicate', conv.id)"
|
||
>
|
||
⧉
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="hasMore" class="load-more">
|
||
<button class="load-more-btn" type="button" :disabled="loadingMore" @click="$emit('load-more')">
|
||
{{ loadingMore ? '载入中...' : '加载更多' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="conversation-collapsed-spacer"></div>
|
||
</template>
|
||
|
||
<div class="conversation-personal-entry" :class="{ collapsed, active: personalPageVisible }">
|
||
<button
|
||
type="button"
|
||
class="personal-page-btn"
|
||
:class="{ 'icon-only': collapsed }"
|
||
title="个人页面"
|
||
@click="$emit('personal')"
|
||
>
|
||
<span class="sr-only">个人页面</span>
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||
<path
|
||
fill-rule="evenodd"
|
||
clip-rule="evenodd"
|
||
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||
></path>
|
||
</svg>
|
||
<span class="personal-label" v-if="!collapsed">个人空间</span>
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
defineOptions({ name: 'ConversationSidebar' });
|
||
|
||
import { computed } from 'vue';
|
||
import { storeToRefs } from 'pinia';
|
||
import { useUiStore } from '@/stores/ui';
|
||
import { useConversationStore } from '@/stores/conversation';
|
||
import { usePersonalizationStore } from '@/stores/personalization';
|
||
|
||
const props = defineProps<{
|
||
formatTime?: (input: unknown) => string;
|
||
iconStyle?: (key: string) => Record<string, string>;
|
||
showCollapseButton?: boolean;
|
||
collapseButtonVariant?: 'toggle' | 'close';
|
||
}>();
|
||
|
||
defineEmits<{
|
||
(event: 'toggle'): void;
|
||
(event: 'create'): void;
|
||
(event: 'search', value: string): void;
|
||
(event: 'select', id: string): void;
|
||
(event: 'load-more'): void;
|
||
(event: 'personal'): void;
|
||
(event: 'delete', id: string): void;
|
||
(event: 'duplicate', id: string): void;
|
||
}>();
|
||
|
||
const uiStore = useUiStore();
|
||
const conversationStore = useConversationStore();
|
||
const personalizationStore = usePersonalizationStore();
|
||
|
||
const { sidebarCollapsed: collapsed } = storeToRefs(uiStore);
|
||
const {
|
||
searchQuery,
|
||
conversations,
|
||
conversationsLoading: loading,
|
||
hasMoreConversations: hasMore,
|
||
loadingMoreConversations: loadingMore,
|
||
currentConversationId
|
||
} = storeToRefs(conversationStore);
|
||
const { visible: personalPageVisible } = storeToRefs(personalizationStore);
|
||
|
||
const formatTime = (value: unknown) => (props.formatTime ? props.formatTime(value) : String(value));
|
||
const iconStyle = (key: string) => (props.iconStyle ? props.iconStyle(key) : {});
|
||
const showCollapseButton = computed(() => props.showCollapseButton !== false);
|
||
const collapseButtonVariant = computed(() => props.collapseButtonVariant || 'toggle');
|
||
</script>
|