mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
feat(ui): frosted-glass tooltip and collapsed-rail alignment
This commit is contained in:
parent
2c1e673fba
commit
99718d276e
@ -58,81 +58,87 @@
|
|||||||
<div class="conversation-list">
|
<div class="conversation-list">
|
||||||
<template v-for="group in groupedConversations" :key="group.label">
|
<template v-for="group in groupedConversations" :key="group.label">
|
||||||
<div v-if="!collapsed || isMobile" class="conv-group-title">{{ group.label }}</div>
|
<div v-if="!collapsed || isMobile" class="conv-group-title">{{ group.label }}</div>
|
||||||
<div
|
<McTooltip
|
||||||
v-for="conv in group.items"
|
v-for="conv in group.items"
|
||||||
:key="conv.conversationId"
|
:key="conv.conversationId"
|
||||||
class="conv-item"
|
:content="conv.title"
|
||||||
:class="{
|
placement="right"
|
||||||
active: !selectMode && currentConversationId === conv.conversationId,
|
:disabled="!collapsed || isMobile || !conv.title"
|
||||||
'is-running': conv.streamStatus === 'running',
|
|
||||||
'is-selected': selectMode && selectedConvIds.includes(conv.conversationId),
|
|
||||||
'menu-open': menuConvId === conv.conversationId,
|
|
||||||
}"
|
|
||||||
@click="onConvClick(conv)"
|
|
||||||
>
|
>
|
||||||
<label
|
<div
|
||||||
v-if="selectMode && (!collapsed || isMobile)"
|
class="conv-item"
|
||||||
class="conv-checkbox"
|
:class="{
|
||||||
@click.stop
|
active: !selectMode && currentConversationId === conv.conversationId,
|
||||||
|
'is-running': conv.streamStatus === 'running',
|
||||||
|
'is-selected': selectMode && selectedConvIds.includes(conv.conversationId),
|
||||||
|
'menu-open': menuConvId === conv.conversationId,
|
||||||
|
}"
|
||||||
|
@click="onConvClick(conv)"
|
||||||
>
|
>
|
||||||
<input
|
<label
|
||||||
type="checkbox"
|
v-if="selectMode && (!collapsed || isMobile)"
|
||||||
:checked="selectedConvIds.includes(conv.conversationId)"
|
class="conv-checkbox"
|
||||||
@change="toggleConvSelection(conv)"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div class="conv-icon">
|
|
||||||
<img :src="channelIconUrl(conv.source)" width="14" height="14" alt="" />
|
|
||||||
<span
|
|
||||||
v-if="conv.streamStatus === 'running'"
|
|
||||||
class="conv-running-dot"
|
|
||||||
:title="t('chat.streamGenerating')"
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
<div v-if="!collapsed || isMobile" class="conv-info">
|
|
||||||
<input
|
|
||||||
v-if="renamingConvId === conv.conversationId"
|
|
||||||
v-model="renameText"
|
|
||||||
class="conv-title-input"
|
|
||||||
@keydown.enter="confirmRename(conv)"
|
|
||||||
@keydown.escape="cancelRename"
|
|
||||||
@blur="confirmRename(conv)"
|
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
>
|
||||||
<div v-else class="conv-title" @dblclick.stop="startRename(conv)">
|
<input
|
||||||
<span>{{ conv.title }}</span>
|
type="checkbox"
|
||||||
<span
|
:checked="selectedConvIds.includes(conv.conversationId)"
|
||||||
v-if="hasUnread(conv)"
|
@change="toggleConvSelection(conv)"
|
||||||
class="conv-unread-dot"
|
/>
|
||||||
:title="t('chat.hasUnread', '有新内容')"
|
</label>
|
||||||
></span>
|
<div class="conv-icon">
|
||||||
|
<img :src="channelIconUrl(conv.source)" width="14" height="14" alt="" />
|
||||||
<span
|
<span
|
||||||
v-if="conv.streamStatus === 'running'"
|
v-if="conv.streamStatus === 'running'"
|
||||||
class="conv-running-badge"
|
class="conv-running-dot"
|
||||||
:title="t('chat.streamGenerating')"
|
:title="t('chat.streamGenerating')"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div v-if="!collapsed || isMobile" class="conv-info">
|
||||||
|
<input
|
||||||
|
v-if="renamingConvId === conv.conversationId"
|
||||||
|
v-model="renameText"
|
||||||
|
class="conv-title-input"
|
||||||
|
@keydown.enter="confirmRename(conv)"
|
||||||
|
@keydown.escape="cancelRename"
|
||||||
|
@blur="confirmRename(conv)"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<div v-else class="conv-title" @dblclick.stop="startRename(conv)">
|
||||||
|
<span>{{ conv.title }}</span>
|
||||||
|
<span
|
||||||
|
v-if="hasUnread(conv)"
|
||||||
|
class="conv-unread-dot"
|
||||||
|
:title="t('chat.hasUnread', '有新内容')"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
v-if="conv.streamStatus === 'running'"
|
||||||
|
class="conv-running-badge"
|
||||||
|
:title="t('chat.streamGenerating')"
|
||||||
|
>
|
||||||
|
<span class="conv-running-badge-pulse"></span>
|
||||||
|
{{ t('chat.streamGenerating') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="conv-meta">
|
||||||
|
<span>{{ t('chat.messages', { count: conv.messageCount }) }}</span>
|
||||||
|
<span class="conv-dot">·</span>
|
||||||
|
<span>{{ formatConversationTime(conv.lastActiveTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Single overflow ("⋮") button — opens the conversation context menu. -->
|
||||||
|
<div v-if="!selectMode && (!collapsed || isMobile)" class="conv-kebab-wrap">
|
||||||
|
<button
|
||||||
|
class="conv-kebab"
|
||||||
|
:class="{ open: menuConvId === conv.conversationId }"
|
||||||
|
@click.stop="openMenu(conv, $event)"
|
||||||
|
:title="t('common.more')"
|
||||||
>
|
>
|
||||||
<span class="conv-running-badge-pulse"></span>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="12" cy="19" r="1.6"/></svg>
|
||||||
{{ t('chat.streamGenerating') }}
|
</button>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="conv-meta">
|
|
||||||
<span>{{ t('chat.messages', { count: conv.messageCount }) }}</span>
|
|
||||||
<span class="conv-dot">·</span>
|
|
||||||
<span>{{ formatConversationTime(conv.lastActiveTime) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Single overflow ("⋮") button — opens the conversation context menu. -->
|
</McTooltip>
|
||||||
<div v-if="!selectMode && (!collapsed || isMobile)" class="conv-kebab-wrap">
|
|
||||||
<button
|
|
||||||
class="conv-kebab"
|
|
||||||
:class="{ open: menuConvId === conv.conversationId }"
|
|
||||||
@click.stop="openMenu(conv, $event)"
|
|
||||||
:title="t('common.more')"
|
|
||||||
>
|
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="12" cy="19" r="1.6"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="conversations.length === 0" class="empty-convs">
|
<div v-if="conversations.length === 0" class="empty-convs">
|
||||||
@ -181,6 +187,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { Plus } from '@element-plus/icons-vue'
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
import AgentPickerDialog from '@/components/common/AgentPickerDialog.vue'
|
import AgentPickerDialog from '@/components/common/AgentPickerDialog.vue'
|
||||||
import DropdownMenu, { type DropdownMenuItem } from '@/components/common/DropdownMenu.vue'
|
import DropdownMenu, { type DropdownMenuItem } from '@/components/common/DropdownMenu.vue'
|
||||||
|
import McTooltip from '@/components/common/McTooltip.vue'
|
||||||
import { conversationApi } from '@/api/index'
|
import { conversationApi } from '@/api/index'
|
||||||
import { channelIconUrl } from '@/utils/channelSource'
|
import { channelIconUrl } from '@/utils/channelSource'
|
||||||
import { mcToast } from '@/composables/useMcToast'
|
import { mcToast } from '@/composables/useMcToast'
|
||||||
|
|||||||
74
mateclaw-ui/src/components/common/McTooltip.vue
Normal file
74
mateclaw-ui/src/components/common/McTooltip.vue
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* MateClaw-styled tooltip.
|
||||||
|
*
|
||||||
|
* A thin wrapper over Element Plus `el-tooltip` that pins the app's
|
||||||
|
* visual identity onto every tooltip: a frosted-glass surface, rounded
|
||||||
|
* geometry, no arrow, and automatic light/dark theming. Use it as a
|
||||||
|
* drop-in replacement for `<el-tooltip>` — every prop and the default /
|
||||||
|
* `#content` slots pass straight through, while `effect`, the frosted
|
||||||
|
* `popper-class` and the arrow visibility stay owned by the wrapper so
|
||||||
|
* tooltips look identical across the app.
|
||||||
|
*/
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/** Extra class(es) merged onto the frosted popper element. */
|
||||||
|
popperClass?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// `mc-tooltip` drives the frosted styling below; callers may layer on more.
|
||||||
|
const mergedPopperClass = computed(
|
||||||
|
() => ['mc-tooltip', props.popperClass].filter(Boolean).join(' '),
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-tooltip
|
||||||
|
:show-after="80"
|
||||||
|
:offset="8"
|
||||||
|
v-bind="$attrs"
|
||||||
|
effect="mateclaw"
|
||||||
|
:show-arrow="false"
|
||||||
|
:popper-class="mergedPopperClass"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<template v-if="$slots.content" #content>
|
||||||
|
<slot name="content" />
|
||||||
|
</template>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Not scoped: el-tooltip teleports its popper to <body>, so the popper
|
||||||
|
is unreachable by scoped styles. The `mc-tooltip` popper class keeps
|
||||||
|
these rules namespaced. The `mateclaw` effect carries no Element Plus
|
||||||
|
theme of its own, so the wrapper fully owns the surface. -->
|
||||||
|
<style>
|
||||||
|
.el-popper.mc-tooltip {
|
||||||
|
background: rgba(255, 250, 245, 0.82);
|
||||||
|
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
border: 1px solid var(--mc-border-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 7px 11px;
|
||||||
|
max-width: 260px;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.5;
|
||||||
|
letter-spacing: -0.005em;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.6) inset,
|
||||||
|
0 8px 24px rgba(25, 14, 8, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .el-popper.mc-tooltip {
|
||||||
|
background: rgba(32, 26, 22, 0.86);
|
||||||
|
border-color: rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow:
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.06) inset,
|
||||||
|
0 8px 24px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1978,6 +1978,8 @@ function handleCodeCopy(e: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.agent-badge-icon {
|
.agent-badge-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,32 +41,38 @@
|
|||||||
<template v-for="group in navGroups" :key="group.key">
|
<template v-for="group in navGroups" :key="group.key">
|
||||||
<div class="nav-group">
|
<div class="nav-group">
|
||||||
<div v-if="!effectiveCollapsed" class="nav-group-title">{{ group.label }}</div>
|
<div v-if="!effectiveCollapsed" class="nav-group-title">{{ group.label }}</div>
|
||||||
<router-link
|
<McTooltip
|
||||||
v-for="item in group.items"
|
v-for="item in group.items"
|
||||||
:key="item.path"
|
:key="item.path"
|
||||||
:to="item.path"
|
:content="item.tooltip || item.label"
|
||||||
class="nav-item"
|
placement="right"
|
||||||
:class="{ active: isNavItemActive(item) }"
|
:disabled="!effectiveCollapsed"
|
||||||
:title="effectiveCollapsed ? (item.tooltip || item.label) : (item.tooltip || '')"
|
|
||||||
@click="onNavClick"
|
|
||||||
>
|
>
|
||||||
<span class="nav-icon" v-html="item.icon"></span>
|
<router-link
|
||||||
<span v-if="!effectiveCollapsed" class="nav-label">{{ item.label }}</span>
|
:to="item.path"
|
||||||
<NavBadge
|
class="nav-item"
|
||||||
v-if="item.path === '/agents' && isAdminRole"
|
:class="{ active: isNavItemActive(item) }"
|
||||||
:dot="liveAlertActive"
|
:title="effectiveCollapsed ? '' : (item.tooltip || '')"
|
||||||
tone="warning"
|
@click="onNavClick"
|
||||||
:collapsed="effectiveCollapsed"
|
>
|
||||||
:title="t('live.attention')"
|
<span class="nav-icon" v-html="item.icon"></span>
|
||||||
/>
|
<span v-if="!effectiveCollapsed" class="nav-label">{{ item.label }}</span>
|
||||||
<NavBadge
|
<NavBadge
|
||||||
v-else-if="item.path === '/security' && isAdminRole"
|
v-if="item.path === '/agents' && isAdminRole"
|
||||||
:count="pendingApprovals"
|
:dot="liveAlertActive"
|
||||||
tone="urgent"
|
tone="warning"
|
||||||
:collapsed="effectiveCollapsed"
|
:collapsed="effectiveCollapsed"
|
||||||
:title="t('notifications.pendingApprovals', { n: pendingApprovals })"
|
:title="t('live.attention')"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
<NavBadge
|
||||||
|
v-else-if="item.path === '/security' && isAdminRole"
|
||||||
|
:count="pendingApprovals"
|
||||||
|
tone="urgent"
|
||||||
|
:collapsed="effectiveCollapsed"
|
||||||
|
:title="t('notifications.pendingApprovals', { n: pendingApprovals })"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</McTooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</nav>
|
</nav>
|
||||||
@ -196,6 +202,7 @@ import OnboardingWizard from '@/views/Onboarding/OnboardingWizard.vue'
|
|||||||
import DoctorDrawer from '@/views/Doctor/DoctorDrawer.vue'
|
import DoctorDrawer from '@/views/Doctor/DoctorDrawer.vue'
|
||||||
import WorkspaceSwitcher from '@/components/workspace/WorkspaceSwitcher.vue'
|
import WorkspaceSwitcher from '@/components/workspace/WorkspaceSwitcher.vue'
|
||||||
import NavBadge from '@/components/common/NavBadge.vue'
|
import NavBadge from '@/components/common/NavBadge.vue'
|
||||||
|
import McTooltip from '@/components/common/McTooltip.vue'
|
||||||
import { useNotificationCenter } from '@/composables/useNotificationCenter'
|
import { useNotificationCenter } from '@/composables/useNotificationCenter'
|
||||||
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
import { applyLocale, currentLocale, type AppLocale } from '@/i18n'
|
import { applyLocale, currentLocale, type AppLocale } from '@/i18n'
|
||||||
@ -746,6 +753,14 @@ watch(() => workspaceStore.currentWorkspaceId, () => {
|
|||||||
|
|
||||||
/* Active indicator bar removed — active state uses bg color + font weight only */
|
/* Active indicator bar removed — active state uses bg color + font weight only */
|
||||||
|
|
||||||
|
/* Collapsed rail: the label is hidden, so center the lone icon on the
|
||||||
|
sidebar's vertical axis instead of leaving it left-aligned by the
|
||||||
|
nav-item's horizontal padding. */
|
||||||
|
.sidebar.collapsed .nav-item {
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-icon { display: flex; align-items: center; flex-shrink: 0; }
|
.nav-icon { display: flex; align-items: center; flex-shrink: 0; }
|
||||||
.nav-label { overflow: hidden; text-overflow: ellipsis; }
|
.nav-label { overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user