Files
admin-ui/src/views/home/components/Sidebar.vue

421 lines
8.6 KiB
Vue
Raw Normal View History

2026-05-26 16:01:13 +08:00
<template>
<div class="sidebar">
<div class="sidebar-header">
<div class="brand-dot"></div>
<div class="user-info">
2026-05-27 11:24:51 +08:00
<div class="ai-avatar">
<span class="ai-icon">AI</span>
</div>
2026-05-26 16:01:13 +08:00
<div class="user-details">
2026-05-27 11:24:51 +08:00
<div class="user-name">AI 助手</div>
<div class="user-status"><span class="status-dot"></span>在线</div>
2026-05-26 16:01:13 +08:00
</div>
</div>
<el-button class="new-chat-btn" @click="handleNewChat">
<el-icon><Plus /></el-icon>
新增对话
</el-button>
</div>
<div class="sidebar-menu">
<div v-for="item in menuItems" :key="item.key" class="menu-item" :class="{ active: activeMenu === item.key }" @click="handleMenuClick(item)">
<el-icon :size="18" class="menu-icon">
<component :is="item.icon" />
</el-icon>
<span class="menu-label">{{ item.label }}</span>
<el-badge v-if="item.badge" :value="item.badge" class="menu-badge" />
</div>
</div>
2026-05-26 17:58:23 +08:00
<div v-if="historyList.length" class="history-section">
<div class="history-title">历史对话</div>
<div class="history-list">
<div
v-for="item in historyList"
:key="item.id"
class="history-item"
:class="{ active: activeHistoryId === item.id }"
@click="handleSelectHistory(item.id)"
>
<div class="history-main">
<div class="history-name">{{ item.title }}</div>
<div class="history-time">{{ item.time }}</div>
</div>
<el-button text class="delete-btn" @click.stop="handleDeleteHistory(item.id)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</div>
2026-05-26 16:01:13 +08:00
<div class="sidebar-footer">
<el-button text class="footer-btn" @click="handleSettings">
<el-icon><Setting /></el-icon>
设置
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
2026-05-26 17:58:23 +08:00
import { Document, Folder, ChatDotRound, MagicStick, Cpu, VideoPlay, Setting, Plus, Delete } from '@element-plus/icons-vue';
2026-05-26 16:01:13 +08:00
import { useRouter } from 'vue-router';
interface MenuItem {
key: string;
label: string;
icon: any;
badge?: number;
route?: string;
}
2026-05-26 17:58:23 +08:00
interface HistoryItem {
id: number;
title: string;
time: string;
}
2026-05-26 16:01:13 +08:00
interface Props {
activeMenu: string;
2026-05-26 17:58:23 +08:00
activeHistoryId: number;
historyList: HistoryItem[];
2026-05-26 16:01:13 +08:00
}
interface Emits {
(e: 'menu-change', key: string): void;
(e: 'new-chat'): void;
2026-05-26 17:58:23 +08:00
(e: 'select-history', id: number): void;
(e: 'delete-history', id: number): void;
2026-05-26 16:01:13 +08:00
}
defineProps<Props>();
const emit = defineEmits<Emits>();
const router = useRouter();
const menuItems: MenuItem[] = [
{ key: 'chat', label: '对话', icon: ChatDotRound },
{ key: 'models', label: '模型管理', icon: Cpu, route: '/settings/modelConfig/modelModule' },
{ key: 'creation', label: '内容创作', icon: VideoPlay, route: '/settings/creation' },
];
const handleMenuClick = (item: MenuItem) => {
if (item.route) {
router.push(item.route);
} else {
emit('menu-change', item.key);
}
};
const handleNewChat = () => {
emit('new-chat');
};
const handleSettings = () => {
router.push('/personal');
};
2026-05-26 17:58:23 +08:00
const handleSelectHistory = (id: number) => {
emit('select-history', id);
};
const handleDeleteHistory = (id: number) => {
emit('delete-history', id);
};
2026-05-26 16:01:13 +08:00
</script>
<style scoped lang="scss">
.sidebar {
width: 252px;
2026-05-27 11:24:51 +08:00
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(249, 251, 255, 0.92) 100%);
2026-05-26 16:01:13 +08:00
border-right: 1px solid #e7edf7;
display: flex;
flex-direction: column;
height: 100%;
2026-05-27 11:24:51 +08:00
box-shadow: 8px 0 30px rgba(15, 23, 42, 0.06);
backdrop-filter: blur(10px);
2026-05-26 16:01:13 +08:00
}
.sidebar-header {
padding: 18px 16px 14px;
border-bottom: 1px solid #e9eef7;
position: relative;
}
.new-chat-btn {
2026-05-27 11:24:51 +08:00
margin-top: 12px;
2026-05-26 16:01:13 +08:00
width: 100%;
2026-05-27 11:24:51 +08:00
height: 40px;
border-radius: 12px;
border: 1px solid rgba(59, 130, 246, 0.28);
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: #ffffff;
2026-05-26 16:01:13 +08:00
font-weight: 600;
letter-spacing: 0.2px;
2026-05-27 11:24:51 +08:00
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.26);
transition: all 0.2s ease;
2026-05-26 16:01:13 +08:00
&:hover {
2026-05-27 11:24:51 +08:00
transform: translateY(-1px);
color: #ffffff;
border-color: rgba(59, 130, 246, 0.4);
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
box-shadow: 0 6px 16px rgba(37, 99, 235, 0.32);
2026-05-26 16:01:13 +08:00
}
}
.brand-dot {
position: absolute;
right: 16px;
top: 18px;
width: 8px;
height: 8px;
border-radius: 999px;
background: linear-gradient(135deg, #60a5fa 0%, #2563eb 100%);
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.14);
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
2026-05-27 11:24:51 +08:00
padding: 10px 10px 8px;
border-radius: 16px;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(147, 197, 253, 0.04) 100%);
border: 1px solid rgba(59, 130, 246, 0.18);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.ai-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 50%, #1d4ed8 100%);
box-shadow:
inset 0 2px 8px rgba(255, 255, 255, 0.35),
0 4px 12px rgba(37, 99, 235, 0.28);
flex-shrink: 0;
}
.ai-icon {
color: #fff;
font-size: 14px;
font-weight: 700;
letter-spacing: 1px;
2026-05-26 16:01:13 +08:00
}
.user-details {
flex: 1;
}
.user-name {
font-size: 15px;
font-weight: 700;
color: #111827;
margin-bottom: 4px;
letter-spacing: 0.2px;
}
.user-status {
font-size: 12px;
color: #10b981;
font-weight: 500;
2026-05-27 11:24:51 +08:00
display: flex;
align-items: center;
gap: 4px;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #10b981;
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
2026-05-26 16:01:13 +08:00
}
.sidebar-menu {
2026-05-26 17:58:23 +08:00
flex-shrink: 0;
padding: 12px 10px 0;
2026-05-26 16:01:13 +08:00
}
.menu-item {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 12px;
margin-bottom: 6px;
border-radius: 11px;
cursor: pointer;
transition: all 0.22s ease;
color: #64748b;
position: relative;
border: 1px solid transparent;
&:hover {
background: #f3f7ff;
color: #1f2937;
border-color: #e3ecfb;
}
&.active {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.16) 0%, rgba(37, 99, 235, 0.12) 100%);
color: #1f4db8;
font-weight: 600;
border-color: rgba(59, 130, 246, 0.2);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55);
&::before {
content: '';
position: absolute;
left: -2px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 18px;
background: linear-gradient(180deg, #60a5fa 0%, #2563eb 100%);
border-radius: 0 4px 4px 0;
}
}
}
.menu-icon {
opacity: 0.92;
}
.menu-label {
flex: 1;
font-size: 13px;
}
.menu-badge {
:deep(.el-badge__content) {
background: #ef4444;
border: none;
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.25);
}
}
2026-05-26 17:58:23 +08:00
.history-section {
flex: 1;
display: flex;
flex-direction: column;
margin-top: 8px;
border-top: 1px solid rgba(233, 238, 247, 0.8);
min-height: 0;
background: linear-gradient(180deg, rgba(249, 251, 255, 0.5) 0%, rgba(249, 251, 255, 0.9) 100%);
}
.history-title {
padding: 12px 12px 8px;
font-size: 11px;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.8px;
}
.history-list {
flex: 1;
padding: 0 8px 10px;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
.history-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 10px 10px;
margin-bottom: 6px;
border-radius: 10px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
background: rgba(255, 255, 255, 0.6);
&:hover {
border-color: #bfdbfe;
background: linear-gradient(135deg, rgba(239, 246, 255, 0.9) 0%, rgba(219, 234, 254, 0.7) 100%);
transform: translateX(2px);
}
&.active {
border-color: #3b82f6;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.12) 0%, rgba(59, 130, 246, 0.04) 100%);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15);
}
.delete-btn {
opacity: 0;
color: #94a3b8;
transition: all 0.2s ease;
padding: 4px;
&:hover {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
border-radius: 4px;
}
}
&:hover .delete-btn {
opacity: 1;
}
}
.history-main {
min-width: 0;
flex: 1;
}
.history-name {
font-size: 12px;
font-weight: 500;
color: #1e293b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 2px;
}
.history-time {
font-size: 11px;
color: #94a3b8;
}
2026-05-26 16:01:13 +08:00
.sidebar-footer {
2026-05-26 17:58:23 +08:00
flex-shrink: 0;
2026-05-26 16:01:13 +08:00
padding: 10px;
border-top: 1px solid #e9eef7;
}
.footer-btn {
width: 100%;
justify-content: flex-start;
gap: 8px;
color: #64748b;
font-size: 13px;
border-radius: 10px;
padding: 10px 12px;
&:hover {
background: #f4f7fd;
color: #1f2937;
}
}
</style>