| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- <template>
- <div class="right-panel right-panel-tabs">
- <div class="panel-tab-bar">
- <div class="panel-tab-toggle-wrapper" ref="toggleRef">
- <div class="panel-tab-toggle" :class="{ active: showTabList }" @click="toggleTabList" title="全部标签页">
- <div class="panel-tab-toggle-icon"></div>
- </div>
- <div v-if="showTabList" class="tab-list-popup" @click.stop @pointerdown.stop>
- <div class="tab-list-search">
- <div class="tab-list-search-icon"></div>
- <input ref="searchInputRef" v-model="searchText" class="tab-list-search-input" placeholder="搜索标签页" />
- </div>
- <div class="tab-list">
- <div
- v-for="tab in filteredTabs"
- :key="tab.id"
- class="tab-list-item"
- :class="{ active: tab.id === activeTabId }"
- @click="handleSelect(tab.id)"
- >
- <div
- class="tab-list-item-icon"
- :class="`tab-icon-${tab.type}`"
- :style="tab.type === 'file' ? { backgroundImage: getFileIconVar(tab.title) } : null"
- ></div>
- <span class="tab-list-item-title">{{ tab.title }}</span>
- </div>
- <EmptyState v-if="filteredTabs.length === 0" size="sm" description="暂无匹配标签页" />
- </div>
- </div>
- </div>
- <div class="panel-tab-scroll" ref="scrollRef" :class="{ sliding: isSliding }">
- <div class="panel-tab-track" :style="isSliding ? { transform: `translateX(${-trackShiftPx}px)` } : null">
- <div
- v-for="tab in tabs"
- :key="tab.id"
- :class="['panel-tab', `panel-tab-${tab.type}`, { active: tab.id === activeTabId }]"
- @click="handleSwitch(tab.id)"
- >
- <div class="panel-tab-icon" :style="tab.type === 'file' ? { backgroundImage: getFileIconVar(tab.title) } : null"></div>
- <span class="panel-tab-title">{{ tab.title }}</span>
- <div class="panel-tab-close" @click.stop="handleClose(tab.id)" title="关闭"></div>
- </div>
- </div>
- </div>
- </div>
- <div class="panel-tab-content">
- <FilePreviewPanel
- v-if="activeTab?.type === 'file'"
- :file="activeTab.file"
- :editable="activeTab.editable"
- :save-filename="activeTab.file.name || ''"
- hide-header
- @close="closeActive"
- @save-success="handleSaveSuccess"
- @open-markdown-tab="emit('open-markdown-tab', $event)"
- />
- <AgentDetailPanel
- v-else-if="activeTab?.type === 'agent'"
- :messageIndex="activeTab.messageIndex"
- :agentName="activeTab.agent"
- :agentId="activeTab.agentId"
- :messages="messages"
- hide-header
- @close="closeActive"
- />
- <ScheduleDetailPanel
- v-else-if="activeTab?.type === 'schedule'"
- :task="activeTab.task"
- hide-header
- @close="closeActive"
- @deleted="emit('schedule-deleted')"
- @updated="emit('schedule-updated')"
- @open-session="handleOpenSession"
- />
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import EmptyState from '/@/views/ventAI/manageAssistent/components/common/EmptyState.vue';
- import { computed, nextTick, ref } from 'vue';
- import { onClickOutside, useResizeObserver } from '@vueuse/core';
- import FilePreviewPanel from '/@/views/ventAI/manageAssistent/components/resource/FilePreviewPanel.vue';
- import AgentDetailPanel from './AgentDetailPanel.vue';
- import ScheduleDetailPanel from './ScheduleDetailPanel.vue';
- import { getFileIconVar } from '/@/views/ventAI/manageAssistent/lib/index';
- import type { Message, RightPanelTab } from '/@/views/ventAI/manageAssistent/types/index';
- const props = defineProps<{
- tabs: RightPanelTab[];
- activeTabId: string | null;
- messages: Message[];
- }>();
- const emit = defineEmits<{
- (e: 'switch-tab', id: string): void;
- (e: 'close-tab', id: string): void;
- (e: 'save-success', content: string): void;
- (e: 'schedule-deleted'): void;
- (e: 'schedule-updated'): void;
- (e: 'open-schedule-session', sessionId: string, prompt: string): void;
- (e: 'open-markdown-tab', payload: { title: string; content: string }): void;
- }>();
- const activeTab = computed(() => props.tabs.find((t) => t.id === props.activeTabId) || null);
- // 按容器实际宽度动态计算可容纳的标签数量;激活标签像素中心对齐容器中心,平移量钳制保证首标签贴左、尾标签贴右
- const TAB_GAP = 8;
- const TAB_BASE_WIDTH = 150;
- const scrollRef = ref<HTMLElement>();
- const visibleCount = ref(1);
- const scrollWidth = ref(0);
- useResizeObserver(scrollRef, () => {
- const width = scrollRef.value?.clientWidth ?? 0;
- scrollWidth.value = width;
- visibleCount.value = Math.max(1, Math.floor((width + TAB_GAP) / (TAB_BASE_WIDTH + TAB_GAP)));
- });
- const trackShiftPx = computed(() => {
- const len = props.tabs.length;
- if (len <= visibleCount.value || !scrollWidth.value) return 0;
- const trackWidth = len * (TAB_BASE_WIDTH + TAB_GAP) - TAB_GAP;
- const maxShift = Math.max(0, trackWidth - scrollWidth.value);
- const activeIdx = Math.max(
- 0,
- props.tabs.findIndex((t) => t.id === props.activeTabId)
- );
- const activeCenter = activeIdx * (TAB_BASE_WIDTH + TAB_GAP) + TAB_BASE_WIDTH / 2;
- return Math.min(Math.max(activeCenter - scrollWidth.value / 2, 0), maxShift);
- });
- const isSliding = computed(() => props.tabs.length > visibleCount.value);
- const toggleRef = ref<HTMLElement>();
- const searchInputRef = ref<HTMLInputElement>();
- const showTabList = ref(false);
- const searchText = ref('');
- const filteredTabs = computed(() => {
- const kw = searchText.value.trim().toLowerCase();
- if (!kw) return props.tabs;
- return props.tabs.filter((t) => t.title.toLowerCase().includes(kw));
- });
- const toggleTabList = async () => {
- showTabList.value = !showTabList.value;
- if (showTabList.value) {
- searchText.value = '';
- await nextTick();
- searchInputRef.value?.focus();
- }
- };
- const handleSelect = (id: string) => {
- emit('switch-tab', id);
- showTabList.value = false;
- };
- onClickOutside(toggleRef, () => {
- showTabList.value = false;
- });
- const handleSwitch = (id: string) => emit('switch-tab', id);
- const handleClose = (id: string) => emit('close-tab', id);
- const closeActive = () => {
- if (activeTab.value) emit('close-tab', activeTab.value.id);
- };
- const handleSaveSuccess = (content: string) => emit('save-success', content);
- const handleOpenSession = (sessionId: string, prompt: string) => emit('open-schedule-session', sessionId, prompt);
- </script>
- <style scoped lang="less">
- .right-panel-tabs {
- display: flex;
- flex-direction: column;
- overflow: hidden;
- .panel-tab-bar {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 10px 12px;
- flex-shrink: 0;
- border-bottom: 1px solid rgb(63 80 106 / 25%);
- .panel-tab-toggle-wrapper {
- position: relative;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- .panel-tab-toggle {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- border: 1px solid rgb(63 80 106 / 35%);
- background: rgb(63 80 106 / 12%);
- cursor: pointer;
- transition: all 0.2s;
- border-radius: 4px;
- &:hover,
- &.active {
- background: rgb(10 132 255 / 15%);
- border-color: rgb(10 132 255 / 40%);
- }
- .panel-tab-toggle-icon {
- width: 14px;
- height: 14px;
- background-image: var(--img-chat-tabs-icon);
- background-repeat: no-repeat;
- background-size: 100% 100%;
- background-position: center;
- opacity: 0.8;
- }
- }
- .tab-list-popup {
- position: absolute;
- top: calc(100% + 6px);
- left: 0;
- z-index: 1000;
- display: flex;
- flex-direction: column;
- width: 240px;
- border: 1px solid #1a4a6f;
- background: #0a2a3f;
- max-height: 320px;
- border-radius: 8px;
- box-shadow: 0 4px 20px rgb(0 0 0 / 40%);
- .tab-list-search {
- display: flex;
- align-items: center;
- height: 34px;
- margin: 10px 10px 4px;
- padding: 0 12px;
- border: none;
- background: transparent;
- transition: border-bottom-color 0.2s;
- gap: 8px;
- border-bottom: 1px solid rgb(63 80 106 / 50%);
- flex-shrink: 0;
- &:hover {
- border-bottom-color: rgb(63 80 106 / 70%);
- }
- &:focus-within {
- border-bottom-color: rgb(10 132 255 / 70%);
- }
- .tab-list-search-icon {
- width: 15px;
- height: 15px;
- flex-shrink: 0;
- background-image: var(--img-chat-options-search-icon);
- background-repeat: no-repeat;
- background-size: 100% 100%;
- background-position: center;
- opacity: 0.7;
- }
- .tab-list-search-input {
- flex: 1;
- height: 100%;
- padding: 0;
- border: none;
- background: transparent;
- color: #e0e6ed;
- min-width: 0;
- font-size: 12px;
- outline: none;
- &::placeholder {
- color: #6b7684;
- }
- }
- }
- .tab-list {
- flex: 1;
- overflow-y: auto;
- padding: 6px;
- .tab-list-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 8px 10px;
- cursor: pointer;
- border-radius: 6px;
- transition: background 0.2s;
- &:hover {
- background: rgb(10 132 255 / 15%);
- }
- &.active {
- background: rgb(10 132 255 / 20%);
- }
- .tab-list-item-icon {
- width: 14px;
- height: 14px;
- flex-shrink: 0;
- background-repeat: no-repeat;
- background-size: 100% 100%;
- background-position: center;
- opacity: 0.8;
- }
- .tab-icon-file {
- background-image: var(--img-chat-attach-icon);
- }
- .tab-icon-agent {
- background-image: var(--img-chat-agent-icon);
- }
- .tab-icon-schedule {
- background-image: var(--img-chat-options-schedule-icon);
- }
- .tab-list-item-title {
- flex: 1;
- color: #c9d1d9;
- overflow: hidden;
- min-width: 0;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-size: 12px;
- }
- }
- }
- }
- }
- .panel-tab-scroll {
- flex: 1;
- min-width: 0;
- overflow: hidden;
- .panel-tab-track {
- display: flex;
- align-items: center;
- gap: 8px;
- width: 100%;
- }
- &.sliding .panel-tab-track {
- width: max-content;
- transition: transform 0.25s ease;
- }
- &:not(.sliding) .panel-tab {
- flex: 1 1 150px;
- max-width: 240px;
- }
- }
- .panel-tab {
- display: flex;
- flex: 0 0 150px;
- align-items: center;
- height: 28px;
- padding: 6px 12px;
- border: 1px solid rgb(63 80 106 / 35%);
- background: rgb(63 80 106 / 12%);
- color: #8b949e;
- cursor: pointer;
- transition: all 0.2s;
- gap: 6px;
- min-width: 0;
- border-radius: 4px;
- white-space: nowrap;
- font-size: 12px;
- &:hover {
- background: rgb(10 132 255 / 8%);
- border-color: rgb(10 132 255 / 30%);
- color: #c9d1d9;
- }
- &.active {
- background: rgb(10 132 255 / 18%);
- border-color: rgb(10 132 255 / 50%);
- color: #e0e6ed;
- box-shadow: 0 1px 4px rgb(10 132 255 / 25%);
- }
- .panel-tab-icon {
- width: 14px;
- height: 14px;
- flex-shrink: 0;
- background-repeat: no-repeat;
- background-size: 100% 100%;
- background-position: center;
- opacity: 0.8;
- }
- .panel-tab-title {
- flex: 1;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .panel-tab-close {
- width: 16px;
- height: 16px;
- flex-shrink: 0;
- background-image: var(--img-chat-close-icon-btn);
- background-repeat: no-repeat;
- background-size: 100% 100%;
- background-position: center;
- opacity: 0.6;
- border-radius: 3px;
- transition: all 0.2s;
- &:hover {
- opacity: 1;
- background-color: rgb(255 77 79 / 20%);
- }
- }
- }
- .panel-tab-file .panel-tab-icon {
- background-image: var(--img-chat-attach-icon);
- }
- .panel-tab-agent .panel-tab-icon {
- background-image: var(--img-chat-agent-icon);
- }
- .panel-tab-schedule .panel-tab-icon {
- background-image: var(--img-chat-options-schedule-icon);
- }
- }
- .panel-tab-content {
- display: flex;
- flex: 1;
- flex-direction: column;
- overflow: hidden;
- min-height: 0;
- }
- }
- </style>
|